@@ -14,8 +14,6 @@ use vector_types::kurbo::{Affine, BezPath, CubicBez, Line, ParamCurve, PathSeg,
1414pub use vector_types:: vector:: misc:: BooleanOperation ;
1515
1616// TODO: Fix boolean ops to work by removing .transform() and .one_instance_*() calls,
17- // TODO: since before we used a Vec of single-item `List`s and now we use a single `List`
18- // TODO: with multiple items while still assuming a single item for the boolean operations.
1917
2018/// Combines the geometric forms of one or more closed paths into a new vector path that results from cutting or joining the paths by the chosen method.
2119#[ node_macro:: node( category( "Vector: Modifier" ) , memoize) ]
@@ -36,24 +34,30 @@ async fn boolean_operation<I: graphic_types::IntoGraphicList + 'n + Send + Clone
3634
3735 // The first index is the bottom of the stack
3836 let flattened = flatten_vector ( & content) ;
39- let mut result_vector_list = boolean_operation_on_vector_list ( & flattened, operation) ;
37+
38+ let mut result_vector_list = match operation {
39+ BooleanOperation :: Union | BooleanOperation :: SubtractFront | BooleanOperation :: SubtractBack | BooleanOperation :: Intersect | BooleanOperation :: Difference => {
40+ boolean_operation_on_vector_list ( & flattened, operation)
41+ }
42+ BooleanOperation :: Trim | BooleanOperation :: Crop => cascading_subtract ( & flattened, operation) ,
43+ } ;
4044
4145 // Replace the transformation matrix with a mutation of the vector points themselves
42- if result_vector_list . element_mut ( 0 ) . is_some ( ) {
43- let transform: DAffine2 = result_vector_list. attribute_cloned_or_default ( ATTR_TRANSFORM , 0 ) ;
44- result_vector_list. set_attribute ( ATTR_TRANSFORM , 0 , DAffine2 :: IDENTITY ) ;
46+ for i in 0 ..result_vector_list . len ( ) {
47+ let transform: DAffine2 = result_vector_list. attribute_cloned_or_default ( ATTR_TRANSFORM , i ) ;
48+ result_vector_list. set_attribute ( ATTR_TRANSFORM , i , DAffine2 :: IDENTITY ) ;
4549
46- let result_vector = result_vector_list. element_mut ( 0 ) . unwrap ( ) ;
50+ let result_vector = result_vector_list. element_mut ( i ) . unwrap ( ) ;
4751 Vector :: transform ( result_vector, transform) ;
4852 result_vector. style . set_stroke_transform ( DAffine2 :: IDENTITY ) ;
4953
5054 // Snapshot the input layers as the `editor:merged_layers` attribute so the renderer can recurse into them
5155 // for editor click-target preservation.
52- result_vector_list. set_attribute ( ATTR_EDITOR_MERGED_LAYERS , 0 , content. clone ( ) ) ;
56+ result_vector_list. set_attribute ( ATTR_EDITOR_MERGED_LAYERS , i , content. clone ( ) ) ;
5357
5458 // Clean up the boolean operation result by merging duplicated points
55- let merge_transform: DAffine2 = result_vector_list. attribute_cloned_or_default ( ATTR_TRANSFORM , 0 ) ;
56- result_vector_list. element_mut ( 0 ) . unwrap ( ) . merge_by_distance_spatial ( merge_transform, 0.0001 ) ;
59+ let merge_transform: DAffine2 = result_vector_list. attribute_cloned_or_default ( ATTR_TRANSFORM , i ) ;
60+ result_vector_list. element_mut ( i ) . unwrap ( ) . merge_by_distance_spatial ( merge_transform, 0.0001 ) ;
5761 }
5862
5963 result_vector_list
@@ -109,21 +113,43 @@ impl WindingNumber {
109113 BooleanOperation :: SubtractBack => self . elems . last ( ) . is_some_and ( is_in) && self . elems . iter ( ) . rev ( ) . skip ( 1 ) . all ( is_out) ,
110114 BooleanOperation :: Intersect => !self . elems . is_empty ( ) && self . elems . iter ( ) . all ( is_in) ,
111115 BooleanOperation :: Difference => self . elems . iter ( ) . any ( is_in) && !self . elems . iter ( ) . all ( is_in) ,
116+ BooleanOperation :: Trim => unreachable ! ( ) ,
117+ BooleanOperation :: Crop => unreachable ! ( ) ,
118+ }
119+ }
120+
121+ fn subtract_front_at ( & self , i : usize ) -> bool {
122+ let is_in = |v : & i16 | * v != 0 ;
123+
124+ self . elems . get ( i) . is_some_and ( is_in) && self . elems . iter ( ) . skip ( i + 1 ) . all ( |v| !is_in ( v) )
125+ }
126+
127+ fn crop_visible_at ( & self , i : usize ) -> bool {
128+ let is_in = |v : & i16 | * v != 0 ;
129+
130+ if self . elems . is_empty ( ) {
131+ return false ;
132+ }
133+
134+ let top_index = self . elems . len ( ) - 1 ;
135+
136+ if i >= top_index {
137+ return false ;
112138 }
139+
140+ self . elems . get ( i) . is_some_and ( is_in) && self . elems . get ( top_index) . is_some_and ( is_in) && self . elems [ i + 1 ..top_index] . iter ( ) . all ( |v| !is_in ( v) )
113141 }
114142}
115143
116144fn boolean_operation_on_vector_list ( vector : & List < Vector > , boolean_operation : BooleanOperation ) -> List < Vector > {
117- const EPSILON : f64 = 1e-5 ;
118145 let mut list = List :: new ( ) ;
119- let mut paths = Vec :: new ( ) ;
120146
121147 let copy_from_index = if matches ! ( boolean_operation, BooleanOperation :: SubtractFront ) {
122148 if !vector. is_empty ( ) { Some ( 0 ) } else { None }
123149 } else {
124150 if !vector. is_empty ( ) { Some ( vector. len ( ) - 1 ) } else { None }
125151 } ;
126- let mut row = if let Some ( index) = copy_from_index {
152+ let mut item = if let Some ( index) = copy_from_index {
127153 let mut attributes = vector. clone_item_attributes ( index) ;
128154 // The boolean op bakes input transforms into the output geometry, so the result item carries no transform of its own
129155 attributes. insert ( ATTR_TRANSFORM , DAffine2 :: IDENTITY ) ;
@@ -137,29 +163,94 @@ fn boolean_operation_on_vector_list(vector: &List<Vector>, boolean_operation: Bo
137163 Item :: < Vector > :: default ( )
138164 } ;
139165
166+ let top = match try_create_topology ( vector) {
167+ Some ( top) => top,
168+ None => return list,
169+ } ;
170+
171+ let contours = top. contours ( |winding| winding. is_inside ( boolean_operation) ) ;
172+
173+ if contours. contours ( ) . next ( ) . is_some ( ) {
174+ append_linesweeper_contours ( item. element_mut ( ) , & contours) ;
175+ list. push ( item) ;
176+ }
177+
178+ list
179+ }
180+
181+ fn cascading_subtract ( vector : & List < Vector > , boolean_operation : BooleanOperation ) -> List < Vector > {
182+ let mut list = List :: new ( ) ;
183+
184+ let top = match try_create_topology ( vector) {
185+ Some ( top) => top,
186+ None => return list,
187+ } ;
188+
189+ for i in 0 ..vector. len ( ) {
190+ let contours = match boolean_operation {
191+ BooleanOperation :: Crop if i == vector. len ( ) - 1 => top. contours ( |winding| winding. is_inside ( BooleanOperation :: SubtractBack ) ) ,
192+
193+ BooleanOperation :: Crop => top. contours ( |winding| winding. crop_visible_at ( i) ) ,
194+
195+ _ => top. contours ( |winding| winding. subtract_front_at ( i) ) ,
196+ } ;
197+
198+ if contours. contours ( ) . next ( ) . is_none ( ) {
199+ continue ;
200+ }
201+
202+ let source = match vector. element ( i) {
203+ Some ( source) => source,
204+ None => continue ,
205+ } ;
206+
207+ let mut attributes = vector. clone_item_attributes ( i) ;
208+ attributes. insert ( ATTR_TRANSFORM , DAffine2 :: IDENTITY ) ;
209+
210+ let mut element = Vector {
211+ style : source. style . clone ( ) ,
212+ ..Default :: default ( )
213+ } ;
214+
215+ if boolean_operation == BooleanOperation :: Crop && i == vector. len ( ) - 1 {
216+ element. style . clear_fill ( ) ;
217+ element. style . clear_stroke ( ) ;
218+ }
219+
220+ append_linesweeper_contours ( & mut element, & contours) ;
221+
222+ let item = Item :: from_parts ( element, attributes) ;
223+ list. push ( item) ;
224+ }
225+
226+ list
227+ }
228+
229+ fn try_create_topology ( vector : & List < Vector > ) -> Option < Topology < WindingNumber > > {
230+ const EPSILON : f64 = 1e-5 ;
231+
232+ let mut paths = Vec :: new ( ) ;
233+
140234 for index in 0 ..vector. len ( ) {
141235 let element = vector. element ( index) . unwrap ( ) ;
142236 paths. push ( to_bez_path ( element, vector. attribute_cloned_or_default ( ATTR_TRANSFORM , index) ) ) ;
143237 }
144238
145- let top = match Topology :: < WindingNumber > :: from_paths ( paths. iter ( ) . enumerate ( ) . map ( |( idx, path) | ( path, ( idx, paths. len ( ) ) ) ) , EPSILON ) {
146- Ok ( top) => top,
239+ match Topology :: < WindingNumber > :: from_paths ( paths. iter ( ) . enumerate ( ) . map ( |( idx, path) | ( path, ( idx, paths. len ( ) ) ) ) , EPSILON ) {
240+ Ok ( top) => Some ( top) ,
147241 Err ( e) => {
148242 log:: error!( "Boolean operation failed while building topology: {e}" ) ;
149- list. push ( row) ;
150- return list;
243+ None
151244 }
152- } ;
153- let contours = top . contours ( |winding| winding . is_inside ( boolean_operation ) ) ;
245+ }
246+ }
154247
248+ fn append_linesweeper_contours ( vector : & mut Vector , contours : & linesweeper:: topology:: Contours ) {
155249 // TODO: Linesweeper emits contours in the opposite winding direction from the rest of Kurbo's and Graphite's vector graphics system (clockwise in screen coordinates).
156250 // TODO: Report this upstream to Linesweeper and remove this `.reverse()` workaround once fixed.
157251 for subpath in from_bez_paths ( contours. contours ( ) . map ( |c| & c. path ) ) {
158- row . element_mut ( ) . append_subpath ( subpath. reverse ( ) , false ) ;
252+ vector . append_subpath ( subpath. reverse ( ) , false ) ;
159253 }
160-
161- list. push ( row) ;
162- list
163254}
164255
165256fn flatten_vector ( graphic_list : & List < Graphic > ) -> List < Vector > {
0 commit comments