1+ use std:: sync:: { Arc , RwLock } ;
2+
13use super :: algorithms:: { bezpath_algorithms:: bezpath_is_inside_bezpath, intersection:: filtered_segment_intersections} ;
24use super :: misc:: dvec2_to_point;
35use crate :: math:: math_ext:: QuadExt ;
46use crate :: math:: quad:: Quad ;
57use crate :: subpath:: Subpath ;
8+ use crate :: transform:: Transform ;
69use crate :: vector:: PointId ;
710use crate :: vector:: misc:: point_to_dvec2;
811use glam:: { DAffine2 , DMat2 , DVec2 } ;
912use kurbo:: { Affine , BezPath , ParamCurve , PathSeg , Shape } ;
1013
14+ type BoundingBox = Option < [ DVec2 ; 2 ] > ;
15+
1116#[ derive( Copy , Clone , Debug , PartialEq , serde:: Serialize , serde:: Deserialize ) ]
1217pub struct FreePoint {
1318 pub id : PointId ,
@@ -30,12 +35,99 @@ pub enum ClickTargetType {
3035 FreePoint ( FreePoint ) ,
3136}
3237
38+ /// Fixed-size ring buffer cache for rotated bounding boxes.
39+ ///
40+ /// Stores up to 8 rotation angles and their corresponding bounding boxes to avoid
41+ /// recomputing expensive bezier curve bounds for repeated rotations. Uses 7-bit
42+ /// fingerprint hashing with MSB as presence flag for fast lookup.
43+ #[ derive( Clone , Debug , Default ) ]
44+ struct BoundingBoxCache {
45+ /// Packed 7-bit fingerprints with MSB presence flags for cache lookup
46+ fingerprints : u64 ,
47+ /// (rotation_angle, cached_bounds) pairs
48+ elements : [ ( f64 , BoundingBox ) ; Self :: CACHE_SIZE ] ,
49+ /// Next position to write in ring buffer
50+ write_ptr : usize ,
51+ }
52+
53+ impl BoundingBoxCache {
54+ /// Cache size - must be ≤ 8 since fingerprints is u64 (8 bytes, 1 byte per element)
55+ const CACHE_SIZE : usize = 8 ;
56+ const FINGERPRINT_BITS : u32 = 7 ;
57+ const PRESENCE_FLAG : u8 = 1 << Self :: FINGERPRINT_BITS ;
58+
59+ /// Generates a 7-bit fingerprint from rotation with MSB as presence flag
60+ fn rotation_fingerprint ( rotation : f64 ) -> u8 {
61+ ( rotation. to_bits ( ) % ( 1 << Self :: FINGERPRINT_BITS ) ) as u8 | Self :: PRESENCE_FLAG
62+ }
63+ /// Attempts to find cached bounding box for the given rotation.
64+ /// Returns Some(bounds) if found, None if not cached.
65+ fn try_read ( & self , rotation : f64 , scale : DVec2 , translation : DVec2 , fingerprint : u8 ) -> Option < BoundingBox > {
66+ // Build bitmask of positions with matching fingerprints for vectorized comparison
67+ let mut mask: u8 = 0 ;
68+ for ( i, fp) in ( 0 ..Self :: CACHE_SIZE ) . zip ( self . fingerprints . to_le_bytes ( ) ) {
69+ // Check MSB for presence and lower 7 bits for fingerprint match
70+ if fp == fingerprint {
71+ mask |= 1 << i;
72+ }
73+ }
74+ // Check each position with matching fingerprint for exact rotation match
75+ while mask != 0 {
76+ let pos = mask. trailing_zeros ( ) as usize ;
77+
78+ if rotation == self . elements [ pos] . 0 {
79+ // Found cached rotation - apply scale and translation to cached bounds
80+ let transform = DAffine2 :: from_scale_angle_translation ( scale, 0. , translation) ;
81+ let new_bounds = self . elements [ pos] . 1 . map ( |[ a, b] | [ transform. transform_point2 ( a) , transform. transform_point2 ( b) ] ) ;
82+
83+ return Some ( new_bounds) ;
84+ }
85+ mask &= !( 1 << pos) ;
86+ }
87+ None
88+ }
89+ /// Computes and caches bounding box for the given rotation, then applies scale/translation.
90+ /// Returns the final transformed bounds.
91+ fn add_to_cache ( & mut self , subpath : & Subpath < PointId > , rotation : f64 , scale : DVec2 , translation : DVec2 , fingerprint : u8 ) -> BoundingBox {
92+ // Compute bounds for pure rotation (expensive operation we want to cache)
93+ let bounds = subpath. bounding_box_with_transform ( DAffine2 :: from_angle ( rotation) ) ;
94+
95+ if bounds. is_none ( ) {
96+ return bounds;
97+ }
98+
99+ // Store in ring buffer at current write position
100+ let write_ptr = self . write_ptr ;
101+ self . elements [ write_ptr] = ( rotation, bounds) ;
102+
103+ // Update fingerprint byte for this position
104+ let mut bytes = self . fingerprints . to_le_bytes ( ) ;
105+ bytes[ write_ptr] = fingerprint;
106+ self . fingerprints = u64:: from_le_bytes ( bytes) ;
107+
108+ // Advance write pointer (ring buffer behavior)
109+ self . write_ptr = ( write_ptr + 1 ) % Self :: CACHE_SIZE ;
110+
111+ // Apply scale and translation to cached rotated bounds
112+ let transform = DAffine2 :: from_scale_angle_translation ( scale, 0. , translation) ;
113+ bounds. map ( |[ a, b] | [ transform. transform_point2 ( a) , transform. transform_point2 ( b) ] )
114+ }
115+ }
116+
33117/// Represents a clickable target for the layer
34- #[ derive( Clone , Debug , PartialEq , serde:: Serialize , serde:: Deserialize ) ]
118+ #[ derive( Clone , Debug , serde:: Serialize , serde:: Deserialize ) ]
35119pub struct ClickTarget {
36120 target_type : ClickTargetType ,
37121 stroke_width : f64 ,
38- bounding_box : Option < [ DVec2 ; 2 ] > ,
122+ bounding_box : BoundingBox ,
123+ #[ serde( skip) ]
124+ bounding_box_cache : Arc < RwLock < BoundingBoxCache > > ,
125+ }
126+
127+ impl PartialEq for ClickTarget {
128+ fn eq ( & self , other : & Self ) -> bool {
129+ self . target_type == other. target_type && self . stroke_width == other. stroke_width && self . bounding_box == other. bounding_box
130+ }
39131}
40132
41133impl ClickTarget {
@@ -45,6 +137,7 @@ impl ClickTarget {
45137 target_type : ClickTargetType :: Subpath ( subpath) ,
46138 stroke_width,
47139 bounding_box,
140+ bounding_box_cache : Default :: default ( ) ,
48141 }
49142 }
50143
@@ -60,23 +153,52 @@ impl ClickTarget {
60153 target_type : ClickTargetType :: FreePoint ( point) ,
61154 stroke_width,
62155 bounding_box,
156+ bounding_box_cache : Default :: default ( ) ,
63157 }
64158 }
65159
66160 pub fn target_type ( & self ) -> & ClickTargetType {
67161 & self . target_type
68162 }
69163
70- pub fn bounding_box ( & self ) -> Option < [ DVec2 ; 2 ] > {
164+ pub fn bounding_box ( & self ) -> BoundingBox {
71165 self . bounding_box
72166 }
73167
74168 pub fn bounding_box_center ( & self ) -> Option < DVec2 > {
75169 self . bounding_box . map ( |bbox| bbox[ 0 ] + ( bbox[ 1 ] - bbox[ 0 ] ) / 2. )
76170 }
77171
78- pub fn bounding_box_with_transform ( & self , transform : DAffine2 ) -> Option < [ DVec2 ; 2 ] > {
79- self . bounding_box . map ( |[ a, b] | [ transform. transform_point2 ( a) , transform. transform_point2 ( b) ] )
172+ pub fn bounding_box_with_transform ( & self , transform : DAffine2 ) -> BoundingBox {
173+ match self . target_type {
174+ ClickTargetType :: Subpath ( ref subpath) => {
175+ // Bypass cache for skewed transforms since rotation decomposition isn't valid
176+ if transform. has_skew ( ) {
177+ return subpath. bounding_box_with_transform ( transform) ;
178+ }
179+
180+ // Decompose transform into rotation, scale, translation for caching strategy
181+ let rotation = transform. decompose_rotation ( ) ;
182+ let scale = transform. decompose_scale ( ) ;
183+ let translation = transform. translation ;
184+
185+ // Generate fingerprint for cache lookup
186+ let fingerprint = BoundingBoxCache :: rotation_fingerprint ( rotation) ;
187+
188+ // Try to read from cache first
189+ let read_lock = self . bounding_box_cache . read ( ) . unwrap ( ) ;
190+ if let Some ( value) = read_lock. try_read ( rotation, scale, translation, fingerprint) {
191+ return value;
192+ }
193+ std:: mem:: drop ( read_lock) ;
194+
195+ // Cache miss - compute and store new entry
196+ let mut write_lock = self . bounding_box_cache . write ( ) . unwrap ( ) ;
197+ write_lock. add_to_cache ( subpath, rotation, scale, translation, fingerprint)
198+ }
199+ // TODO: use point for calculation of bbox
200+ ClickTargetType :: FreePoint ( _) => self . bounding_box . map ( |[ a, b] | [ transform. transform_point2 ( a) , transform. transform_point2 ( b) ] ) ,
201+ }
80202 }
81203
82204 pub fn apply_transform ( & mut self , affine_transform : DAffine2 ) {
@@ -170,3 +292,165 @@ impl ClickTarget {
170292 }
171293 }
172294}
295+
296+ #[ cfg( test) ]
297+ mod tests {
298+ use super :: * ;
299+ use crate :: subpath:: Subpath ;
300+ use glam:: DVec2 ;
301+ use std:: f64:: consts:: PI ;
302+
303+ #[ test]
304+ fn test_bounding_box_cache_fingerprint_generation ( ) {
305+ // Test that fingerprints have MSB set and use only 7 bits for data
306+ let rotation1 = 0.0 ;
307+ let rotation2 = PI / 3.0 ;
308+ let rotation3 = PI / 2.0 ;
309+
310+ let fp1 = BoundingBoxCache :: rotation_fingerprint ( rotation1) ;
311+ let fp2 = BoundingBoxCache :: rotation_fingerprint ( rotation2) ;
312+ let fp3 = BoundingBoxCache :: rotation_fingerprint ( rotation3) ;
313+
314+ // All fingerprints should have MSB set (presence flag)
315+ assert_eq ! ( fp1 & BoundingBoxCache :: PRESENCE_FLAG , BoundingBoxCache :: PRESENCE_FLAG ) ;
316+ assert_eq ! ( fp2 & BoundingBoxCache :: PRESENCE_FLAG , BoundingBoxCache :: PRESENCE_FLAG ) ;
317+ assert_eq ! ( fp3 & BoundingBoxCache :: PRESENCE_FLAG , BoundingBoxCache :: PRESENCE_FLAG ) ;
318+
319+ // Lower 7 bits should contain the actual fingerprint data
320+ let data1 = fp1 & !BoundingBoxCache :: PRESENCE_FLAG ;
321+ let data2 = fp2 & !BoundingBoxCache :: PRESENCE_FLAG ;
322+ let data3 = fp3 & !BoundingBoxCache :: PRESENCE_FLAG ;
323+
324+ // Data portions should be different (unless collision)
325+ assert ! ( data1 != data2 && data2 != data3 && data3 != data1) ;
326+ }
327+
328+ #[ test]
329+ fn test_bounding_box_cache_basic_operations ( ) {
330+ let mut cache = BoundingBoxCache :: default ( ) ;
331+
332+ // Create a simple rectangle subpath for testing
333+ let subpath = Subpath :: new_rect ( DVec2 :: ZERO , DVec2 :: new ( 100.0 , 50.0 ) ) ;
334+
335+ let rotation = PI / 4.0 ;
336+ let scale = DVec2 :: new ( 2.0 , 2.0 ) ;
337+ let translation = DVec2 :: new ( 10.0 , 20.0 ) ;
338+ let fingerprint = BoundingBoxCache :: rotation_fingerprint ( rotation) ;
339+
340+ // Cache should be empty initially
341+ assert ! ( cache. try_read( rotation, scale, translation, fingerprint) . is_none( ) ) ;
342+
343+ // Add to cache
344+ let result = cache. add_to_cache ( & subpath, rotation, scale, translation, fingerprint) ;
345+ assert ! ( result. is_some( ) ) ;
346+
347+ // Should now be able to read from cache
348+ let cached = cache. try_read ( rotation, scale, translation, fingerprint) ;
349+ assert ! ( cached. is_some( ) ) ;
350+ assert_eq ! ( cached. unwrap( ) , result) ;
351+ }
352+
353+ #[ test]
354+ fn test_bounding_box_cache_ring_buffer_behavior ( ) {
355+ let mut cache = BoundingBoxCache :: default ( ) ;
356+ let subpath = Subpath :: new_rect ( DVec2 :: ZERO , DVec2 :: new ( 10.0 , 10.0 ) ) ;
357+ let scale = DVec2 :: ONE ;
358+ let translation = DVec2 :: ZERO ;
359+
360+ // Fill cache beyond capacity to test ring buffer behavior
361+ let rotations: Vec < f64 > = ( 0 ..10 ) . map ( |i| i as f64 * PI / 8.0 ) . collect ( ) ;
362+
363+ for rotation in & rotations {
364+ let fingerprint = BoundingBoxCache :: rotation_fingerprint ( * rotation) ;
365+ cache. add_to_cache ( & subpath, * rotation, scale, translation, fingerprint) ;
366+ }
367+
368+ // First two entries should be overwritten (cache size is 8)
369+ let first_fp = BoundingBoxCache :: rotation_fingerprint ( rotations[ 0 ] ) ;
370+ let second_fp = BoundingBoxCache :: rotation_fingerprint ( rotations[ 1 ] ) ;
371+ let last_fp = BoundingBoxCache :: rotation_fingerprint ( rotations[ 9 ] ) ;
372+
373+ assert ! ( cache. try_read( rotations[ 0 ] , scale, translation, first_fp) . is_none( ) ) ;
374+ assert ! ( cache. try_read( rotations[ 1 ] , scale, translation, second_fp) . is_none( ) ) ;
375+ assert ! ( cache. try_read( rotations[ 9 ] , scale, translation, last_fp) . is_some( ) ) ;
376+ }
377+
378+ #[ test]
379+ fn test_click_target_bounding_box_caching ( ) {
380+ // Create a click target with a simple rectangle
381+ let subpath = Subpath :: new_rect ( DVec2 :: ZERO , DVec2 :: new ( 100.0 , 50.0 ) ) ;
382+ let click_target = ClickTarget :: new_with_subpath ( subpath, 1.0 ) ;
383+
384+ let rotation = PI / 6.0 ;
385+ let scale = DVec2 :: new ( 1.5 , 1.5 ) ;
386+ let translation = DVec2 :: new ( 20.0 , 30.0 ) ;
387+ let transform = DAffine2 :: from_scale_angle_translation ( scale, rotation, translation) ;
388+
389+ // Helper function to count present values in cache
390+ let count_present_values = || {
391+ let cache = click_target. bounding_box_cache . read ( ) . unwrap ( ) ;
392+ cache. fingerprints . to_le_bytes ( ) . iter ( ) . filter ( |& & fp| fp & BoundingBoxCache :: PRESENCE_FLAG != 0 ) . count ( )
393+ } ;
394+
395+ // Initially cache should be empty
396+ assert_eq ! ( count_present_values( ) , 0 ) ;
397+
398+ // First call should compute and cache
399+ let result1 = click_target. bounding_box_with_transform ( transform) ;
400+ assert ! ( result1. is_some( ) ) ;
401+ assert_eq ! ( count_present_values( ) , 1 ) ;
402+
403+ // Second call with same transform should use cache, not add new entry
404+ let result2 = click_target. bounding_box_with_transform ( transform) ;
405+ assert_eq ! ( result1, result2) ;
406+ assert_eq ! ( count_present_values( ) , 1 ) ; // Should still be 1, not 2
407+
408+ // Different scale/translation but same rotation should use cached rotation
409+ let transform2 = DAffine2 :: from_scale_angle_translation ( DVec2 :: new ( 2.0 , 2.0 ) , rotation, DVec2 :: new ( 50.0 , 60.0 ) ) ;
410+ let result3 = click_target. bounding_box_with_transform ( transform2) ;
411+ assert ! ( result3. is_some( ) ) ;
412+ assert_ne ! ( result1, result3) ; // Different due to different scale/translation
413+ assert_eq ! ( count_present_values( ) , 1 ) ; // Should still be 1, reused same rotation
414+ }
415+
416+ #[ test]
417+ fn test_click_target_skew_bypass_cache ( ) {
418+ let subpath = Subpath :: new_rect ( DVec2 :: ZERO , DVec2 :: new ( 100.0 , 50.0 ) ) ;
419+ let click_target = ClickTarget :: new_with_subpath ( subpath. clone ( ) , 1.0 ) ;
420+
421+ // Create a transform with skew (non-uniform scaling in different directions)
422+ let skew_transform = DAffine2 :: from_cols_array ( & [ 2.0 , 0.5 , 0.0 , 1.0 , 10.0 , 20.0 ] ) ;
423+ assert ! ( skew_transform. has_skew( ) ) ;
424+
425+ // Should bypass cache and compute directly
426+ let result = click_target. bounding_box_with_transform ( skew_transform) ;
427+ let expected = subpath. bounding_box_with_transform ( skew_transform) ;
428+ assert_eq ! ( result, expected) ;
429+ }
430+
431+ #[ test]
432+ fn test_cache_fingerprint_collision_handling ( ) {
433+ let mut cache = BoundingBoxCache :: default ( ) ;
434+ let subpath = Subpath :: new_rect ( DVec2 :: ZERO , DVec2 :: new ( 10.0 , 10.0 ) ) ;
435+ let scale = DVec2 :: ONE ;
436+ let translation = DVec2 :: ZERO ;
437+
438+ // Find two rotations that produce the same fingerprint (collision)
439+ let rotation1 = 0.0 ;
440+ let rotation2 = 0.25 ;
441+ let fp1 = BoundingBoxCache :: rotation_fingerprint ( rotation1) ;
442+ let fp2 = BoundingBoxCache :: rotation_fingerprint ( rotation2) ;
443+
444+ // If we found a collision, test that exact rotation matching still works
445+ if fp1 == fp2 && rotation1 != rotation2 {
446+ // Add first rotation
447+ cache. add_to_cache ( & subpath, rotation1, scale, translation, fp1) ;
448+
449+ // Should find the exact rotation
450+ assert ! ( cache. try_read( rotation1, scale, translation, fp1) . is_some( ) ) ;
451+
452+ // Should not find the colliding rotation (different exact value)
453+ assert ! ( cache. try_read( rotation2, scale, translation, fp2) . is_none( ) ) ;
454+ }
455+ }
456+ }
0 commit comments