diff --git a/Cargo.lock b/Cargo.lock index 853d170aa2..a23863c93f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -202,6 +202,9 @@ name = "arrayvec" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +dependencies = [ + "serde", +] [[package]] name = "as-raw-xcb-connection" @@ -3391,6 +3394,19 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4a5ff6bcca6c4867b1c4fd4ef63e4db7436ef363e0ad7531d1558856bae64f4" +[[package]] +name = "linesweeper" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8421b276e96af0ace5f3d8d2d165d0dea07fe764d2fe94ec06bb1acaf8a1e759" +dependencies = [ + "arrayvec", + "kurbo", + "polycool", + "rustc-hash 2.1.1", + "smallvec", +] + [[package]] name = "linux-raw-sys" version = "0.9.4" @@ -4289,10 +4305,11 @@ dependencies = [ "dyn-any", "glam", "graphic-types", + "linesweeper", "log", "node-macro", - "path-bool", "serde", + "smallvec", "tsify", "vector-types", "wasm-bindgen", diff --git a/Cargo.toml b/Cargo.toml index a7bd74b4f0..9c58c7cd19 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -212,6 +212,8 @@ cargo-gpu = { git = "https://github.com/Firestar99/cargo-gpu", rev = "3952a22d16 qrcodegen = "1.8" lzma-rust2 = { version = "0.16", default-features = false, features = ["std", "encoder", "optimization", "xz"] } scraper = "0.25" +linesweeper = "0.3" +smallvec = "1.13.2" [workspace.lints.rust] unexpected_cfgs = { level = "allow", check-cfg = ['cfg(target_arch, values("spirv"))'] } diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index cf31a4337a..e9c8ba2f11 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -29,7 +29,7 @@ use glam::{DAffine2, DVec2, IVec2}; use graph_craft::document::value::TaggedValue; use graph_craft::document::{NodeId, NodeInput, NodeNetwork, OldNodeNetwork}; use graphene_std::math::quad::Quad; -use graphene_std::path_bool::{boolean_intersect, path_bool_lib}; +use graphene_std::path_bool::boolean_intersect; use graphene_std::raster::BlendMode; use graphene_std::raster_types::Raster; use graphene_std::render_node::wgpu_available; @@ -37,9 +37,9 @@ use graphene_std::subpath::Subpath; use graphene_std::table::Table; use graphene_std::vector::PointId; use graphene_std::vector::click_target::{ClickTarget, ClickTargetType}; -use graphene_std::vector::misc::{dvec2_to_point, point_to_dvec2}; +use graphene_std::vector::misc::dvec2_to_point; use graphene_std::vector::style::RenderMode; -use kurbo::{Affine, CubicBez, Line, ParamCurve, PathSeg, QuadBez}; +use kurbo::{Affine, BezPath, Line, PathSeg}; use std::path::PathBuf; use std::sync::Arc; use std::time::Duration; @@ -2982,7 +2982,7 @@ fn default_document_network_interface() -> NodeNetworkInterface { enum XRayTarget { Point(DVec2), Quad(Quad), - Path(Vec), + Path(BezPath), Polygon(Subpath), } @@ -3000,17 +3000,12 @@ pub struct ClickXRayIter<'a> { parent_targets: Vec<(LayerNodeIdentifier, XRayTarget)>, } -fn quad_to_path_lib_segments(quad: Quad) -> Vec { - quad.all_edges().into_iter().map(|[start, end]| path_bool_lib::PathSegment::Line(start, end)).collect() +fn quad_to_kurbo(quad: Quad) -> BezPath { + BezPath::from_path_segments(quad.all_edges().into_iter().map(|[start, end]| PathSeg::Line(Line::new(dvec2_to_point(start), dvec2_to_point(end))))) } -fn click_targets_to_path_lib_segments<'a>(click_targets: impl Iterator, transform: DAffine2) -> Vec { - let segment = |bezier: PathSeg| match bezier { - PathSeg::Line(line) => path_bool_lib::PathSegment::Line(point_to_dvec2(line.p0), point_to_dvec2(line.p1)), - PathSeg::Quad(quad_bez) => path_bool_lib::PathSegment::Quadratic(point_to_dvec2(quad_bez.p0), point_to_dvec2(quad_bez.p1), point_to_dvec2(quad_bez.p2)), - PathSeg::Cubic(cubic_bez) => path_bool_lib::PathSegment::Cubic(point_to_dvec2(cubic_bez.p0), point_to_dvec2(cubic_bez.p1), point_to_dvec2(cubic_bez.p2), point_to_dvec2(cubic_bez.p3)), - }; - click_targets +fn click_targets_to_kurbo<'a>(click_targets: impl Iterator, transform: DAffine2) -> BezPath { + let segments = click_targets .filter_map(|target| { if let ClickTargetType::Subpath(subpath) = target.target_type() { Some(subpath.iter()) @@ -3019,8 +3014,8 @@ fn click_targets_to_path_lib_segments<'a>(click_targets: impl Iterator ClickXRayIter<'a> { @@ -3041,22 +3036,8 @@ impl<'a> ClickXRayIter<'a> { } /// Handles the checking of the layer where the target is a rect or path - fn check_layer_area_target( - &mut self, - click_targets: Option<&[Arc]>, - clip: bool, - layer: LayerNodeIdentifier, - path: Vec, - transform: DAffine2, - ) -> XRayResult { - // Convert back to Kurbo types for intersections - let segment = |bezier: &path_bool_lib::PathSegment| match *bezier { - path_bool_lib::PathSegment::Line(start, end) => PathSeg::Line(Line::new(dvec2_to_point(start), dvec2_to_point(end))), - path_bool_lib::PathSegment::Cubic(start, h1, h2, end) => PathSeg::Cubic(CubicBez::new(dvec2_to_point(start), dvec2_to_point(h1), dvec2_to_point(h2), dvec2_to_point(end))), - path_bool_lib::PathSegment::Quadratic(start, h1, end) => PathSeg::Quad(QuadBez::new(dvec2_to_point(start), dvec2_to_point(h1), dvec2_to_point(end))), - path_bool_lib::PathSegment::Arc(_, _, _, _, _, _, _) => unimplemented!(), - }; - let get_clip = || path.iter().map(segment); + fn check_layer_area_target(&mut self, click_targets: Option<&[Arc]>, clip: bool, layer: LayerNodeIdentifier, path: BezPath, transform: DAffine2) -> XRayResult { + let get_clip = || path.segments(); let intersects = click_targets.is_some_and(|targets| targets.iter().any(|target| target.intersect_path(get_clip, transform))); let clicked = intersects; @@ -3065,8 +3046,9 @@ impl<'a> ClickXRayIter<'a> { // In the case of a clip path where the area partially intersects, it is necessary to do a boolean operation. // We do this on this using the target area to reduce computation (as the target area is usually very simple). if clip && intersects { - let clip_path = click_targets_to_path_lib_segments(click_targets.iter().flat_map(|x| x.iter()).map(|x| x.as_ref()), transform); - let subtracted = boolean_intersect(path, clip_path).into_iter().flatten().collect::>(); + let clip_path = click_targets_to_kurbo(click_targets.iter().flat_map(|x| x.iter()).map(|x| x.as_ref()), transform); + let intersection = boolean_intersect(&path, &clip_path); + let subtracted = BezPath::from_path_segments(intersection.iter().flat_map(|p| p.segments())); if subtracted.is_empty() { use_children = false; } else { @@ -3099,13 +3081,10 @@ impl<'a> ClickXRayIter<'a> { use_children: !clip || intersects, } } - XRayTarget::Quad(quad) => self.check_layer_area_target(click_targets, clip, layer, quad_to_path_lib_segments(*quad), transform), + XRayTarget::Quad(quad) => self.check_layer_area_target(click_targets, clip, layer, quad_to_kurbo(*quad), transform), XRayTarget::Path(path) => self.check_layer_area_target(click_targets, clip, layer, path.clone(), transform), XRayTarget::Polygon(polygon) => { - let polygon = polygon - .iter_closed() - .map(|line| path_bool_lib::PathSegment::Line(point_to_dvec2(line.start()), point_to_dvec2(line.end()))) - .collect(); + let polygon = BezPath::from_path_segments(polygon.iter_closed()); self.check_layer_area_target(click_targets, clip, layer, polygon, transform) } } diff --git a/node-graph/nodes/path-bool/Cargo.toml b/node-graph/nodes/path-bool/Cargo.toml index d755adcb75..54010339e2 100644 --- a/node-graph/nodes/path-bool/Cargo.toml +++ b/node-graph/nodes/path-bool/Cargo.toml @@ -16,9 +16,10 @@ core-types = { workspace = true } graphic-types = { workspace = true } node-macro = { workspace = true } glam = { workspace = true } +linesweeper = { workspace = true } log = { workspace = true } -path-bool = { workspace = true } serde = { workspace = true } +smallvec = { workspace = true } vector-types = { workspace = true } # Optional workspace dependencies diff --git a/node-graph/nodes/path-bool/src/lib.rs b/node-graph/nodes/path-bool/src/lib.rs index 49ab3004b4..890a9337a8 100644 --- a/node-graph/nodes/path-bool/src/lib.rs +++ b/node-graph/nodes/path-bool/src/lib.rs @@ -2,14 +2,15 @@ use core_types::table::{Table, TableRow, TableRowRef}; use core_types::{Color, Ctx}; use dyn_any::DynAny; use glam::{DAffine2, DVec2}; -use graphic_types::vector_types::subpath::{ManipulatorGroup, PathSegPoints, Subpath, pathseg_points}; +use graphic_types::vector_types::subpath::{ManipulatorGroup, Subpath}; use graphic_types::vector_types::vector::PointId; use graphic_types::vector_types::vector::algorithms::merge_by_distance::MergeByDistanceExt; use graphic_types::vector_types::vector::style::Fill; use graphic_types::{Graphic, Vector}; -pub use path_bool as path_bool_lib; -use path_bool::{FillRule, PathBooleanOperation}; -use std::ops::Mul; +use linesweeper::topology::Topology; +use linesweeper::{BinaryOp, FillRule, binary_op}; +use smallvec::SmallVec; +use vector_types::kurbo::{Affine, BezPath, CubicBez, Line, ParamCurve, PathSeg, Point, QuadBez}; // TODO: Fix boolean ops to work by removing .transform() and .one_instance_*() calls, // TODO: since before we used a Vec of single-row tables and now we use a single table @@ -68,164 +69,98 @@ async fn boolean_operation(vector: impl DoubleEndedIterator> + Clone, boolean_operation: BooleanOperation) -> Table { - match boolean_operation { - BooleanOperation::Union => union(vector), - BooleanOperation::SubtractFront => subtract(vector), - BooleanOperation::SubtractBack => subtract(vector.rev()), - BooleanOperation::Intersect => intersect(vector), - BooleanOperation::Difference => difference(vector), - } +#[derive(Clone, Debug, Default, PartialEq, Eq)] +struct WindingNumber { + elems: SmallVec<[i16; 8]>, } -fn union<'a>(vector: impl DoubleEndedIterator>) -> Table { - // Reverse the vector table rows so that the result style is the style of the first vector row - let mut vector_reversed = vector.rev(); - - let mut result_vector_table = Table::new_from_row(vector_reversed.next().map(|x| x.into_cloned()).unwrap_or_default()); - let mut first_row = result_vector_table.iter_mut().next().expect("Expected the one row we just pushed"); - - // Loop over all vector table rows and union it with the result - let default = TableRow::default(); - let mut second_vector = Some(vector_reversed.next().unwrap_or(default.as_ref())); - while let Some(lower_vector) = second_vector { - let transform_of_lower_into_space_of_upper = first_row.transform.inverse() * *lower_vector.transform; - - let result = &mut first_row.element; +impl linesweeper::topology::WindingNumber for WindingNumber { + type Tag = (usize, usize); - let upper_path_string = to_path(result, DAffine2::IDENTITY); - let lower_path_string = to_path(lower_vector.element, transform_of_lower_into_space_of_upper); - - #[allow(unused_unsafe)] - let boolean_operation_string = unsafe { boolean_union(upper_path_string, lower_path_string) }; - let boolean_operation_result = from_path(&boolean_operation_string); - - result.colinear_manipulators = boolean_operation_result.colinear_manipulators; - result.point_domain = boolean_operation_result.point_domain; - result.segment_domain = boolean_operation_result.segment_domain; - result.region_domain = boolean_operation_result.region_domain; - - second_vector = vector_reversed.next(); + fn single((tag, out_of): (usize, usize), positive: bool) -> Self { + let mut elems = SmallVec::with_capacity(out_of); + elems.resize(out_of, 0); + elems[tag] = if positive { 1 } else { -1 }; + Self { elems } } - - result_vector_table } -fn subtract<'a>(vector: impl Iterator>) -> Table { - let mut vector = vector.into_iter(); - - let mut result_vector_table = Table::new_from_row(vector.next().map(|x| x.into_cloned()).unwrap_or_default()); - let mut first_row = result_vector_table.iter_mut().next().expect("Expected the one row we just pushed"); - let first_row_transform = if first_row.transform.matrix2.determinant() != 0. { - first_row.transform.inverse() - } else { - DAffine2::IDENTITY - }; - - let mut next_vector = vector.next(); - - while let Some(lower_vector) = next_vector { - let transform_of_lower_into_space_of_upper = first_row_transform * *lower_vector.transform; - - let result = &mut first_row.element; - - let upper_path_string = to_path(result, DAffine2::IDENTITY); - let lower_path_string = to_path(lower_vector.element, transform_of_lower_into_space_of_upper); - - #[allow(unused_unsafe)] - let boolean_operation_string = unsafe { boolean_subtract(upper_path_string, lower_path_string) }; - let boolean_operation_result = from_path(&boolean_operation_string); - - result.colinear_manipulators = boolean_operation_result.colinear_manipulators; - result.point_domain = boolean_operation_result.point_domain; - result.segment_domain = boolean_operation_result.segment_domain; - result.region_domain = boolean_operation_result.region_domain; - - next_vector = vector.next(); +impl std::ops::AddAssign for WindingNumber { + fn add_assign(&mut self, rhs: Self) { + if rhs.elems.is_empty() { + return; + } + if self.elems.is_empty() { + self.elems = rhs.elems; + } else { + for (me, them) in self.elems.iter_mut().zip(&rhs.elems) { + *me += *them; + } + } } - - result_vector_table } -fn intersect<'a>(vector: impl DoubleEndedIterator>) -> Table { - let mut vector = vector.rev(); - - let mut result_vector_table = Table::new_from_row(vector.next().map(|x| x.into_cloned()).unwrap_or_default()); - let mut first_row = result_vector_table.iter_mut().next().expect("Expected the one row we just pushed"); - - let default = TableRow::default(); - let mut second_vector = Some(vector.next().unwrap_or(default.as_ref())); +impl std::ops::Add for WindingNumber { + type Output = WindingNumber; - // For each vector table row, set the result to the intersection of that path and the current result - while let Some(lower_vector) = second_vector { - let transform_of_lower_into_space_of_upper = first_row.transform.inverse() * *lower_vector.transform; - - let result = &mut first_row.element; - - let upper_path_string = to_path(result, DAffine2::IDENTITY); - let lower_path_string = to_path(lower_vector.element, transform_of_lower_into_space_of_upper); - - #[allow(unused_unsafe)] - let boolean_operation_string = unsafe { boolean_intersect(upper_path_string, lower_path_string) }; - let boolean_operation_result = from_path(&boolean_operation_string); - - result.colinear_manipulators = boolean_operation_result.colinear_manipulators; - result.point_domain = boolean_operation_result.point_domain; - result.segment_domain = boolean_operation_result.segment_domain; - result.region_domain = boolean_operation_result.region_domain; - second_vector = vector.next(); + fn add(mut self, rhs: Self) -> Self::Output { + self += rhs; + self } - - result_vector_table } -fn difference<'a>(vector: impl DoubleEndedIterator> + Clone) -> Table { - let mut vector_iter = vector.clone().rev(); - let mut any_intersection = TableRow::default(); - let default = TableRow::default(); - let mut second_vector = Some(vector_iter.next().unwrap_or(default.as_ref())); - - // Find where all vector table row paths intersect at least once - while let Some(lower_vector) = second_vector { - let filtered_vector = vector.clone().filter(|v| *v != lower_vector).collect::>().into_iter(); - let unioned = boolean_operation_on_vector_table(filtered_vector, BooleanOperation::Union); - let first_row = unioned.iter().next().expect("Expected at least one row after the boolean union"); - - let transform_of_lower_into_space_of_upper = first_row.transform.inverse() * *lower_vector.transform; - - let upper_path_string = to_path(first_row.element, DAffine2::IDENTITY); - let lower_path_string = to_path(lower_vector.element, transform_of_lower_into_space_of_upper); - - #[allow(unused_unsafe)] - let boolean_intersection_string = unsafe { boolean_intersect(upper_path_string, lower_path_string) }; - let mut element = from_path(&boolean_intersection_string); - element.style = first_row.element.style.clone(); - let boolean_intersection_result = TableRow { - element, - transform: *first_row.transform, - alpha_blending: *first_row.alpha_blending, - source_node_id: *first_row.source_node_id, - }; +impl WindingNumber { + fn is_inside(&self, op: BooleanOperation) -> bool { + let is_in = |w: &i16| *w != 0; + let is_out = |w: &i16| *w == 0; + match op { + BooleanOperation::Union => self.elems.iter().any(is_in), + BooleanOperation::SubtractFront => self.elems.first().is_some_and(is_in) && self.elems.iter().skip(1).all(is_out), + 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), + } + } +} - let transform_of_lower_into_space_of_upper = boolean_intersection_result.transform.inverse() * any_intersection.transform; +fn boolean_operation_on_vector_table<'a>(vector: impl DoubleEndedIterator> + Clone, boolean_operation: BooleanOperation) -> Table { + const EPSILON: f64 = 1e-5; + let mut table = Table::new(); + let mut paths = Vec::new(); + let mut row = TableRow::::default(); - let upper_path_string = to_path(&boolean_intersection_result.element, DAffine2::IDENTITY); - let lower_path_string = to_path(&any_intersection.element, transform_of_lower_into_space_of_upper); + let copy_from = if matches!(boolean_operation, BooleanOperation::SubtractFront) { + vector.clone().next() + } else { + vector.clone().next_back() + }; + if let Some(copy_from) = copy_from { + row.alpha_blending = *copy_from.alpha_blending; + row.source_node_id = *copy_from.source_node_id; + row.element.style = copy_from.element.style.clone(); + row.element.upstream_data = copy_from.element.upstream_data.clone(); + } - #[allow(unused_unsafe)] - let union_result = from_path(&unsafe { boolean_union(upper_path_string, lower_path_string) }); - any_intersection.element = union_result; + for element in vector { + paths.push(to_bez_path(element.element, *element.transform)); + } - any_intersection.transform = boolean_intersection_result.transform; - any_intersection.element.style = boolean_intersection_result.element.style.clone(); - any_intersection.alpha_blending = boolean_intersection_result.alpha_blending; + let top = match Topology::::from_paths(paths.iter().enumerate().map(|(idx, path)| (path, (idx, paths.len()))), EPSILON) { + Ok(top) => top, + Err(e) => { + log::error!("Boolean operation failed while building topology: {e}"); + table.push(row); + return table; + } + }; + let contours = top.contours(|winding| winding.is_inside(boolean_operation)); - second_vector = vector_iter.next(); + for subpath in from_bez_paths(contours.contours().map(|c| &c.path)) { + row.element.append_subpath(subpath, false); } - // Subtract the area where they intersect at least once from the union of all vector paths - let union = boolean_operation_on_vector_table(vector, BooleanOperation::Union); - boolean_operation_on_vector_table(union.iter().chain(std::iter::once(any_intersection.as_ref())), BooleanOperation::SubtractFront) + table.push(row); + table } fn flatten_vector(graphic_table: &Table) -> Table { @@ -325,70 +260,58 @@ fn flatten_vector(graphic_table: &Table) -> Table { .collect() } -fn to_path(vector: &Vector, transform: DAffine2) -> Vec { - let mut path = Vec::new(); +// This quantization should potentially be removed since it's not conceptually necessary, +// but without it, the oak leaf in the Changing Seasons demo artwork is funky because +// quantization is needed for the top and bottom points to line up vertically. +fn quantize_segment(seg: PathSeg) -> PathSeg { + const QUANTIZE_EPS: f64 = 1e-8; + fn q(p: Point) -> Point { + Point::new((p.x / QUANTIZE_EPS).round() * QUANTIZE_EPS, (p.y / QUANTIZE_EPS).round() * QUANTIZE_EPS) + } + + match seg { + PathSeg::Line(s) => PathSeg::Line(Line::new(q(s.p0), q(s.p1))), + PathSeg::Quad(s) => PathSeg::Quad(QuadBez::new(q(s.p0), q(s.p1), q(s.p2))), + PathSeg::Cubic(s) => PathSeg::Cubic(CubicBez::new(q(s.p0), q(s.p1), q(s.p2), q(s.p3))), + } +} + +fn to_bez_path(vector: &Vector, transform: DAffine2) -> BezPath { + let mut path = BezPath::new(); for subpath in vector.stroke_bezier_paths() { - to_path_segments(&mut path, &subpath, transform); + push_subpath(&mut path, &subpath, transform); } path } -fn to_path_segments(path: &mut Vec, subpath: &Subpath, transform: DAffine2) { - use path_bool::PathSegment; - let mut global_start = None; - let mut global_end = DVec2::ZERO; +fn push_subpath(path: &mut BezPath, subpath: &Subpath, transform: DAffine2) { + let transform = Affine::new(transform.to_cols_array()); + let mut first = true; - for bezier in subpath.iter() { - const EPS: f64 = 1e-8; - let transform_point = |pos: DVec2| transform.transform_point2(pos).mul(EPS.recip()).round().mul(EPS); - - let PathSegPoints { p0, p1, p2, p3 } = pathseg_points(bezier); - - let p0 = transform_point(p0); - let p1 = p1.map(transform_point); - let p2 = p2.map(transform_point); - let p3 = transform_point(p3); - - if global_start.is_none() { - global_start = Some(p0); + for seg in subpath.iter_closed() { + let quantized = quantize_segment(transform * seg); + if first { + first = false; + path.move_to(quantized.start()); } - global_end = p3; - - let segment = match (p1, p2) { - (None, None) => PathSegment::Line(p0, p3), - (None, Some(p2)) | (Some(p2), None) => PathSegment::Quadratic(p0, p2, p3), - (Some(p1), Some(p2)) => PathSegment::Cubic(p0, p1, p2, p3), - }; - - path.push(segment); - } - if let Some(start) = global_start { - path.push(PathSegment::Line(global_end, start)); + path.push(quantized.as_path_el()); } + path.close_path(); } -fn from_path(path_data: &[Path]) -> Vector { - const EPSILON: f64 = 1e-5; - - fn is_close(a: DVec2, b: DVec2) -> bool { - (a - b).length_squared() < EPSILON * EPSILON - } - +fn from_bez_paths<'a>(paths: impl Iterator) -> Vec> { let mut all_subpaths = Vec::new(); - for path in path_data.iter().filter(|path| !path.is_empty()) { - let cubics: Vec<[DVec2; 4]> = path.iter().map(|segment| segment.to_cubic()).collect(); + for path in paths { + let cubics: Vec = path.segments().map(|segment| segment.to_cubic()).collect(); let mut manipulators_list = Vec::new(); let mut current_start = None; for (index, cubic) in cubics.iter().enumerate() { - let [start, handle1, handle2, end] = *cubic; + let d = |p: Point| DVec2::new(p.x, p.y); + let [start, handle1, handle2, end] = [d(cubic.p0), d(cubic.p1), d(cubic.p2), d(cubic.p3)]; - if current_start.is_none() || !is_close(start, current_start.unwrap()) { - // Start a new subpath - if !manipulators_list.is_empty() { - all_subpaths.push(Subpath::new(std::mem::take(&mut manipulators_list), true)); - } + if current_start.is_none() { // Use the correct in-handle (None) and out-handle for the start point manipulators_list.push(ManipulatorGroup::new(start, None, Some(handle1))); } else { @@ -411,31 +334,15 @@ fn from_path(path_data: &[Path]) -> Vector { } } - Vector::from_subpaths(all_subpaths, false) -} - -type Path = Vec; - -fn boolean_union(a: Path, b: Path) -> Vec { - path_bool(a, b, PathBooleanOperation::Union) + all_subpaths } -fn path_bool(a: Path, b: Path, op: PathBooleanOperation) -> Vec { - match path_bool::path_boolean(&a, FillRule::NonZero, &b, FillRule::NonZero, op) { - Ok(results) => results, +pub fn boolean_intersect(a: &BezPath, b: &BezPath) -> Vec { + match binary_op(a, b, FillRule::NonZero, BinaryOp::Intersection) { + Ok(contours) => contours.contours().map(|c| c.path.clone()).collect(), Err(e) => { - let a_path = path_bool::path_to_path_data(&a, 0.001); - let b_path = path_bool::path_to_path_data(&b, 0.001); - log::error!("Boolean error {e:?} encountered while processing {a_path}\n {op:?}\n {b_path}"); + log::error!("Boolean Operation failed (a: {} segments, b: {} segments): {e}", a.segments().count(), b.segments().count()); Vec::new() } } } - -fn boolean_subtract(a: Path, b: Path) -> Vec { - path_bool(a, b, PathBooleanOperation::Difference) -} - -pub fn boolean_intersect(a: Path, b: Path) -> Vec { - path_bool(a, b, PathBooleanOperation::Intersection) -}