Skip to content

Commit 14dbdb3

Browse files
committed
Add Trim and Crop boolean operations
1 parent 629a1f4 commit 14dbdb3

3 files changed

Lines changed: 134 additions & 23 deletions

File tree

editor/src/messages/menu_bar/menu_bar_message_handler.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,22 @@ impl LayoutHolder for MenuBarMessageHandler {
494494
DocumentMessage::GroupSelectedLayers { group_folder_type }.into()
495495
})
496496
.disabled(no_active_document || !has_selected_layers),
497+
MenuListEntry::new("Trim")
498+
.label("Trim")
499+
.icon("BooleanSubtractFront")
500+
.on_commit(|_| {
501+
let group_folder_type = GroupFolderType::BooleanOperation(BooleanOperation::Trim);
502+
DocumentMessage::GroupSelectedLayers { group_folder_type }.into()
503+
})
504+
.disabled(no_active_document || !has_selected_layers),
505+
MenuListEntry::new("Crop")
506+
.label("Crop")
507+
.icon("BooleanSubtractFront")
508+
.on_commit(|_| {
509+
let group_folder_type = GroupFolderType::BooleanOperation(BooleanOperation::Crop);
510+
DocumentMessage::GroupSelectedLayers { group_folder_type }.into()
511+
})
512+
.disabled(no_active_document || !has_selected_layers),
497513
]]),
498514
MenuListEntry::new("Blend")
499515
.label("Blend")

node-graph/libraries/vector-types/src/vector/misc.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ pub enum BooleanOperation {
2323
Intersect,
2424
#[icon("BooleanDifference")]
2525
Difference,
26+
#[icon("BooleanSubtractFront")]
27+
Trim,
28+
#[icon("BooleanSubtractFront")]
29+
Crop,
2630
}
2731

2832
/// Represents different geometric interpretations of calculating the centroid (center of mass).

node-graph/nodes/path-bool/src/lib.rs

Lines changed: 114 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@ use vector_types::kurbo::{Affine, BezPath, CubicBez, Line, ParamCurve, PathSeg,
1414
pub 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

116144
fn 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

165256
fn flatten_vector(graphic_list: &List<Graphic>) -> List<Vector> {

0 commit comments

Comments
 (0)