graphite2 / libraries /path-bool /src /path_boolean.rs
openfree's picture
Deploy from GitHub repository
2409829 verified
//! Implements boolean operations on paths using graph-based algorithms.
//!
//! This module uses concepts from graph theory to efficiently perform boolean
//! operations on complex paths. The main algorithms involve creating a graph
//! representation of the paths, simplifying this graph, and then working with
//! its dual graph to determine the result of the boolean operation.
//!
//! ## Graph Minor
//!
//! A graph minor is a simplified version of a graph, obtained by contracting edges
//! (merging connected vertices) and removing isolated vertices. In the context of
//! path boolean operations, we use a graph minor to simplify the initial graph
//! representation of the paths. This simplification involves:
//!
//! 1. Merging collinear segments into single edges.
//! 2. Removing vertices that don't represent significant features (like intersections
//! or endpoints).
//!
//! The resulting graph minor preserves the topological structure of the paths while
//! reducing computational complexity.
//!
//! For more information on graph minors, see:
//! <https://en.wikipedia.org/wiki/Graph_minor>
//!
//! ## Dual Graph
//!
//! The dual graph is a graph derived from another graph (the primal graph). In the
//! context of path boolean operations, we construct the dual graph as follows:
//!
//! 1. Each face (region) in the primal graph becomes a vertex in the dual graph.
//! 2. Each edge in the primal graph becomes an edge in the dual graph, connecting
//! the vertices that represent the faces on either side of the original edge.
//!
//! The dual graph allows us to efficiently determine which regions are inside or
//! outside the original paths, which is crucial for performing boolean operations.
//!
//! For more information on dual graphs, see:
//! <https://en.wikipedia.org/wiki/Dual_graph>
//!
//! ## Algorithm Overview
//!
//! The boolean operation algorithm follows these main steps:
//!
//! 1. Create a graph representation of both input paths (MajorGraph).
//! 2. Simplify this graph to create a graph minor (MinorGraph).
//! 3. Construct the dual graph of the MinorGraph.
//! 4. Use the dual graph to determine which regions should be included in the result,
//! based on the specific boolean operation being performed.
//! 5. Reconstruct the resulting path(s) from the selected regions.
//!
//! This approach allows for efficient and accurate boolean operations, even on
//! complex paths with many intersections or self-intersections.
new_key_type! {
pub struct MajorVertexKey;
pub struct MajorEdgeKey;
pub struct MinorVertexKey;
pub struct MinorEdgeKey;
pub struct DualVertexKey;
pub struct DualEdgeKey;
}
// Copyright 2024 Adam Platkevič <[email protected]>
//
// SPDX-License-Identifier: MIT
use crate::aabb::{Aabb, bounding_box_around_point, bounding_box_max_extent, merge_bounding_boxes};
use crate::epsilons::Epsilons;
use crate::intersection_path_segment::{path_segment_intersection, segments_equal};
use crate::path::Path;
use crate::path_cubic_segment_self_intersection::path_cubic_segment_self_intersection;
use crate::path_segment::PathSegment;
#[cfg(feature = "logging")]
use crate::path_to_path_data;
use crate::quad_tree::QuadTree;
use glam::DVec2;
use slotmap::{SlotMap, new_key_type};
use std::cmp::Ordering;
use std::collections::{HashMap, HashSet, VecDeque};
use std::fmt::Display;
/// Represents the types of boolean operations that can be performed on paths.
#[derive(Debug, Clone, Copy)]
pub enum PathBooleanOperation {
/// Computes the union of two paths.
///
/// The result contains all areas that are inside either path A or path B (or both).
/// This operation is useful for combining shapes or creating complex outlines.
Union,
/// Computes the difference between two paths (A minus B).
///
/// The result contains all areas that are inside path A but not inside path B.
/// This operation is useful for cutting holes or subtracting shapes from each other.
Difference,
/// Computes the intersection of two paths.
///
/// The result contains only the areas that are inside both path A and path B.
/// This operation is useful for finding overlapping regions between shapes.
Intersection,
/// Computes the symmetric difference (exclusive or) of two paths.
///
/// The result contains areas that are inside either path A or path B, but not in both.
/// This operation is useful for creating non-overlapping regions or finding boundaries.
Exclusion,
/// Divides the first path using the second path as a "knife".
///
/// This operation splits path A wherever it intersects with path B, but keeps all
/// parts of path A. It's useful for creating segments or partitioning shapes.
Division,
/// Breaks both paths into separate pieces where they intersect.
///
/// This operation splits both path A and path B at their intersection points,
/// resulting in all possible non-overlapping segments from both paths.
/// It's useful for creating detailed breakdowns of overlapping shapes.
Fracture,
}
/// Specifies how to determine the "inside" of a path for filling.
#[derive(Debug, Clone, Copy)]
pub enum FillRule {
/// A point is inside if a ray from the point to infinity crosses an odd number of path segments.
NonZero,
/// A point is inside if a ray from the point to infinity crosses an even number of path segments.
EvenOdd,
}
const INTERSECTION_TREE_DEPTH: usize = 8;
const POINT_TREE_DEPTH: usize = 8;
pub const EPS: Epsilons = Epsilons {
point: 1e-5,
linear: 1e-4,
param: 1e-8,
};
type MajorGraphEdgeStage1 = (PathSegment, u8);
type MajorGraphEdgeStage2 = (PathSegment, u8, Aabb);
#[derive(Debug, Clone)]
pub struct MajorGraphEdge {
seg: PathSegment,
parent: u8,
incident_vertices: [MajorVertexKey; 2],
direction_flag: Direction,
twin: Option<MajorEdgeKey>,
}
#[derive(Debug, Clone, Default)]
pub struct MajorGraphVertex {
#[cfg_attr(not(feature = "logging"), expect(dead_code))]
pub point: DVec2,
outgoing_edges: Vec<MajorEdgeKey>,
}
/// Represents the initial graph structure used in boolean operations.
///
/// This graph contains all segments from both input paths.
#[derive(Debug, Clone)]
struct MajorGraph {
edges: SlotMap<MajorEdgeKey, MajorGraphEdge>,
vertices: SlotMap<MajorVertexKey, MajorGraphVertex>,
}
#[derive(Debug, Clone, PartialEq)]
struct MinorGraphEdge {
segments: Vec<PathSegment>,
parent: u8,
incident_vertices: [MinorVertexKey; 2],
direction_flag: Direction,
twin: Option<MinorEdgeKey>,
}
impl MinorGraphEdge {
fn start_segment(&self) -> PathSegment {
let segment = self.segments[0];
match self.direction_flag {
Direction::Forward => segment,
Direction::Backwards => segment.reverse(),
}
}
}
// Compares Segments based on their derivative at the start. If the derivative
// is equal, check the curvature instead. This should correctly sort most instances.
fn compare_segments(a: &PathSegment, b: &PathSegment) -> Ordering {
let angle_a = a.start_angle();
let angle_b = b.start_angle();
// Normalize angles to [0, 2π)
let angle_a = (angle_a * 1000.).round() / 1000.;
let angle_b = (angle_b * 1000.).round() / 1000.;
// Compare angles first
match angle_b.partial_cmp(&angle_a) {
Some(Ordering::Equal) => {
// If angles are equal (or very close), compare curvatures
let curvature_a = a.start_curvature();
let curvature_b = b.start_curvature();
curvature_a.partial_cmp(&curvature_b).unwrap_or(Ordering::Equal)
}
Some(ordering) => ordering,
None => Ordering::Equal, // Handle NaN cases
}
}
impl PartialOrd for MinorGraphEdge {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(compare_segments(&self.start_segment(), &other.start_segment()))
}
}
#[derive(Debug, Clone, Default)]
struct MinorGraphVertex {
outgoing_edges: Vec<MinorEdgeKey>,
}
#[derive(Debug, Clone)]
struct MinorGraphCycle {
segments: Vec<PathSegment>,
parent: u8,
direction_flag: Direction,
}
/// Represents a simplified graph structure derived from the MajorGraph.
///
/// This graph combines collinear segments and removes unnecessary vertices.
#[derive(Debug, Clone)]
struct MinorGraph {
edges: SlotMap<MinorEdgeKey, MinorGraphEdge>,
vertices: SlotMap<MinorVertexKey, MinorGraphVertex>,
cycles: Vec<MinorGraphCycle>,
}
#[derive(Debug, Clone, PartialEq)]
struct DualGraphHalfEdge {
segments: Vec<PathSegment>,
parent: u8,
incident_vertex: DualVertexKey,
direction_flag: Direction,
twin: Option<DualEdgeKey>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct DualGraphVertex {
incident_edges: Vec<DualEdgeKey>,
}
/// Represents a component in the dual graph.
///
/// A component is a connected subset of the dual graph, typically corresponding
/// to a distinct region in the original paths.
#[derive(Debug, Clone)]
struct DualGraphComponent {
edges: Vec<DualEdgeKey>,
vertices: Vec<DualVertexKey>,
outer_face: Option<DualVertexKey>,
}
/// Represents the dual graph of the MinorGraph.
///
/// In this graph, faces of the MinorGraph become vertices, and edges represent
/// adjacency between faces. This structure is crucial for determining the
/// inside/outside regions of the paths.
#[derive(Debug, Clone)]
struct DualGraph {
components: Vec<DualGraphComponent>,
edges: SlotMap<DualEdgeKey, DualGraphHalfEdge>,
vertices: SlotMap<DualVertexKey, DualGraphVertex>,
}
/// Represents the hierarchical nesting of regions in the paths.
///
/// This tree structure captures how different regions of the paths are contained
/// within each other
#[derive(Debug, Clone)]
struct NestingTree {
component: DualGraphComponent,
outgoing_edges: HashMap<DualVertexKey, Vec<NestingTree>>,
}
#[cfg(feature = "logging")]
fn major_graph_to_dot(graph: &MajorGraph) -> String {
let mut dot = String::from("digraph {\n");
for (vertex_key, vertex) in &graph.vertices {
dot.push_str(&format!(" {:?} [label=\"{:.1},{:.1}\"]\n", (vertex_key.0.as_ffi() & 0xFF), vertex.point.x, vertex.point.y));
}
for (_, edge) in &graph.edges {
dot.push_str(&format!(
" {:?} -> {:?}: {:0b}\n",
(edge.incident_vertices[0].0.as_ffi() & 0xFF),
(edge.incident_vertices[1].0.as_ffi() & 0xFF),
edge.parent
));
}
dot.push_str("}\n");
dot
}
#[cfg(feature = "logging")]
fn minor_graph_to_dot(edges: &SlotMap<MinorEdgeKey, MinorGraphEdge>) -> String {
let mut dot = String::from("digraph {\n");
for edge in edges.values() {
dot.push_str(&format!(
" {:?} -> {:?}: {:0b}\n",
(edge.incident_vertices[0].0.as_ffi() & 0xFF),
(edge.incident_vertices[1].0.as_ffi() & 0xFF),
edge.parent
));
}
dot.push_str("}\n");
dot
}
#[cfg(feature = "logging")]
fn dual_graph_to_dot(components: &[DualGraphComponent], edges: &SlotMap<DualEdgeKey, DualGraphHalfEdge>) -> String {
let mut dot = String::from("strict graph {\n");
for component in components {
for &edge_key in &component.edges {
let edge = &edges[edge_key];
dot.push_str(&format!(
" {:?} -- {:?}\n",
(edge.incident_vertex.0.as_ffi() & 0xFF),
(edges[edge.twin.unwrap()].incident_vertex.0.as_ffi() & 0xFF)
));
}
}
dot.push_str("}\n");
dot
}
fn segment_to_edge(parent: u8) -> impl Fn(&PathSegment) -> Option<MajorGraphEdgeStage1> {
move |seg| {
if bounding_box_max_extent(&seg.bounding_box()) < EPS.point {
return None;
}
match seg {
// Convert Line Segments expressed as cubic beziers to proper line segments
PathSegment::Cubic(start, _, _, end) => {
let direction = seg.sample_at(0.1);
if (*end - *start).angle_to(direction - *start).abs() < EPS.point * 4. {
Some((PathSegment::Line(*start, *end), parent))
} else {
Some((*seg, parent))
}
}
seg => Some((*seg, parent)),
}
}
}
fn split_at_self_intersections(edges: &mut Vec<MajorGraphEdgeStage1>) {
let mut new_edges = Vec::new();
for (seg, parent) in edges.iter_mut() {
if let PathSegment::Cubic(..) = seg {
if let Some(intersection) = path_cubic_segment_self_intersection(seg) {
let mut intersection = intersection;
if intersection[0] > intersection[1] {
intersection.swap(0, 1);
}
let [t1, t2] = intersection;
if (t1 - t2).abs() < EPS.param {
let (seg1, seg2) = seg.split_at(t1);
*seg = seg1;
new_edges.push((seg2, *parent));
} else {
let (seg1, tmp_seg) = seg.split_at(t1);
let (seg2, seg3) = &tmp_seg.split_at((t2 - t1) / (1. - t1));
*seg = seg1;
new_edges.push((*seg2, *parent));
new_edges.push((*seg3, *parent));
}
}
}
}
edges.extend(new_edges);
}
/// Splits path segments at their intersections with other segments.
///
/// This function performs the following steps:
/// 1. Computes bounding boxes for all input edges.
/// 2. Creates a spatial index (quad tree) of edges for efficient intersection checks.
/// 3. For each edge:
/// a. Finds potential intersecting edges using the spatial index.
/// b. Computes precise intersections with these candidates.
/// c. Records the intersection points as split locations.
/// 4. Splits the original edges at the recorded intersection points.
/// 5. Returns the split edges along with an overall bounding box.
///
/// The function uses an epsilon value to handle floating-point imprecision
/// when determining if intersections occur at endpoints.
///
/// # Arguments
///
/// * `edges` - A slice of initial path segments (MajorGraphEdgeStage1).
///
/// # Returns
///
/// A tuple containing:
/// * A vector of split edges (MajorGraphEdgeStage2).
/// * An optional overall bounding box (AaBb) for all edges.
fn split_at_intersections(edges: &[MajorGraphEdgeStage1]) -> (Vec<MajorGraphEdgeStage2>, Option<Aabb>) {
// Step 1: Add bounding boxes to edges
let with_bounding_box: Vec<MajorGraphEdgeStage2> = edges.iter().map(|(seg, parent)| (*seg, *parent, seg.bounding_box())).collect();
// Step 2: Calculate total bounding box
let total_bounding_box = with_bounding_box.iter().fold(None, |acc, (_, _, bb)| Some(merge_bounding_boxes(acc, bb)));
let total_bounding_box = match total_bounding_box {
Some(bb) => bb,
None => return (Vec::new(), None),
};
// Step 3: Create edge tree for efficient intersection checks
let mut edge_tree = QuadTree::new(total_bounding_box, INTERSECTION_TREE_DEPTH, 8);
let mut splits_per_edge: HashMap<usize, Vec<f64>> = HashMap::new();
fn add_split(splits_per_edge: &mut HashMap<usize, Vec<f64>>, i: usize, t: f64) {
splits_per_edge.entry(i).or_default().push(t);
}
// Step 4: Find intersections and record split points
for (i, edge) in with_bounding_box.iter().enumerate() {
let candidates = edge_tree.find(&edge.2);
for &j in &candidates {
let candidate: &(PathSegment, u8) = &edges[j];
let include_endpoints = edge.1 != candidate.1 || !(candidate.0.end().abs_diff_eq(edge.0.start(), EPS.point) || candidate.0.start().abs_diff_eq(edge.0.end(), EPS.point));
let intersection = path_segment_intersection(&edge.0, &candidate.0, include_endpoints, &EPS);
for [t0, t1] in intersection {
add_split(&mut splits_per_edge, i, t0);
add_split(&mut splits_per_edge, j, t1);
}
}
edge_tree.insert(edge.2, i);
}
// Step 5: Apply splits to create new edges
let mut new_edges = Vec::new();
for (i, (seg, parent, _)) in with_bounding_box.into_iter().enumerate() {
if let Some(splits) = splits_per_edge.get(&i) {
let mut splits = splits.clone();
splits.sort_by(|a, b| a.partial_cmp(b).unwrap());
let mut tmp_seg = seg;
let mut prev_t = 0.;
for &t in splits.iter() {
if t > 1. - EPS.param {
break;
}
let tt = (t - prev_t) / (1. - prev_t);
prev_t = t;
if tt < EPS.param {
continue;
}
if tt > 1. - EPS.param {
continue;
}
let (seg1, seg2) = tmp_seg.split_at(tt);
new_edges.push((seg1, parent, seg1.bounding_box()));
tmp_seg = seg2;
}
new_edges.push((tmp_seg, parent, tmp_seg.bounding_box()));
} else {
new_edges.push((seg, parent, seg.bounding_box()));
}
}
(new_edges, Some(total_bounding_box))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Direction {
Forward,
Backwards,
}
impl std::ops::Neg for Direction {
type Output = Self;
fn neg(self) -> Self::Output {
match self {
Self::Forward => Self::Backwards,
Self::Backwards => Self::Forward,
}
}
}
impl std::ops::Not for Direction {
type Output = Self;
fn not(self) -> Self::Output {
match self {
Self::Forward => Self::Backwards,
Self::Backwards => Self::Forward,
}
}
}
impl Direction {
pub fn forward(self) -> bool {
self == Self::Forward
}
}
// TODO:(@TrueDoctor) Optimize this by rounding each vertex up and down and then inserting them in a hashmap. This should remove the need for bbox calculations and the quad tree
fn find_vertices(edges: &[MajorGraphEdgeStage2], bounding_box: Aabb) -> MajorGraph {
let mut vertex_tree = QuadTree::new(bounding_box, POINT_TREE_DEPTH, 8);
let mut graph = MajorGraph {
edges: SlotMap::with_key(),
vertices: SlotMap::with_key(),
};
let mut parents: HashMap<MajorEdgeKey, u8> = HashMap::new();
let mut vertex_pair_id_to_edges: HashMap<_, Vec<(MajorGraphEdgeStage2, MajorEdgeKey, MajorEdgeKey)>> = HashMap::new();
for (seg, parent, bounding_box) in edges {
let mut get_vertex = |point: DVec2| -> MajorVertexKey {
let box_around_point = bounding_box_around_point(point, EPS.point);
if let Some(&existing_vertex) = vertex_tree.find(&box_around_point).iter().next() {
existing_vertex
} else {
let vertex_key = graph.vertices.insert(MajorGraphVertex { point, outgoing_edges: Vec::new() });
vertex_tree.insert(box_around_point, vertex_key);
vertex_key
}
};
let start_vertex = get_vertex(seg.start());
let end_vertex = get_vertex(seg.end());
if start_vertex == end_vertex {
match seg {
PathSegment::Line(..) => continue,
PathSegment::Cubic(_, c1, c2, _) => {
if c1.abs_diff_eq(*c2, EPS.point) {
continue;
}
}
PathSegment::Quadratic(_, c, _) => {
if seg.start().abs_diff_eq(*c, EPS.point) {
continue;
}
}
PathSegment::Arc(_, _, _, _, _, false, _) => continue,
_ => {}
}
}
let vertex_pair_id = (start_vertex.min(end_vertex), start_vertex.max(end_vertex));
if let Some(existing_edges) = vertex_pair_id_to_edges.get(&vertex_pair_id) {
if let Some(existing_edge) = existing_edges
.iter()
.find(|(other_seg, ..)| segments_equal(seg, &other_seg.0, EPS.point) || segments_equal(&seg.reverse(), &other_seg.0, EPS.point))
{
*parents.entry(existing_edge.1).or_default() |= parent;
*parents.entry(existing_edge.2).or_default() |= parent;
continue;
}
}
let fwd_edge_key = graph.edges.insert(MajorGraphEdge {
seg: *seg,
parent: *parent,
incident_vertices: [start_vertex, end_vertex],
direction_flag: Direction::Forward,
twin: None,
});
let bwd_edge_key = graph.edges.insert(MajorGraphEdge {
seg: *seg,
parent: *parent,
incident_vertices: [end_vertex, start_vertex],
direction_flag: Direction::Backwards,
twin: Some(fwd_edge_key),
});
graph.edges[fwd_edge_key].twin = Some(bwd_edge_key);
graph.vertices[start_vertex].outgoing_edges.push(fwd_edge_key);
graph.vertices[end_vertex].outgoing_edges.push(bwd_edge_key);
vertex_pair_id_to_edges
.entry(vertex_pair_id)
.or_default()
.push(((*seg, *parent, *bounding_box), fwd_edge_key, bwd_edge_key));
}
for (edge_key, parent) in parents {
graph.edges[edge_key].parent |= parent;
}
graph
}
fn get_order(vertex: &MajorGraphVertex) -> usize {
vertex.outgoing_edges.len()
}
/// Computes the minor graph from the major graph.
///
/// This function simplifies the graph structure by performing the following steps:
/// 1. Iterates through vertices of the major graph.
/// 2. For vertices with exactly two edges (degree 2):
/// a. Combines the two edges into a single edge if they have the same parent.
/// b. Updates the endpoints of the new edge to skip the current vertex.
/// 3. For vertices with degree != 2:
/// a. Creates a new vertex in the minor graph.
/// b. Creates new edges in the minor graph for each outgoing edge.
/// 4. Handles any cyclic components (closed loops with no high-degree vertices).
///
/// The resulting minor graph preserves the topological structure of the paths
/// while reducing the number of vertices and edges.
///
/// # Arguments
///
/// * `major_graph` - A reference to the MajorGraph.
///
/// # Returns
///
/// A new MinorGraph representing the simplified structure.
fn compute_minor(major_graph: &MajorGraph) -> MinorGraph {
let mut new_edges = SlotMap::with_key();
let mut new_vertices = SlotMap::with_key();
let mut to_minor_vertex = HashMap::new();
let mut id_to_edge = HashMap::new();
let mut visited = HashSet::new();
// Handle components that are not cycles
for (major_vertex_key, vertex) in &major_graph.vertices {
// Edges are contracted
if get_order(vertex) == 2 {
continue;
}
let start_vertex = *to_minor_vertex
.entry(major_vertex_key)
.or_insert_with(|| new_vertices.insert(MinorGraphVertex { outgoing_edges: Vec::new() }));
for &start_edge_key in &vertex.outgoing_edges {
let mut segments = Vec::new();
let mut edge_key = start_edge_key;
let mut edge = &major_graph.edges[edge_key];
while edge.parent == major_graph.edges[start_edge_key].parent
&& edge.direction_flag == major_graph.edges[start_edge_key].direction_flag
&& get_order(&major_graph.vertices[edge.incident_vertices[1]]) == 2
{
segments.push(edge.seg);
visited.insert(edge.incident_vertices[1]);
let next_vertex = &major_graph.vertices[edge.incident_vertices[1]];
// Choose the edge which is not our twin so we can make progress
edge_key = *next_vertex.outgoing_edges.iter().find(|&&e| Some(e) != edge.twin).unwrap();
edge = &major_graph.edges[edge_key];
}
segments.push(edge.seg);
let end_vertex = *to_minor_vertex
.entry(edge.incident_vertices[1])
.or_insert_with(|| new_vertices.insert(MinorGraphVertex { outgoing_edges: Vec::new() }));
assert!(major_graph.edges[start_edge_key].twin.is_some());
assert!(edge.twin.is_some());
let edge_id = (start_edge_key, edge_key);
let twin_id = (edge.twin.unwrap(), major_graph.edges[start_edge_key].twin.unwrap());
let twin_key = id_to_edge.get(&twin_id);
let new_edge_key = new_edges.insert(MinorGraphEdge {
segments,
parent: major_graph.edges[start_edge_key].parent,
incident_vertices: [start_vertex, end_vertex],
direction_flag: major_graph.edges[start_edge_key].direction_flag,
twin: twin_key.copied(),
});
if let Some(&twin_key) = twin_key {
new_edges[twin_key].twin = Some(new_edge_key);
}
id_to_edge.insert(edge_id, new_edge_key);
new_vertices[start_vertex].outgoing_edges.push(new_edge_key);
}
}
// Handle cyclic components (if any)
let mut cycles = Vec::new();
for (major_vertex_key, vertex) in &major_graph.vertices {
if vertex.outgoing_edges.len() != 2 || visited.contains(&major_vertex_key) {
continue;
}
let mut edge_key = vertex.outgoing_edges[0];
let mut edge = &major_graph.edges[edge_key];
let mut cycle = MinorGraphCycle {
segments: Vec::new(),
parent: edge.parent,
direction_flag: edge.direction_flag,
};
loop {
cycle.segments.push(edge.seg);
visited.insert(edge.incident_vertices[0]);
assert_eq!(major_graph.vertices[edge.incident_vertices[1]].outgoing_edges.len(), 2, "Found an unvisited vertex of order != 2.");
let next_vertex = &major_graph.vertices[edge.incident_vertices[1]];
edge_key = *next_vertex.outgoing_edges.iter().find(|&&e| Some(e) != edge.twin).unwrap();
edge = &major_graph.edges[edge_key];
if edge.incident_vertices[0] == major_vertex_key {
break;
}
}
cycles.push(cycle);
}
MinorGraph {
edges: new_edges,
vertices: new_vertices,
cycles,
}
}
fn remove_dangling_edges(graph: &mut MinorGraph) {
// Basically DFS for each parent with BFS number
fn walk(parent: u8, graph: &MinorGraph) -> HashSet<MinorVertexKey> {
let mut kept_vertices = HashSet::new();
let mut vertex_to_level = HashMap::new();
fn visit(
vertex: MinorVertexKey,
incoming_edge: Option<MinorEdgeKey>,
level: usize,
graph: &MinorGraph,
vertex_to_level: &mut HashMap<MinorVertexKey, usize>,
kept_vertices: &mut HashSet<MinorVertexKey>,
parent: u8,
) -> usize {
if let Some(&existing_level) = vertex_to_level.get(&vertex) {
return existing_level;
}
vertex_to_level.insert(vertex, level);
let mut min_level = usize::MAX;
for &edge_key in &graph.vertices[vertex].outgoing_edges {
let edge = &graph.edges[edge_key];
if edge.parent & parent != 0 && Some(edge_key) != incoming_edge {
min_level = min_level.min(visit(edge.incident_vertices[1], edge.twin, level + 1, graph, vertex_to_level, kept_vertices, parent));
}
}
if min_level <= level {
kept_vertices.insert(vertex);
}
min_level
}
for edge in graph.edges.values() {
if edge.parent & parent != 0 {
visit(edge.incident_vertices[0], None, 0, graph, &mut vertex_to_level, &mut kept_vertices, parent);
}
}
kept_vertices
}
let kept_vertices_a = walk(1, graph);
let kept_vertices_b = walk(2, graph);
graph.vertices.retain(|k, _| kept_vertices_a.contains(&k) || kept_vertices_b.contains(&k));
for vertex in graph.vertices.values_mut() {
vertex.outgoing_edges.retain(|&edge_key| {
let edge = &graph.edges[edge_key];
(edge.parent & 1 == 1 && kept_vertices_a.contains(&edge.incident_vertices[0]) && kept_vertices_a.contains(&edge.incident_vertices[1]))
|| (edge.parent & 2 == 2 && kept_vertices_b.contains(&edge.incident_vertices[0]) && kept_vertices_b.contains(&edge.incident_vertices[1]))
});
}
// TODO(@TrueDoctor): merge
graph.edges.retain(|_, edge| {
(edge.parent & 1 == 1 && kept_vertices_a.contains(&edge.incident_vertices[0]) && kept_vertices_a.contains(&edge.incident_vertices[1]))
|| (edge.parent & 2 == 2 && kept_vertices_b.contains(&edge.incident_vertices[0]) && kept_vertices_b.contains(&edge.incident_vertices[1]))
});
}
fn sort_outgoing_edges_by_angle(graph: &mut MinorGraph) {
for (vertex_key, vertex) in graph.vertices.iter_mut() {
if vertex.outgoing_edges.len() > 2 {
vertex.outgoing_edges.sort_by(|&a, &b| graph.edges[a].partial_cmp(&graph.edges[b]).unwrap());
if cfg!(feature = "logging") {
eprintln!("Outgoing edges for {:?}:", vertex_key);
for &edge_key in &vertex.outgoing_edges {
let edge = &graph.edges[edge_key];
let angle = edge.start_segment().start_angle();
eprintln!("{:?}: {}°", edge_key.0, angle.to_degrees())
}
}
}
}
}
fn face_to_polygon(face: &DualGraphVertex, edges: &SlotMap<DualEdgeKey, DualGraphHalfEdge>) -> Vec<DVec2> {
const CNT: usize = 3;
face.incident_edges
.iter()
.flat_map(|&edge_key| {
let edge = &edges[edge_key];
edge.segments.iter().flat_map(move |seg| {
(0..CNT).map(move |i| {
let t0 = i as f64 / CNT as f64;
let t = if edge.direction_flag.forward() { t0 } else { 1. - t0 };
seg.sample_at(t)
})
})
})
.collect()
}
fn interval_crosses_point(a: f64, b: f64, p: f64) -> bool {
let dy1 = a >= p;
let dy2 = b < p;
dy1 == dy2
}
fn line_segment_intersects_horizontal_ray(a: DVec2, b: DVec2, point: DVec2) -> bool {
if !interval_crosses_point(a.y, b.y, point.y) {
return false;
}
let x = crate::math::lin_map(point.y, a.y, b.y, a.x, b.x);
x >= point.x
}
fn compute_point_winding(polygon: &[DVec2], tested_point: DVec2) -> i32 {
if polygon.len() <= 2 {
return 0;
}
let mut prev_point = polygon[polygon.len() - 1];
let mut winding = 0;
for &point in polygon {
if line_segment_intersects_horizontal_ray(prev_point, point, tested_point) {
winding += if point.y > prev_point.y { -1 } else { 1 };
}
prev_point = point;
}
winding
}
fn compute_winding(face: &DualGraphVertex, edges: &SlotMap<DualEdgeKey, DualGraphHalfEdge>) -> Option<i32> {
let polygon = face_to_polygon(face, edges);
for i in 0..polygon.len() {
let a = polygon[i];
let b = polygon[(i + 1) % polygon.len()];
let c = polygon[(i + 2) % polygon.len()];
let center = (a + b + c) / 3.;
let winding = compute_point_winding(&polygon, center);
if winding != 0 {
return Some(winding);
}
}
None
}
fn compute_signed_area(face: &DualGraphVertex, edges: &SlotMap<DualEdgeKey, DualGraphHalfEdge>) -> f64 {
let polygon = face_to_polygon(face, edges);
if polygon.len() <= 4 {
return -1.;
}
#[cfg(feature = "logging")]
eprintln!("vertex: {:?}", face);
#[cfg(feature = "logging")]
for point in &polygon {
eprintln!("{}, {}", point.x, point.y);
}
let mut area = 0.;
for i in 0..polygon.len() {
let a = polygon[i];
let b = polygon[(i + 1) % polygon.len()];
area += a.x * b.y;
area -= b.x * a.y;
}
#[cfg(feature = "logging")]
eprintln!("winding: {}", area);
area
}
/// Computes the dual graph from the minor graph.
///
/// This function creates the dual graph by following these steps:
/// 1. Initializes empty structures for dual graph vertices and edges.
/// 2. For each edge in the minor graph:
/// a. Creates a new face (dual vertex) if not already created.
/// b. Traverses around the face, creating dual edges for each minor edge.
/// c. Connects dual edges to their twins (edges representing the same minor edge).
/// 3. Handles special cases like isolated cycles.
/// 4. Groups dual graph elements into connected components.
/// 5. Determines the outer face for each component.
///
/// The dual graph represents faces of the minor graph as vertices and adjacencies
/// between faces as edges, effectively flipping the concepts of vertices and faces.
///
/// # Arguments
///
/// * `minor_graph` - A reference to the MinorGraph.
///
/// # Returns
///
/// A Result containing either the computed DualGraph or a BooleanError if the
/// operation cannot be completed successfully.
fn compute_dual(minor_graph: &MinorGraph) -> Result<DualGraph, BooleanError> {
let mut new_vertices: Vec<DualVertexKey> = Vec::new();
let mut minor_to_dual_edge: HashMap<MinorEdgeKey, DualEdgeKey> = HashMap::new();
let mut dual_edges = SlotMap::with_key();
let mut dual_vertices = SlotMap::with_key();
for (start_edge_key, start_edge) in &minor_graph.edges {
#[cfg(feature = "logging")]
eprintln!("Processing start edge: {}", (start_edge_key.0.as_ffi() & 0xFF));
if minor_to_dual_edge.contains_key(&start_edge_key) {
continue;
}
let face_key = dual_vertices.insert(DualGraphVertex { incident_edges: Vec::new() });
let mut edge_key = start_edge_key;
let mut edge = start_edge;
loop {
#[cfg(feature = "logging")]
eprintln!("Processing edge: {}", (edge_key.0.as_ffi() & 0xFF));
let twin = edge.twin.expect("Edge doesn't have a twin");
let twin_dual_key = minor_to_dual_edge.get(&twin).copied();
let new_edge_key = dual_edges.insert(DualGraphHalfEdge {
segments: edge.segments.clone(),
parent: edge.parent,
incident_vertex: face_key,
direction_flag: edge.direction_flag,
twin: twin_dual_key,
});
if let Some(twin_key) = twin_dual_key {
dual_edges[twin_key].twin = Some(new_edge_key);
}
minor_to_dual_edge.insert(edge_key, new_edge_key);
dual_vertices[face_key].incident_edges.push(new_edge_key);
edge_key = get_next_edge(edge_key, minor_graph);
#[cfg(feature = "logging")]
eprintln!("Next edge: {}", (edge_key.0.as_ffi() & 0xFF));
edge = &minor_graph.edges[edge_key];
if edge.incident_vertices[0] == start_edge.incident_vertices[0] {
break;
}
}
new_vertices.push(face_key);
}
for cycle in &minor_graph.cycles {
let inner_face_key = dual_vertices.insert(DualGraphVertex { incident_edges: Vec::new() });
let outer_face_key = dual_vertices.insert(DualGraphVertex { incident_edges: Vec::new() });
let inner_half_edge_key = dual_edges.insert(DualGraphHalfEdge {
segments: cycle.segments.clone(),
parent: cycle.parent,
incident_vertex: inner_face_key,
direction_flag: cycle.direction_flag,
twin: None,
});
let outer_half_edge_key = dual_edges.insert(DualGraphHalfEdge {
segments: cycle.segments.iter().cloned().rev().collect(),
parent: cycle.parent,
incident_vertex: outer_face_key,
direction_flag: !cycle.direction_flag,
twin: Some(inner_half_edge_key),
});
dual_edges[inner_half_edge_key].twin = Some(outer_half_edge_key);
dual_vertices[inner_face_key].incident_edges.push(inner_half_edge_key);
dual_vertices[outer_face_key].incident_edges.push(outer_half_edge_key);
new_vertices.push(inner_face_key);
new_vertices.push(outer_face_key);
}
let mut components = Vec::new();
let mut visited_vertices = HashSet::new();
let mut visited_edges = HashSet::new();
if cfg!(feature = "logging") {
eprintln!("faces: {}, dual-edges: {}, cycles: {}", new_vertices.len(), dual_edges.len(), minor_graph.cycles.len())
}
// This can be very useful for debugging:
// Copy the face outlines to a file called faces_combined.csv and then use this gnuplot command:
// ```
// plot 'faces_combined.csv' i 0:99 w l, 'faces_combined.csv' index 0 w l lc 'red'
// ```
// The first part of the command plots all faces to the graph and the second comand plots one surface,
// specified by the index, in red. This allows you to check if all surfaces are closed paths and can
// be used in conjunction with the flag debugging to identify issues later down the line as well.
#[cfg(feature = "logging")]
for (vertex_key, vertex) in &dual_vertices {
eprintln!("\n\n#{:?}", vertex_key.0);
let polygon = face_to_polygon(vertex, &dual_edges);
for point in polygon.iter() {
eprintln!("{}, {}", point.x, point.y);
}
eprintln!("{}, {}", polygon[0].x, polygon[0].y);
}
for &start_vertex_key in &new_vertices {
if visited_vertices.contains(&start_vertex_key) {
continue;
}
let mut component_vertices = Vec::new();
let mut component_edges = Vec::new();
let mut stack = vec![start_vertex_key];
while let Some(vertex_key) = stack.pop() {
if visited_vertices.insert(vertex_key) {
component_vertices.push(vertex_key);
}
for &edge_key in &dual_vertices[vertex_key].incident_edges {
if !visited_edges.insert(edge_key) {
continue;
}
let edge = &dual_edges[edge_key];
let twin_key = edge.twin.expect("Edge doesn't have a twin.");
component_edges.push(edge_key);
component_edges.push(twin_key);
visited_edges.insert(twin_key);
stack.push(dual_edges[twin_key].incident_vertex);
}
}
#[cfg(feature = "logging")]
eprintln!("component_vertices: {}", component_vertices.len());
let windings: Option<Vec<_>> = component_vertices
.iter()
.map(|face_key| compute_winding(&dual_vertices[*face_key], &dual_edges).map(|w| (face_key, w)))
.collect();
let Some(windings) = windings else {
return Err(BooleanError::NoEarInPolygon);
};
let areas: Vec<_> = component_vertices
.iter()
.map(|face_key| (face_key, compute_signed_area(&dual_vertices[*face_key], &dual_edges)))
.collect();
#[cfg(feature = "logging")]
dbg!(&areas);
#[cfg(feature = "logging")]
if cfg!(feature = "logging") {
eprintln!(
"{}",
dual_graph_to_dot(
&[DualGraphComponent {
vertices: component_vertices.clone(),
edges: component_edges.clone(),
outer_face: None,
}],
&dual_edges,
)
);
}
let mut count = windings.iter().filter(|(_, winding)| winding < &0).count();
let mut reverse_winding = false;
// If the paths are reversed use positive winding as outer face
if windings.len() > 2 && count == windings.len() - 1 {
count = 1;
reverse_winding = true;
}
let outer_face_key = if count != 1 {
#[cfg(feature = "logging")]
eprintln!("Found multiple outer faces: {areas:?}, falling back to area calculation");
let (key, _) = *areas.iter().max_by_key(|(_, area)| ((area.abs() * 1000.) as u64)).unwrap();
*key
} else {
*windings
.iter()
.find(|&&(&_, ref winding)| (winding < &0) ^ reverse_winding)
.expect("No outer face of a component found.")
.0
};
#[cfg(feature = "logging")]
dbg!(outer_face_key);
components.push(DualGraphComponent {
vertices: component_vertices,
edges: component_edges,
outer_face: Some(outer_face_key),
});
}
Ok(DualGraph {
vertices: dual_vertices,
edges: dual_edges,
components,
})
}
fn get_next_edge(edge_key: MinorEdgeKey, graph: &MinorGraph) -> MinorEdgeKey {
let edge = &graph.edges[edge_key];
let vertex = &graph.vertices[edge.incident_vertices[1]];
#[cfg(feature = "logging")]
eprintln!("{edge_key:?}, twin: {:?}, {:?}", edge.twin, vertex.outgoing_edges);
let index = vertex.outgoing_edges.iter().position(|&e| Some(edge_key) == graph.edges[e].twin).unwrap();
vertex.outgoing_edges[(index + 1) % vertex.outgoing_edges.len()]
}
fn test_inclusion(a: &DualGraphComponent, b: &DualGraphComponent, edges: &SlotMap<DualEdgeKey, DualGraphHalfEdge>, vertices: &SlotMap<DualVertexKey, DualGraphVertex>) -> Option<DualVertexKey> {
let tested_point = edges[a.edges[0]].segments[0].start();
for (face_key, face) in b.vertices.iter().map(|&key| (key, &vertices[key])) {
if Some(face_key) == b.outer_face {
continue;
}
let mut count = 0;
for &edge_key in &face.incident_edges {
let edge = &edges[edge_key];
for seg in &edge.segments {
count += path_segment_horizontal_ray_intersection_count(seg, tested_point);
}
}
if count % 2 == 1 {
return Some(face_key);
}
}
None
}
fn bounding_box_intersects_horizontal_ray(bounding_box: &Aabb, point: DVec2) -> bool {
interval_crosses_point(bounding_box.top, bounding_box.bottom, point[1]) && bounding_box.right >= point[0]
}
struct IntersectionSegment {
bounding_box: Aabb,
seg: PathSegment,
}
pub fn path_segment_horizontal_ray_intersection_count(orig_seg: &PathSegment, point: DVec2) -> usize {
let total_bounding_box = orig_seg.bounding_box();
if !bounding_box_intersects_horizontal_ray(&total_bounding_box, point) {
return 0;
}
let mut segments = vec![IntersectionSegment {
bounding_box: total_bounding_box,
seg: *orig_seg,
}];
let mut count = 0;
while !segments.is_empty() {
let mut next_segments = Vec::new();
for segment in segments {
if bounding_box_max_extent(&segment.bounding_box) < EPS.linear {
if line_segment_intersects_horizontal_ray(segment.seg.start(), segment.seg.end(), point) {
count += 1;
}
} else {
let split = &segment.seg.split_at(0.5);
let bounding_box0 = split.0.bounding_box();
let bounding_box1 = split.1.bounding_box();
if bounding_box_intersects_horizontal_ray(&bounding_box0, point) {
next_segments.push(IntersectionSegment {
bounding_box: bounding_box0,
seg: split.0,
});
}
if bounding_box_intersects_horizontal_ray(&bounding_box1, point) {
next_segments.push(IntersectionSegment {
bounding_box: bounding_box1,
seg: split.1,
});
}
}
}
segments = next_segments;
}
count
}
/// Computes the nesting tree of the dual graph components.
///
/// This function builds a hierarchical structure representing how the components
/// of the dual graph are nested within each other. It does this by:
/// 1. Initializing an empty list of top-level nesting trees.
/// 2. For each component in the dual graph:
/// a. Tests for inclusion against existing nesting trees.
/// b. If included in an existing tree, recursively inserts it at the appropriate level.
/// c. If not included, creates a new top-level tree.
/// d. Checks if any existing trees should become children of the new tree.
/// 3. Continues this process until all components are placed in the nesting structure.
///
/// The resulting nesting tree captures the containment relationships between
/// different regions of the original paths.
///
/// # Arguments
///
/// * `dual_graph` - A reference to the DualGraph.
///
/// # Returns
///
/// A vector of NestingTree structures representing the top-level components and their nested subcomponents.
fn compute_nesting_tree(DualGraph { components, vertices, edges }: &DualGraph) -> Vec<NestingTree> {
let mut nesting_trees = Vec::new();
for component in components {
insert_component(&mut nesting_trees, component, edges, vertices);
}
nesting_trees
}
fn insert_component(trees: &mut Vec<NestingTree>, component: &DualGraphComponent, edges: &SlotMap<DualEdgeKey, DualGraphHalfEdge>, vertices: &SlotMap<DualVertexKey, DualGraphVertex>) {
for tree in trees.iter_mut() {
if let Some(face_key) = test_inclusion(component, &tree.component, edges, vertices) {
if let Some(children) = tree.outgoing_edges.get_mut(&face_key) {
insert_component(children, component, edges, vertices);
} else {
tree.outgoing_edges.insert(
face_key,
vec![NestingTree {
component: component.clone(),
outgoing_edges: HashMap::new(),
}],
);
}
return;
}
}
let mut new_tree = NestingTree {
component: component.clone(),
outgoing_edges: HashMap::new(),
};
let mut i = 0;
while i < trees.len() {
if let Some(face_key) = test_inclusion(&trees[i].component, &new_tree.component, edges, vertices) {
// TODO: (@TrueDoctor) use swap remove
let tree = trees.remove(i);
new_tree.outgoing_edges.entry(face_key).or_default().push(tree);
} else {
i += 1;
}
}
trees.push(new_tree);
}
fn get_flag(count: i32, fill_rule: FillRule) -> u8 {
match fill_rule {
FillRule::NonZero => {
if count == 0 {
0
} else {
1
}
}
FillRule::EvenOdd => (count % 2).unsigned_abs() as u8,
}
}
/// Determines which faces should be included in the result based on the boolean operation.
///
/// This function applies the specified boolean operation and fill rules to decide
/// which regions of the dual graph should be part of the resulting path.
fn flag_faces(
nesting_trees: &[NestingTree],
a_fill_rule: FillRule,
b_fill_rule: FillRule,
edges: &SlotMap<DualEdgeKey, DualGraphHalfEdge>,
vertices: &SlotMap<DualVertexKey, DualGraphVertex>,
flags: &mut HashMap<DualVertexKey, u8>,
) {
for tree in nesting_trees.iter() {
let mut tree_stack = vec![(tree, 0, 0)];
while let Some((current_tree, a_running_count, b_running_count)) = tree_stack.pop() {
let mut visited_faces = HashSet::new();
let mut face_stack = VecDeque::new();
let outer_face_key = current_tree.component.outer_face.expect("Component doesn't have an outer face.");
face_stack.push_back((outer_face_key, a_running_count, b_running_count));
while let Some((face_key, a_count, b_count)) = face_stack.pop_front() {
if visited_faces.contains(&face_key) {
continue;
}
visited_faces.insert(face_key);
let a_flag = get_flag(a_count, a_fill_rule);
let b_flag = get_flag(b_count, b_fill_rule);
*flags.entry(face_key).or_default() = a_flag | (b_flag << 1);
for edge_key in &vertices[face_key].incident_edges {
let edge = &edges[*edge_key];
let twin_key = edge.twin.expect("Edge doesn't have a twin");
#[cfg(feature = "logging")]
eprintln!("Processing edge: {:?} to: {:?}", edge_key.0, edges[twin_key].incident_vertex.0);
let mut next_a_count = a_count;
if edge.parent & 1 != 0 {
next_a_count += if edge.direction_flag.forward() { 1 } else { -1 };
}
let mut next_b_count = b_count;
if edge.parent & 2 != 0 {
next_b_count += if edge.direction_flag.forward() { 1 } else { -1 };
}
#[cfg(feature = "logging")]
eprintln!("next_count a: {}, b:{}", next_a_count, next_b_count);
face_stack.push_back((edges[twin_key].incident_vertex, next_a_count, next_b_count));
}
// Collect subtrees to be processed later
if let Some(subtrees) = current_tree.outgoing_edges.get(&face_key) {
for subtree in subtrees {
tree_stack.push((subtree, a_count, b_count));
}
}
}
}
}
}
fn get_selected_faces<'a>(predicate: &'a impl Fn(u8) -> bool, flags: &'a HashMap<DualVertexKey, u8>) -> impl Iterator<Item = DualVertexKey> + 'a {
flags.iter().filter_map(|(key, &flag)| predicate(flag).then_some(*key))
}
fn walk_faces<'a>(faces: &'a [DualVertexKey], edges: &SlotMap<DualEdgeKey, DualGraphHalfEdge>, vertices: &SlotMap<DualVertexKey, DualGraphVertex>) -> impl Iterator<Item = PathSegment> + use<'a> {
let face_set: HashSet<_> = faces.iter().copied().collect();
// TODO: Try using a binary search to avoid the hashset construction
let is_removed_edge = |edge: &DualGraphHalfEdge| face_set.contains(&edge.incident_vertex) == face_set.contains(&edges[edge.twin.unwrap()].incident_vertex);
let mut edge_to_next = HashMap::new();
for face_key in faces {
let face = &vertices[*face_key];
let mut prev_edge = *face.incident_edges.last().unwrap();
for &edge in &face.incident_edges {
edge_to_next.insert(prev_edge, edge);
prev_edge = edge;
}
}
let mut visited_edges = HashSet::new();
let mut result = Vec::new();
for &face_key in faces {
let face = &vertices[face_key];
for &start_edge in &face.incident_edges {
if is_removed_edge(&edges[start_edge]) || visited_edges.contains(&start_edge) {
continue;
}
let mut edge = start_edge;
loop {
let current_edge = &edges[edge];
if current_edge.direction_flag.forward() {
result.extend(current_edge.segments.iter().cloned());
} else {
result.extend(current_edge.segments.iter().map(PathSegment::reverse));
}
visited_edges.insert(edge);
edge = *edge_to_next.get(&edge).unwrap();
while is_removed_edge(&edges[edge]) {
edge = *edge_to_next.get(&edges[edge].twin.unwrap()).unwrap();
}
if edge == start_edge {
break;
}
}
}
}
result.into_iter()
}
/// Reconstructs the resulting path(s) from the selected faces of the dual graph.
///
/// This function takes the faces that were flagged for inclusion and reconstructs
/// the path segments that form the boundaries of these faces, resulting in the
/// final output of the boolean operation.
fn dump_faces(
nesting_trees: &[NestingTree],
predicate: impl Fn(u8) -> bool + Copy,
edges: &SlotMap<DualEdgeKey, DualGraphHalfEdge>,
vertices: &SlotMap<DualVertexKey, DualGraphVertex>,
flags: &HashMap<DualVertexKey, u8>,
) -> Vec<Path> {
let mut paths = Vec::new();
fn visit(
tree: &NestingTree,
predicate: impl Fn(u8) -> bool + Copy,
paths: &mut Vec<Path>,
edges: &SlotMap<DualEdgeKey, DualGraphHalfEdge>,
vertices: &SlotMap<DualVertexKey, DualGraphVertex>,
flags: &HashMap<DualVertexKey, u8>,
) {
for &face_key in tree.component.vertices.iter() {
let face = &vertices[face_key];
let flag = flags[&face_key];
if !predicate(flag) || Some(face_key) == tree.component.outer_face {
continue;
}
let mut path = Vec::new();
for &edge_key in &face.incident_edges {
let edge = &edges[edge_key];
if edge.direction_flag.forward() {
path.extend(edge.segments.iter().cloned());
} else {
path.extend(edge.segments.iter().map(PathSegment::reverse));
}
}
// Poke holes in the face
if let Some(subtrees) = tree.outgoing_edges.get(&face_key) {
for subtree in subtrees {
let outer_face_key = subtree.component.outer_face.unwrap();
for &edge_key in &vertices[outer_face_key].incident_edges {
let edge = &edges[edge_key];
if edge.direction_flag.forward() {
path.extend(edge.segments.iter().cloned());
} else {
path.extend(edge.segments.iter().map(PathSegment::reverse));
}
}
}
}
paths.push(path);
}
for subtrees in tree.outgoing_edges.values() {
for subtree in subtrees {
visit(subtree, predicate, paths, edges, vertices, flags);
}
}
}
for tree in nesting_trees {
visit(tree, predicate, &mut paths, edges, vertices, flags);
}
paths
}
const OPERATION_PREDICATES: [fn(u8) -> bool; 6] = [
|flag: u8| flag > 0, // Union
|flag: u8| flag == 1, // Difference
|flag: u8| flag == 0b11, // Intersection
|flag: u8| flag == 1 || flag == 2, // Exclusion
|flag: u8| (flag & 1) == 1, // Division
|flag: u8| flag > 0, // Fracture
];
/// Represents errors that can occur during boolean operations on paths.
#[derive(Debug)]
pub enum BooleanError {
/// Indicates that multiple outer faces were found where only one was expected.
MultipleOuterFaces,
/// Indicates that no valid ear was found in a polygon during triangulation. <https://en.wikipedia.org/wiki/Vertex_(geometry)#Ears>
NoEarInPolygon,
InvalidPathCommand(char),
}
impl Display for BooleanError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::MultipleOuterFaces => f.write_str("Found multiple candidates for the outer face in a connected component of the dual graph."),
Self::NoEarInPolygon => f.write_str("Failed to compute winding order for one of the faces, this usually happens when the polygon is malformed."),
Self::InvalidPathCommand(cmd) => f.write_fmt(format_args!("Encountered a '{cmd}' while parsing the svg data which was not recognized")),
}
}
}
/// Performs boolean operations on two paths.
///
/// Takes two paths, applies specified fill rules, and performs a boolean operation,
/// returning the resulting path(s).
///
/// # Examples
///
/// ```
/// use path_bool::{path_boolean, FillRule, PathBooleanOperation, path_from_path_data, path_to_path_data};
///
/// let path_a = path_from_path_data("M 10 10 L 50 10 L 30 40 Z").unwrap();
/// let path_b = path_from_path_data("M 20 30 L 60 30 L 60 50 L 20 50 Z").unwrap();
///
/// let result = path_boolean(
/// &path_a,
/// FillRule::NonZero,
/// &path_b,
/// FillRule::NonZero,
/// PathBooleanOperation::Intersection
/// ).unwrap();
///
/// let result_data = path_to_path_data(&result[0], 0.001);
/// assert_eq!(result_data, "M 36.666666666667,30.000000000000 L 23.333333333333,30.000000000000 L 30.000000000000,40.000000000000 L 36.666666666667,30.000000000000 Z");
/// ```
///
/// # Operations
///
/// The function supports various boolean operations:
/// - Union
/// - Difference
/// - Intersection
/// - Exclusion
/// - Division
/// - Fracture
///
/// See [`PathBooleanOperation`] for more details on each operation.
///
/// # Algorithm
///
/// The boolean operation is performed in several steps:
///
/// 1. Preprocessing: Convert input paths to edges and split at intersections.
/// 2. Graph Construction: Build a graph representation of path segments.
/// 3. Intersection Analysis: Compute intersections between path segments.
/// 4. Graph Transformation: Convert the initial graph into the graph minor using edge contractions.
/// 5. Nesting Analysis: Determine nesting relationships between path parts.
/// 6. Boolean Evaluation: Apply the specified operation based on nesting.
/// 7. Result Construction: Generate final path(s) based on the operation result.
///
/// # Errors
///
/// Returns a [`BooleanError`] if:
/// - Input paths are invalid or cannot be processed.
/// - The operation encounters an unsolvable geometric configuration.
/// - Issues arise in determining the nesting structure of the paths.
pub fn path_boolean(a: &Path, a_fill_rule: FillRule, b: &Path, b_fill_rule: FillRule, op: PathBooleanOperation) -> Result<Vec<Path>, BooleanError> {
let mut unsplit_edges: Vec<MajorGraphEdgeStage1> = a.iter().map(segment_to_edge(1)).chain(b.iter().map(segment_to_edge(2))).flatten().collect();
split_at_self_intersections(&mut unsplit_edges);
let (split_edges, total_bounding_box) = split_at_intersections(&unsplit_edges);
#[cfg(feature = "logging")]
for (edge, _, _) in split_edges.iter() {
eprintln!("{}", path_to_path_data(&vec![*edge], 0.001));
}
let total_bounding_box = match total_bounding_box {
Some(bb) => bb,
None => return Ok(Vec::new()), // Input geometry is empty
};
let major_graph = find_vertices(&split_edges, total_bounding_box);
#[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, 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);
}
for (edge_key, edge) in &minor_graph.edges {
assert!(minor_graph.vertices.contains_key(edge.incident_vertices[0]), "Edge {:?} has invalid start vertex", edge_key);
assert!(minor_graph.vertices.contains_key(edge.incident_vertices[1]), "Edge {:?} has invalid end vertex", edge_key);
assert!(edge.twin.is_some(), "Edge {:?} should have a twin", edge_key);
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 dual_graph = compute_dual(&minor_graph)?;
let nesting_trees = compute_nesting_tree(&dual_graph);
#[cfg(feature = "logging")]
for tree in &nesting_trees {
eprintln!("nesting_trees: {:?}", tree);
}
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 = HashMap::new();
flag_faces(&nesting_trees, a_fill_rule, b_fill_rule, edges, vertices, &mut flags);
#[cfg(feature = "logging")]
for (face, flag) in &flags {
eprintln!("{:?}: {:b}", face.0, flag);
}
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<DualVertexKey> = get_selected_faces(&predicate, &flags).collect();
selected_faces.sort_unstable();
Ok(vec![walk_faces(&selected_faces, edges, vertices).collect()])
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use glam::DVec2;
use std::f64::consts::TAU; // Assuming DVec2 is defined in your crate
#[test]
fn test_split_at_intersections() {
let unsplit_edges = unsplit_edges();
let (split_edges, total_bounding_box) = split_at_intersections(&unsplit_edges);
// Check that we have a valid bounding box
assert!(total_bounding_box.is_some());
// Check that we have more edges after splitting (due to intersections)
assert!(split_edges.len() >= unsplit_edges.len());
// Check that all edges have a valid bounding box
for (_, _, bb) in &split_edges {
assert!(bb.left <= bb.right);
assert!(bb.top <= bb.bottom);
}
// You might want to add more specific checks based on the expected behavior
// of your split_at_intersections function
}
fn unsplit_edges() -> Vec<(PathSegment, u8)> {
let unsplit_edges = vec![
(PathSegment::Arc(DVec2::new(39., 20.), 19., 19., 0., false, true, DVec2::new(20., 39.)), 1),
(PathSegment::Arc(DVec2::new(20., 39.), 19., 19., 0., false, true, DVec2::new(1., 20.)), 1),
(PathSegment::Arc(DVec2::new(1., 20.), 19., 19., 0., false, true, DVec2::new(20., 1.)), 1),
(PathSegment::Arc(DVec2::new(20., 1.), 19., 19., 0., false, true, DVec2::new(39., 20.)), 1),
(PathSegment::Arc(DVec2::new(47., 28.), 19., 19., 0., false, true, DVec2::new(28., 47.)), 2),
(PathSegment::Arc(DVec2::new(28., 47.), 19., 19., 0., false, true, DVec2::new(9., 28.)), 2),
(PathSegment::Arc(DVec2::new(9., 28.), 19., 19., 0., false, true, DVec2::new(28., 9.)), 2),
(PathSegment::Arc(DVec2::new(28., 9.), 19., 19., 0., false, true, DVec2::new(47., 28.)), 2),
];
unsplit_edges
}
#[test]
fn test_compute_minor() {
// Set up the initial graph
let unsplit_edges = unsplit_edges();
let (split_edges, total_bounding_box) = split_at_intersections(&unsplit_edges);
let major_graph = find_vertices(&split_edges, total_bounding_box.unwrap());
// Compute minor graph
let minor_graph = compute_minor(&major_graph);
// Print minor graph state
eprintln!("Minor Graph:");
print_minor_graph_state(&minor_graph);
// Assertions
assert_eq!(minor_graph.edges.len(), 8, "Expected 8 edges in minor graph");
assert_eq!(minor_graph.vertices.len(), 2, "Expected 2 vertices in minor graph");
assert!(minor_graph.cycles.is_empty(), "Expected no cycles in minor graph");
// Check that each vertex has 4 outgoing edges
for (vertex_key, vertex) in &minor_graph.vertices {
assert_eq!(vertex.outgoing_edges.len(), 4, "Vertex {:?} should have 4 outgoing edges", vertex_key);
}
// Check that all edges have valid incident vertices and twins
for (edge_key, edge) in &minor_graph.edges {
assert!(minor_graph.vertices.contains_key(edge.incident_vertices[0]), "Edge {:?} has invalid start vertex", edge_key);
assert!(minor_graph.vertices.contains_key(edge.incident_vertices[1]), "Edge {:?} has invalid end vertex", edge_key);
assert!(edge.twin.is_some(), "Edge {:?} should have a twin", edge_key);
let twin = &minor_graph.edges[edge.twin.unwrap()];
assert_eq!(twin.twin.unwrap(), edge_key, "Twin relationship should be symmetrical for edge {:?}", edge_key);
}
// Check that parents are correctly assigned
assert_eq!(minor_graph.edges.values().filter(|e| e.parent == 1).count(), 4, "Expected 4 edges with parent 1");
assert_eq!(minor_graph.edges.values().filter(|e| e.parent == 2).count(), 4, "Expected 4 edges with parent 2");
}
fn print_minor_graph_state(graph: &MinorGraph) {
eprintln!(" Vertices: {}", graph.vertices.len());
eprintln!(" Edges: {}", graph.edges.len());
eprintln!(" Cycles: {}", graph.cycles.len());
for (vertex_key, vertex) in &graph.vertices {
eprintln!(" Vertex {:?}: {} outgoing edges", vertex_key, vertex.outgoing_edges.len());
}
for (edge_key, edge) in &graph.edges {
eprintln!(" Edge {:?}:", edge_key);
eprintln!(" Parent: {}", edge.parent);
eprintln!(" Twin: {:?}", edge.twin);
eprintln!(" Incident vertices: {:?}", edge.incident_vertices);
}
}
#[test]
fn test_sort_outgoing_edges_by_angle() {
// Set up the initial graph
let unsplit_edges = unsplit_edges();
let (split_edges, total_bounding_box) = split_at_intersections(&unsplit_edges);
let major_graph = find_vertices(&split_edges, total_bounding_box.unwrap());
let mut minor_graph = compute_minor(&major_graph);
// Print initial state
eprintln!("Initial Minor Graph:");
print_minor_graph_state(&minor_graph);
// Store initial edge order
let initial_edge_order: HashMap<MinorVertexKey, Vec<MinorEdgeKey>> = minor_graph.vertices.iter().map(|(k, v)| (k, v.outgoing_edges.clone())).collect();
// Apply sort_outgoing_edges_by_angle
sort_outgoing_edges_by_angle(&mut minor_graph);
// Print final state
eprintln!("\nAfter sort_outgoing_edges_by_angle:");
print_minor_graph_state(&minor_graph);
// Assertions
assert_eq!(minor_graph.edges.len(), 8, "Number of edges should remain unchanged");
assert_eq!(minor_graph.vertices.len(), 2, "Number of vertices should remain unchanged");
assert!(minor_graph.cycles.is_empty(), "Expected no cycles");
// Check that each vertex still has 4 outgoing edges
for (vertex_key, vertex) in &minor_graph.vertices {
assert_eq!(vertex.outgoing_edges.len(), 4, "Vertex {:?} should have 4 outgoing edges", vertex_key);
}
// Check that the edges are sorted by angle
for (vertex_key, vertex) in &minor_graph.vertices {
let angles: Vec<f64> = vertex.outgoing_edges.iter().map(|&edge_key| get_incidence_angle(&minor_graph.edges[edge_key])).collect();
// Check if angles are in ascending order
for i in 1..angles.len() {
assert!(angles[i] >= angles[i - 1], "Edges for vertex {:?} are not sorted by angle {} {}", vertex_key, angles[i], angles[i - 1]);
}
// Check that the set of edges is the same as before, just in different order
let initial_edges: HashSet<_> = initial_edge_order[&vertex_key].iter().collect();
let sorted_edges: HashSet<_> = vertex.outgoing_edges.iter().collect();
assert_eq!(initial_edges, sorted_edges, "Set of edges for vertex {:?} changed after sorting", vertex_key);
}
// Check that all edges still have valid incident vertices and twins
for (edge_key, edge) in &minor_graph.edges {
assert!(minor_graph.vertices.contains_key(edge.incident_vertices[0]), "Edge {:?} has invalid start vertex", edge_key);
assert!(minor_graph.vertices.contains_key(edge.incident_vertices[1]), "Edge {:?} has invalid end vertex", edge_key);
assert!(edge.twin.is_some(), "Edge {:?} should have a twin", edge_key);
let twin = &minor_graph.edges[edge.twin.unwrap()];
assert_eq!(twin.twin.unwrap(), edge_key, "Twin relationship should be symmetrical for edge {:?}", edge_key);
}
}
fn get_incidence_angle(edge: &MinorGraphEdge) -> f64 {
let seg = &edge.segments[0]; // First segment is always the incident one in both fwd and bwd
let (p0, p1) = if edge.direction_flag.forward() {
(seg.sample_at(0.), seg.sample_at(0.1))
} else {
(seg.sample_at(1.), seg.sample_at(1. - 0.1))
};
((p1.y - p0.y).atan2(p1.x - p0.x) + TAU) % TAU
}
#[test]
fn test_path_segment_horizontal_ray_intersection_count() {
let orig_seg = PathSegment::Arc(DVec2::new(24., 10.090978), 13.909023, 13.909023, 0., false, true, DVec2::new(47., 24.));
let point = DVec2::new(37.99, 24.);
eprintln!("Starting test with segment: {:?}", orig_seg);
eprintln!("Test point: {:?}", point);
let count = path_segment_horizontal_ray_intersection_count(&orig_seg, point);
eprintln!("Final intersection count: {}", count);
let expected_count = 1;
assert_eq!(count, expected_count, "Intersection count mismatch");
}
#[test]
fn test_bounding_box_intersects_horizontal_ray() {
let bbox = Aabb {
top: 10.,
right: 40.,
bottom: 30.,
left: 20.,
};
assert!(bounding_box_intersects_horizontal_ray(&bbox, DVec2::new(0., 30.)));
assert!(bounding_box_intersects_horizontal_ray(&bbox, DVec2::new(20., 30.)));
assert!(bounding_box_intersects_horizontal_ray(&bbox, DVec2::new(10., 20.)));
assert!(!bounding_box_intersects_horizontal_ray(&bbox, DVec2::new(30., 40.)));
}
}