diff --git a/Cargo.lock b/Cargo.lock index ce4bcaf338..ea6e5f3a87 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -208,6 +208,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" @@ -3329,6 +3332,17 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4a5ff6bcca6c4867b1c4fd4ef63e4db7436ef363e0ad7531d1558856bae64f4" +[[package]] +name = "linesweeper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc21d1be43970d5ab6672e2286dfb17decf345dcef6d198abc9bfeb98bced2d" +dependencies = [ + "arrayvec", + "kurbo 0.12.0", + "polycool", +] + [[package]] name = "linux-raw-sys" version = "0.9.4" @@ -4250,6 +4264,8 @@ dependencies = [ "glam", "glob", "image", + "kurbo 0.12.0", + "linesweeper", "lyon_geom", "regex", "resvg", diff --git a/libraries/path-bool/Cargo.toml b/libraries/path-bool/Cargo.toml index b864aa4d59..cf913c2caa 100644 --- a/libraries/path-bool/Cargo.toml +++ b/libraries/path-bool/Cargo.toml @@ -27,6 +27,8 @@ default = ["parsing"] glam = "0.29.0" regex = "1.10.6" slotmap = "1.0.7" +kurbo = "0.12.0" +linesweeper = "0.1.2" lyon_geom = "1.0" roots = "0.0.8" rustc-hash = "2.0.0" diff --git a/libraries/path-bool/src/path_boolean.rs b/libraries/path-bool/src/path_boolean.rs index 34d178559e..862e8d761b 100644 --- a/libraries/path-bool/src/path_boolean.rs +++ b/libraries/path-bool/src/path_boolean.rs @@ -51,6 +51,8 @@ //! This approach allows for efficient and accurate boolean operations, even on //! complex paths with many intersections or self-intersections. +#![allow(dead_code, unused_imports)] + new_key_type! { pub struct MajorVertexKey; pub struct MajorEdgeKey; @@ -74,6 +76,8 @@ use crate::path_segment::PathSegment; use crate::path_to_path_data; use glam::{BVec2, DVec2, I64Vec2}; +use kurbo::BezPath; +use linesweeper::topology::{BinaryWindingNumber, Topology}; use roots::{Roots, find_roots_cubic}; use rustc_hash::FxHashMap as HashMap; use rustc_hash::FxHashSet as HashSet; @@ -1694,96 +1698,58 @@ impl Display for BooleanError { /// - Issues arise in determining the nesting structure of the paths. #[inline(never)] pub fn path_boolean(a: &Path, a_fill_rule: FillRule, b: &Path, b_fill_rule: FillRule, op: PathBooleanOperation) -> Result, BooleanError> { - let mut unsplit_edges: Vec = a.iter().map(segment_to_edge(1)).chain(b.iter().map(segment_to_edge(2))).flatten().collect(); - - if unsplit_edges.is_empty() { - return Ok(Vec::new()); - } - split_at_self_intersections(&mut unsplit_edges); - - let split_edges = split_at_intersections(&unsplit_edges); - - #[cfg(feature = "logging")] - for (edge, _) in split_edges.iter() { - eprintln!("{}", path_to_path_data(&vec![*edge], 0.001)); - } - - let major_graph = find_vertices(&split_edges); - - #[cfg(feature = "logging")] - eprintln!("Major graph:"); - #[cfg(feature = "logging")] - eprintln!("{}", major_graph_to_dot(&major_graph)); - - let mut minor_graph = compute_minor(&major_graph); - - #[cfg(feature = "logging")] - eprintln!("Minor graph:"); - #[cfg(feature = "logging")] - eprintln!("{}", minor_graph_to_dot(&minor_graph.edges)); - - remove_dangling_edges(&mut minor_graph); - #[cfg(feature = "logging")] - eprintln!("After removing dangling edges:"); - #[cfg(feature = "logging")] - eprintln!("{}", minor_graph_to_dot(&minor_graph.edges)); - - #[cfg(feature = "logging")] - for (key, edge) in minor_graph.edges.iter() { - eprintln!("{key:?}:\n{}", path_to_path_data(&edge.segments.to_vec(), 0.001)); - } - #[cfg(feature = "logging")] - for vertex in minor_graph.vertices.values() { - eprintln!("{vertex:?}"); - } - sort_outgoing_edges_by_angle(&mut minor_graph); - #[cfg(feature = "logging")] - for vertex in minor_graph.vertices.values() { - eprintln!("{vertex:?}"); - } + let a = path_to_kurbo(a); + let b = path_to_kurbo(b); - for (edge_key, edge) in &minor_graph.edges { - assert!(minor_graph.vertices.contains_key(edge.incident_vertices[0]), "Edge {edge_key:?} has invalid start vertex"); - assert!(minor_graph.vertices.contains_key(edge.incident_vertices[1]), "Edge {edge_key:?} has invalid end vertex"); - assert!(edge.twin.is_some(), "Edge {edge_key:?} should have a twin"); - let twin = &minor_graph.edges[edge.twin.unwrap()]; - assert_eq!(twin.twin.unwrap(), edge_key, "Twin relationship should be symmetrical for edge {edge_key:?}"); - } - - let mut dual_graph = compute_dual(&minor_graph)?; + let inside = |windings: BinaryWindingNumber| -> bool { + let in_a = match a_fill_rule { + FillRule::NonZero => windings.shape_a != 0, + FillRule::EvenOdd => windings.shape_a % 2 == 1, + }; + let in_b = match b_fill_rule { + FillRule::NonZero => windings.shape_b != 0, + FillRule::EvenOdd => windings.shape_b % 2 == 1, + }; - let nesting_trees = compute_nesting_tree(&mut dual_graph); + match op { + PathBooleanOperation::Union => in_a || in_b, + PathBooleanOperation::Difference => in_a && !in_b, + PathBooleanOperation::Intersection => in_a && in_b, + PathBooleanOperation::Exclusion => in_a != in_b, + PathBooleanOperation::Division => unimplemented!(), + PathBooleanOperation::Fracture => unimplemented!(), + } + }; + let top = Topology::from_paths_binary(&a, &b, 1e-5).map_err(|_| BooleanError::MultipleOuterFaces)?; + let contours = top.contours(inside); + Ok(contours.contours().map(|c| path_from_kurbo(&c.path)).collect()) +} - #[cfg(feature = "logging")] - for tree in &nesting_trees { - eprintln!("nesting_trees: {tree:?}"); +fn seg_to_kurbo(seg: &PathSegment) -> kurbo::PathSeg { + let p = |dv: &DVec2| kurbo::Point::new(dv.x, dv.y); + match seg { + PathSegment::Line(a, b) => kurbo::Line::new(p(a), p(b)).into(), + PathSegment::Quadratic(a, b, c) => kurbo::QuadBez::new(p(a), p(b), p(c)).into(), + PathSegment::Cubic(a, b, c, d) => kurbo::CubicBez::new(p(a), p(b), p(c), p(d)).into(), + PathSegment::Arc(..) => unimplemented!(), } +} - let DualGraph { edges, vertices, .. } = &dual_graph; - - #[cfg(feature = "logging")] - eprintln!("Dual Graph:"); - #[cfg(feature = "logging")] - eprintln!("{}", dual_graph_to_dot(&dual_graph.components, edges)); - - let mut flags = new_hash_map(vertices.len()); - flag_faces(&nesting_trees, a_fill_rule, b_fill_rule, edges, vertices, &mut flags); +fn path_to_kurbo(p: &Path) -> kurbo::BezPath { + BezPath::from_path_segments(p.iter().map(seg_to_kurbo)) +} - #[cfg(feature = "logging")] - for (face, flag) in &flags { - eprintln!("{:?}: {:b}", face.0, flag); +fn seg_from_kurbo(seg: kurbo::PathSeg) -> PathSegment { + let d = |p: kurbo::Point| DVec2::new(p.x, p.y); + match seg { + kurbo::PathSeg::Line(l) => PathSegment::Line(d(l.p0), d(l.p1)), + kurbo::PathSeg::Quad(q) => PathSegment::Quadratic(d(q.p0), d(q.p1), d(q.p2)), + kurbo::PathSeg::Cubic(c) => PathSegment::Cubic(d(c.p0), d(c.p1), d(c.p2), d(c.p3)), } +} - let predicate = OPERATION_PREDICATES[op as usize]; - - match op { - PathBooleanOperation::Division | PathBooleanOperation::Fracture => Ok(dump_faces(&nesting_trees, predicate, edges, vertices, &flags)), - _ => { - let mut selected_faces: Vec = get_selected_faces(&predicate, &flags).collect(); - selected_faces.sort_unstable(); - Ok(vec![walk_faces(&selected_faces, edges, vertices).collect()]) - } - } +fn path_from_kurbo(p: &BezPath) -> Path { + p.segments().map(seg_from_kurbo).collect() } #[cfg(test)]