-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Add additional boolean operations #4122
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -14,8 +14,6 @@ use vector_types::kurbo::{Affine, BezPath, CubicBez, Line, ParamCurve, PathSeg, | |||||||||||||||||||||||||||||||||||||||||||||||||||||
| pub use vector_types::vector::misc::BooleanOperation; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // TODO: Fix boolean ops to work by removing .transform() and .one_instance_*() calls, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // TODO: since before we used a Vec of single-item `Table`s and now we use a single `Table` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // TODO: with multiple items while still assuming a single item for the boolean operations. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// 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. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| #[node_macro::node(category("Vector: Modifier"), memoize)] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -36,24 +34,29 @@ async fn boolean_operation<I: graphic_types::IntoGraphicTable + 'n + Send + Clon | |||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // The first index is the bottom of the stack | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let flattened = flatten_vector(&content); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let mut result_vector_table = boolean_operation_on_vector_table(&flattened, operation); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let mut result_vector_table = match operation { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| BooleanOperation::Union | BooleanOperation::SubtractFront | BooleanOperation::SubtractBack | BooleanOperation::Intersect | BooleanOperation::Difference => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| boolean_operation_on_vector_table(&flattened, operation) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| BooleanOperation::Trim | BooleanOperation::Crop => cascading_subtract(&flattened, operation), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Replace the transformation matrix with a mutation of the vector points themselves | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if result_vector_table.element_mut(0).is_some() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let transform: DAffine2 = result_vector_table.attribute_cloned_or_default(ATTR_TRANSFORM, 0); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| result_vector_table.set_attribute(ATTR_TRANSFORM, 0, DAffine2::IDENTITY); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for i in 0..result_vector_table.len() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let transform: DAffine2 = result_vector_table.attribute_cloned_or_default(ATTR_TRANSFORM, i); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| result_vector_table.set_attribute(ATTR_TRANSFORM, i, DAffine2::IDENTITY); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let result_vector = result_vector_table.element_mut(0).unwrap(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let result_vector = result_vector_table.element_mut(i).unwrap(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Vector::transform(result_vector, transform); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| result_vector.style.set_stroke_transform(DAffine2::IDENTITY); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Snapshot the input layers as the `editor:merged_layers` attribute so the renderer can recurse into them | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // for editor click-target preservation. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| result_vector_table.set_attribute(ATTR_EDITOR_MERGED_LAYERS, 0, content.clone()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| result_vector_table.set_attribute(ATTR_EDITOR_MERGED_LAYERS, i, content.clone()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Clean up the boolean operation result by merging duplicated points | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let merge_transform: DAffine2 = result_vector_table.attribute_cloned_or_default(ATTR_TRANSFORM, 0); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| result_vector_table.element_mut(0).unwrap().merge_by_distance_spatial(merge_transform, 0.0001); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let merge_transform: DAffine2 = result_vector_table.attribute_cloned_or_default(ATTR_TRANSFORM, i); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| result_vector_table.element_mut(i).unwrap().merge_by_distance_spatial(merge_transform, 0.0001); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| result_vector_table | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -109,14 +112,36 @@ impl WindingNumber { | |||||||||||||||||||||||||||||||||||||||||||||||||||||
| BooleanOperation::SubtractBack => self.elems.last().is_some_and(is_in) && self.elems.iter().rev().skip(1).all(is_out), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| BooleanOperation::Intersect => !self.elems.is_empty() && self.elems.iter().all(is_in), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| BooleanOperation::Difference => self.elems.iter().any(is_in) && !self.elems.iter().all(is_in), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| BooleanOperation::Trim => unreachable!(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| BooleanOperation::Crop => unreachable!(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fn subtract_front_at(&self, i: usize) -> bool { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let is_in = |v: &i16| *v != 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.elems.get(i).is_some_and(is_in) && self.elems.iter().skip(i + 1).all(|v| !is_in(v)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fn crop_visible_at(&self, i: usize) -> bool { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let is_in = |v: &i16| *v != 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if self.elems.is_empty() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let top_index = self.elems.len() - 1; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if i >= top_index { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fn boolean_operation_on_vector_table(vector: &Table<Vector>, boolean_operation: BooleanOperation) -> Table<Vector> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const EPSILON: f64 = 1e-5; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let mut table = Table::new(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let mut paths = Vec::new(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let copy_from_index = if matches!(boolean_operation, BooleanOperation::SubtractFront) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if !vector.is_empty() { Some(0) } else { None } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -137,29 +162,97 @@ fn boolean_operation_on_vector_table(vector: &Table<Vector>, boolean_operation: | |||||||||||||||||||||||||||||||||||||||||||||||||||||
| TableRow::<Vector>::default() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let top = match try_create_topology(vector) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Some(top) => top, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| None => return table, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let contours = top.contours(|winding| winding.is_inside(boolean_operation)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| append_linesweeper_contours(row.element_mut(), &contours); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| table.push(row); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+170
to
+174
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This code pushes a row to the result table even if the boolean operation produced no geometry (empty contours). It is better to skip adding the row if the result is empty to avoid polluting the document with invisible layers.
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| table | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fn cascading_subtract(vector: &Table<Vector>, boolean_operation: BooleanOperation) -> Table<Vector> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let mut table = Table::new(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let top = match try_create_topology(vector) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Some(top) => top, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| None => return table, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let end_index = match boolean_operation { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| BooleanOperation::Crop => vector.len().saturating_sub(1), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _ => vector.len(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let predicate = match boolean_operation { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| BooleanOperation::Crop => WindingNumber::crop_visible_at, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _ => WindingNumber::subtract_front_at, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for i in 0..end_index { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let contours = top.contours(|w| predicate(w, i)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let source = match vector.element(i) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Some(source) => source, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| None => continue, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let mut attributes = vector.clone_row_attributes(i); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| attributes.insert(ATTR_TRANSFORM, DAffine2::IDENTITY); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let mut element = Vector { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| style: source.style.clone(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ..Default::default() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| append_linesweeper_contours(&mut element, &contours); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let row = TableRow::from_parts(element, attributes); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| table.push(row); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+196
to
+216
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Similar to the general boolean operation, for i in 0..end_index {
let contours = top.contours(|w| predicate(w, i));
if contours.contours().next().is_none() {
continue;
}
let source = match vector.element(i) {
Some(source) => source,
None => continue,
};
let mut attributes = vector.clone_row_attributes(i);
attributes.insert(ATTR_TRANSFORM, DAffine2::IDENTITY);
let mut element = Vector {
style: source.style.clone(),
..Default::default()
};
append_linesweeper_contours(&mut element, &contours);
let row = TableRow::from_parts(element, attributes);
table.push(row);
}References
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if boolean_operation == BooleanOperation::Crop { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let top_remainder = boolean_operation_on_vector_table(vector, BooleanOperation::SubtractBack); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if let Some(mut row) = top_remainder.clone_row(0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let result_vector = row.element_mut(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| result_vector.style.clear_fill(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| result_vector.style.clear_stroke(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| table.push(row); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+218
to
+226
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Calling
Suggested change
References
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| table | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fn try_create_topology(vector: &Table<Vector>) -> Option<Topology<WindingNumber>> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const EPSILON: f64 = 1e-5; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let mut paths = Vec::new(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for index in 0..vector.len() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let element = vector.element(index).unwrap(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| paths.push(to_bez_path(element, vector.attribute_cloned_or_default(ATTR_TRANSFORM, index))); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let top = match Topology::<WindingNumber>::from_paths(paths.iter().enumerate().map(|(idx, path)| (path, (idx, paths.len()))), EPSILON) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Ok(top) => top, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| match Topology::<WindingNumber>::from_paths(paths.iter().enumerate().map(|(idx, path)| (path, (idx, paths.len()))), EPSILON) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Ok(top) => Some(top), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Err(e) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| log::error!("Boolean operation failed while building topology: {e}"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| table.push(row); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return table; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| None | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let contours = top.contours(|winding| winding.is_inside(boolean_operation)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fn append_linesweeper_contours(vector: &mut Vector, contours: &linesweeper::topology::Contours) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 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). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // TODO: Report this upstream to Linesweeper and remove this `.reverse()` workaround once fixed. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for subpath in from_bez_paths(contours.contours().map(|c| &c.path)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| row.element_mut().append_subpath(subpath.reverse(), false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| vector.append_subpath(subpath.reverse(), false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| table.push(row); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| table | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fn flatten_vector(graphic_table: &Table<Graphic>) -> Table<Vector> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The transformation logic in this loop is redundant because the helper functions (
boolean_operation_on_vector_tableandcascading_subtract) already set theATTR_TRANSFORMattribute toDAffine2::IDENTITYand bake the original transforms into the geometry. Consequently,transformwill always be identity here, makingVector::transforma no-op.Additionally, fetching
merge_transformfrom the table after it has been set to identity is unnecessary; you can simply passDAffine2::IDENTITYtomerge_by_distance_spatial.References