Replace the Boolean Operations node's algorithm with the Linesweeper library (#2670)

* Attempt one-shot n-ary ops

* Make it not crash

* Try to remove more path_bool

* Add quantization

* Update to latest

* Nits

* Code review

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
jneem 2026-03-11 00:00:49 -05:00 committed by GitHub
parent 095c2a6d47
commit 58aae4f87b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 165 additions and 259 deletions

19
Cargo.lock generated
View File

@ -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",

View File

@ -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"))'] }

View File

@ -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_bool_lib::PathSegment>),
Path(BezPath),
Polygon(Subpath<PointId>),
}
@ -3000,17 +3000,12 @@ pub struct ClickXRayIter<'a> {
parent_targets: Vec<(LayerNodeIdentifier, XRayTarget)>,
}
fn quad_to_path_lib_segments(quad: Quad) -> Vec<path_bool_lib::PathSegment> {
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<Item = &'a ClickTarget>, transform: DAffine2) -> Vec<path_bool_lib::PathSegment> {
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<Item = &'a ClickTarget>, 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<Item = &'
}
})
.flatten()
.map(|bezier| segment(Affine::new(transform.to_cols_array()) * bezier))
.collect()
.map(|bezier| Affine::new(transform.to_cols_array()) * bezier);
BezPath::from_path_segments(segments)
}
impl<'a> 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<ClickTarget>]>,
clip: bool,
layer: LayerNodeIdentifier,
path: Vec<path_bool_lib::PathSegment>,
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<ClickTarget>]>, 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::<Vec<_>>();
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)
}
}

View File

@ -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

View File

@ -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<I: graphic_types::IntoGraphicTable + 'n + Send + Clon
result_vector_table
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
struct WindingNumber {
elems: SmallVec<[i16; 8]>,
}
impl linesweeper::topology::WindingNumber for WindingNumber {
type Tag = (usize, usize);
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 }
}
}
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;
}
}
}
}
impl std::ops::Add for WindingNumber {
type Output = WindingNumber;
fn add(mut self, rhs: Self) -> Self::Output {
self += rhs;
self
}
}
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),
}
}
}
fn boolean_operation_on_vector_table<'a>(vector: impl DoubleEndedIterator<Item = TableRowRef<'a, Vector>> + Clone, boolean_operation: BooleanOperation) -> Table<Vector> {
match boolean_operation {
BooleanOperation::Union => union(vector),
BooleanOperation::SubtractFront => subtract(vector),
BooleanOperation::SubtractBack => subtract(vector.rev()),
BooleanOperation::Intersect => intersect(vector),
BooleanOperation::Difference => difference(vector),
}
}
const EPSILON: f64 = 1e-5;
let mut table = Table::new();
let mut paths = Vec::new();
let mut row = TableRow::<Vector>::default();
fn union<'a>(vector: impl DoubleEndedIterator<Item = TableRowRef<'a, Vector>>) -> Table<Vector> {
// 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;
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();
}
result_vector_table
}
fn subtract<'a>(vector: impl Iterator<Item = TableRowRef<'a, Vector>>) -> Table<Vector> {
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()
let copy_from = if matches!(boolean_operation, BooleanOperation::SubtractFront) {
vector.clone().next()
} else {
DAffine2::IDENTITY
vector.clone().next_back()
};
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();
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();
}
result_vector_table
}
fn intersect<'a>(vector: impl DoubleEndedIterator<Item = TableRowRef<'a, Vector>>) -> Table<Vector> {
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()));
// 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();
for element in vector {
paths.push(to_bez_path(element.element, *element.transform));
}
result_vector_table
}
let top = match Topology::<WindingNumber>::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));
fn difference<'a>(vector: impl DoubleEndedIterator<Item = TableRowRef<'a, Vector>> + Clone) -> Table<Vector> {
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::<Vec<_>>().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,
};
let transform_of_lower_into_space_of_upper = boolean_intersection_result.transform.inverse() * any_intersection.transform;
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);
#[allow(unused_unsafe)]
let union_result = from_path(&unsafe { boolean_union(upper_path_string, lower_path_string) });
any_intersection.element = union_result;
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;
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<Graphic>) -> Table<Vector> {
@ -325,70 +260,58 @@ fn flatten_vector(graphic_table: &Table<Graphic>) -> Table<Vector> {
.collect()
}
fn to_path(vector: &Vector, transform: DAffine2) -> Vec<path_bool::PathSegment> {
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<path_bool::PathSegment>, subpath: &Subpath<PointId>, 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<PointId>, 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<Item = &'a BezPath>) -> Vec<Subpath<PointId>> {
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<CubicBez> = 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)
all_subpaths
}
type Path = Vec<path_bool::PathSegment>;
fn boolean_union(a: Path, b: Path) -> Vec<Path> {
path_bool(a, b, PathBooleanOperation::Union)
}
fn path_bool(a: Path, b: Path, op: PathBooleanOperation) -> Vec<Path> {
match path_bool::path_boolean(&a, FillRule::NonZero, &b, FillRule::NonZero, op) {
Ok(results) => results,
pub fn boolean_intersect(a: &BezPath, b: &BezPath) -> Vec<BezPath> {
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> {
path_bool(a, b, PathBooleanOperation::Difference)
}
pub fn boolean_intersect(a: Path, b: Path) -> Vec<Path> {
path_bool(a, b, PathBooleanOperation::Intersection)
}