Skip to content

Commit 30f8c55

Browse files
committed
Implement support for overlapping path segments
1 parent 85ed8b8 commit 30f8c55

File tree

4 files changed

+161
-26
lines changed

4 files changed

+161
-26
lines changed

libraries/path-bool/src/intersection_path_segment.rs

Lines changed: 98 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use crate::epsilons::Epsilons;
77
use crate::line_segment::{line_segment_intersection, line_segments_intersect};
88
use crate::line_segment_aabb::line_segment_aabb_intersect;
99
use crate::math::lerp;
10-
use crate::path_segment::{path_segment_bounding_box, sample_path_segment_at, split_segment_at, PathSegment};
10+
use crate::path_segment::{get_end_point, get_start_point, path_segment_bounding_box, sample_path_segment_at, split_segment_at, PathSegment};
1111
use crate::vector::{vectors_equal, Vector};
1212

1313
#[derive(Clone)]
@@ -65,7 +65,17 @@ pub fn segments_equal(seg0: &PathSegment, seg1: &PathSegment, point_epsilon: f64
6565
match (*seg0, *seg1) {
6666
(PathSegment::Line(start0, end0), PathSegment::Line(start1, end1)) => vectors_equal(start0, start1, point_epsilon) && vectors_equal(end0, end1, point_epsilon),
6767
(PathSegment::Cubic(p00, p01, p02, p03), PathSegment::Cubic(p10, p11, p12, p13)) => {
68-
vectors_equal(p00, p10, point_epsilon) && vectors_equal(p01, p11, point_epsilon) && vectors_equal(p02, p12, point_epsilon) && vectors_equal(p03, p13, point_epsilon)
68+
let start_and_end_equal = vectors_equal(p00, p10, point_epsilon) && vectors_equal(p03, p13, point_epsilon);
69+
70+
let parameter_equal = vectors_equal(p01, p11, point_epsilon) && vectors_equal(p02, p12, point_epsilon);
71+
let direction1 = sample_path_segment_at(seg0, 0.1);
72+
let direction2 = sample_path_segment_at(seg1, 0.1);
73+
let angles_equal = (direction1 - p00).angle_to(direction2 - p00).abs() < point_epsilon * 4.;
74+
if angles_equal {
75+
eprintln!("deduplicating {:?} {:?} because the angles are equal", seg0, seg1);
76+
}
77+
78+
start_and_end_equal && (parameter_equal || angles_equal)
6979
}
7080
(PathSegment::Quadratic(p00, p01, p02), PathSegment::Quadratic(p10, p11, p12)) => {
7181
vectors_equal(p00, p10, point_epsilon) && vectors_equal(p01, p11, point_epsilon) && vectors_equal(p02, p12, point_epsilon)
@@ -123,6 +133,8 @@ pub fn path_segment_intersection(seg0: &PathSegment, seg1: &PathSegment, endpoin
123133
// dbg!("checking pairs");
124134

125135
if pairs.len() > 1000 {
136+
// TODO: check for intersections of the start/end points. If the two lines overlap, return split points for the start/end points. Use a binary search to check where the points are on the line.
137+
return dbg!(calculate_overlap_intersections(seg0, seg1, eps));
126138
return vec![];
127139
}
128140

@@ -179,6 +191,90 @@ pub fn path_segment_intersection(seg0: &PathSegment, seg1: &PathSegment, endpoin
179191
params
180192
}
181193

194+
fn calculate_overlap_intersections(seg0: &PathSegment, seg1: &PathSegment, eps: &Epsilons) -> Vec<[f64; 2]> {
195+
let start0 = get_start_point(seg0);
196+
let end0 = get_end_point(seg0);
197+
let start1 = get_start_point(seg1);
198+
let end1 = get_end_point(seg1);
199+
200+
let mut intersections = Vec::new();
201+
202+
// Check start0 against seg1
203+
if let Some(t1) = find_point_on_segment(seg1, start0, eps) {
204+
intersections.push([0.0, t1]);
205+
}
206+
207+
// Check end0 against seg1
208+
if let Some(t1) = find_point_on_segment(seg1, end0, eps) {
209+
intersections.push([1.0, t1]);
210+
}
211+
212+
// Check start1 against seg0
213+
if let Some(t0) = find_point_on_segment(seg0, start1, eps) {
214+
intersections.push([t0, 0.0]);
215+
}
216+
217+
// Check end1 against seg0
218+
if let Some(t0) = find_point_on_segment(seg0, end1, eps) {
219+
intersections.push([t0, 1.0]);
220+
}
221+
222+
// Remove duplicates and sort intersections
223+
intersections.sort_unstable_by(|a, b| a[0].partial_cmp(&b[0]).unwrap());
224+
intersections.dedup_by(|a, b| vectors_equal(Vector::new(a[0], a[1]), Vector::new(b[0], b[1]), eps.param));
225+
226+
// Handle special cases
227+
if intersections.is_empty() {
228+
// Check if segments are identical
229+
if vectors_equal(start0, start1, eps.point) && vectors_equal(end0, end1, eps.point) {
230+
return vec![[0.0, 0.0], [1.0, 1.0]];
231+
}
232+
} else if intersections.len() > 2 {
233+
// Keep only the first and last intersection points
234+
intersections = vec![intersections[0], intersections[intersections.len() - 1]];
235+
}
236+
237+
intersections
238+
}
239+
240+
fn find_point_on_segment(seg: &PathSegment, point: Vector, eps: &Epsilons) -> Option<f64> {
241+
let start = 0.0;
242+
let end = 1.0;
243+
let mut t = 0.5;
244+
245+
for _ in 0..32 {
246+
// Limit iterations to prevent infinite loops
247+
let current_point = sample_path_segment_at(seg, t);
248+
249+
if vectors_equal(current_point, point, eps.point) {
250+
return Some(t);
251+
}
252+
253+
let start_point = sample_path_segment_at(seg, start);
254+
let end_point = sample_path_segment_at(seg, end);
255+
256+
let dist_start = (point - start_point).length_squared();
257+
let dist_end = (point - end_point).length_squared();
258+
let dist_current = (point - current_point).length_squared();
259+
260+
if dist_current < dist_start && dist_current < dist_end {
261+
return Some(t);
262+
}
263+
264+
if dist_start < dist_end {
265+
t = (start + t) / 2.0;
266+
} else {
267+
t = (t + end) / 2.0;
268+
}
269+
270+
if (end - start) < eps.param {
271+
break;
272+
}
273+
}
274+
275+
None
276+
}
277+
182278
#[cfg(test)]
183279
mod test {
184280
use super::*;

libraries/path-bool/src/lib.rs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ mod test {
153153
"M458.370270,572.165771C428.525848,486.720093 368.618805,467.485992 273,476 C107.564178,490.730591 161.737915,383.575775 0,340 C0,340 0,689 0,689 C56,700 106.513901,779.342590 188,694.666687 C306.607422,571.416260 372.033966,552.205139 458.370270,572.165771 Z",
154154
);
155155

156-
let result = path_boolean(&a, FillRule::NonZero, &b, FillRule::NonZero, PathBooleanOperation::Difference).unwrap();
156+
let result = path_boolean(&a, FillRule::NonZero, &b, FillRule::NonZero, PathBooleanOperation::Union).unwrap();
157157

158158
// Add assertions here based on expected results
159159
assert_eq!(result.len(), 1, "Expected 1 resulting path for Union operation");
@@ -170,6 +170,36 @@ mod test {
170170

171171
let result = path_boolean(&a, FillRule::NonZero, &b, FillRule::NonZero, PathBooleanOperation::Difference).unwrap();
172172

173+
// Add assertions here based on expected results
174+
assert_eq!(result.len(), 1, "Expected 1 resulting path for Union operation");
175+
dbg!(path_to_path_data(&result[0], 0.001));
176+
// Add more specific assertions about the resulting path if needed
177+
assert!(!result[0].is_empty());
178+
}
179+
#[test]
180+
fn painted_dreams_4() {
181+
let a = path_from_path_data("M458.370270,572.165771C372.033966,552.205139 306.607422,571.416260 188.000000,694.666687 C106.513901,779.342590 56.000000,700.000000 0.000000,689.000000 C0.000000,689.000000 0.000000,768.000000 0.000000,768.000000 C0.000000,768.000000 481.333344,768.000000 481.333344,768.000000 C481.474091,680.589417 474.095154,617.186768 458.370270,572.165771 Z ");
182+
let b = path_from_path_data(
183+
"M364.000000,768.000000C272.000000,686.000000 294.333333,468.666667 173.333333,506.666667 C110.156241,526.507407 0.000000,608.000000 0.000000,608.000000 L -0.000000,768.000000 L 364.000000,768.000000 Z",
184+
);
185+
186+
let result = path_boolean(&a, FillRule::NonZero, &b, FillRule::NonZero, PathBooleanOperation::Difference).unwrap();
187+
188+
// Add assertions here based on expected results
189+
assert_eq!(result.len(), 1, "Expected 1 resulting path for Union operation");
190+
dbg!(path_to_path_data(&result[0], 0.001));
191+
// Add more specific assertions about the resulting path if needed
192+
assert!(!result[0].is_empty());
193+
}
194+
#[test]
195+
fn painted_dreams_5() {
196+
let a = path_from_path_data("M889.000000,0.000000C889.000000,0.000000 889.000000,21.000000 898.000000,46.000000 C909.595887,78.210796 872.365858,104.085306 869.000000,147.000000 C865.000000,198.000000 915.000000,237.000000 933.000000,273.000000 C951.000000,309.000000 951.703704,335.407407 923.000000,349.000000 C898.996281,360.366922 881.000000,367.000000 902.000000,394.000000 C923.000000,421.000000 928.592593,431.407407 898.000000,468.000000 C912.888889,472.888889 929.333333,513.333333 896.000000,523.000000 C896.000000,523.000000 876.000000,533.333333 886.000000,572.000000 C896.458810,612.440732 873.333333,657.777778 802.666667,656.444444 C738.670245,655.236965 689.000000,643.000000 655.000000,636.000000 C621.000000,629.000000 604.000000,623.000000 585.000000,666.000000 C566.000000,709.000000 564.000000,768.000000 564.000000,768.000000 C564.000000,768.000000 0.000000,768.000000 0.000000,768.000000 C0.000000,768.000000 0.000000,0.000000 0.000000,0.000000 C0.000000,0.000000 889.000000,0.000000 889.000000,0.000000 Z");
197+
let b = path_from_path_data(
198+
"M891.555556,569.382716C891.555556,569.382716 883.555556,577.777778 879.111111,595.851852 C874.666667,613.925926 857.185185,631.407407 830.814815,633.777778 C804.444444,636.148148 765.629630,637.925926 708.148148,616.296296 C650.666667,594.666667 560.666667,568.000000 468.000000,487.333333 C375.333333,406.666667 283.333333,354.666667 283.333333,354.666667 C332.000000,330.666667 373.407788,298.323579 468.479950,219.785706 C495.739209,197.267187 505.084065,165.580817 514.452332,146.721008 C525.711584,124.054345 577.519713,94.951389 589.958848,64.658436 C601.152263,37.399177 601.175694,0.000010 601.175694,0.000000 C601.175694,0.000000 0.000000,0.000000 0.000000,0.000000 C0.000000,0.000000 0.000000,768.000000 0.000000,768.000000 C0.000000,768.000000 891.555556,768.000000 891.555556,768.000000 C891.555556,768.000000 891.555556,569.382716 891.555556,569.382716 Z",
199+
);
200+
201+
let result = path_boolean(&a, FillRule::NonZero, &b, FillRule::NonZero, PathBooleanOperation::Intersection).unwrap();
202+
173203
// Add assertions here based on expected results
174204
assert_eq!(result.len(), 1, "Expected 1 resulting path for Union operation");
175205
dbg!(path_to_path_data(&result[0], 0.001));

libraries/path-bool/src/line_segment.rs

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,18 @@ const COLLINEAR_EPS: f64 = f64::EPSILON * 64.0;
1212
pub fn line_segment_intersection([p1, p2]: LineSegment, [p3, p4]: LineSegment, eps: f64) -> Option<(f64, f64)> {
1313
// https://en.wikipedia.org/wiki/Intersection_(geometry)#Two_line_segments
1414

15-
let a1 = p2.x - p1.x;
16-
let b1 = p3.x - p4.x;
17-
let c1 = p3.x - p1.x;
18-
let a2 = p2.y - p1.y;
19-
let b2 = p3.y - p4.y;
20-
let c2 = p3.y - p1.y;
15+
let a = p2 - p1;
16+
let b = p3 - p4;
17+
let c = p3 - p1;
2118

22-
let denom = a1 * b2 - a2 * b1;
19+
let denom = a.x * b.y - a.y * b.x;
2320

2421
if denom.abs() < COLLINEAR_EPS {
2522
return None;
2623
}
2724

28-
let s = (c1 * b2 - c2 * b1) / denom;
29-
let t = (a1 * c2 - a2 * c1) / denom;
25+
let s = (c.x * b.y - c.y * b.x) / denom;
26+
let t = (a.x * c.y - a.y * c.x) / denom;
3027

3128
if (-eps..=1.0 + eps).contains(&s) && (-eps..=1.0 + eps).contains(&t) {
3229
Some((s, t))

libraries/path-bool/src/path_boolean.rs

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -247,15 +247,15 @@ fn dual_graph_to_dot(components: &[DualGraphComponent], edges: &SlotMap<DualEdge
247247

248248
fn segment_to_edge(parent: u8) -> impl Fn(&PathSegment) -> Option<MajorGraphEdgeStage1> {
249249
move |seg| {
250-
if bounding_box_max_extent(&path_segment_bounding_box(seg)) < f64::EPSILON {
250+
if bounding_box_max_extent(&path_segment_bounding_box(seg)) < EPS.point {
251251
return None;
252252
}
253253

254254
match seg {
255255
// Convert Line Segments expressed as cubic beziers to proper line segments
256256
PathSegment::Cubic(start, _, _, end) => {
257257
let direction = sample_path_segment_at(seg, 0.1);
258-
if (end - start).angle_to(direction - start).abs() < EPS.param {
258+
if (end - start).angle_to(direction - start).abs() < EPS.point * 4. {
259259
Some((PathSegment::Line(*start, *end), parent))
260260
} else {
261261
Some((*seg, parent))
@@ -752,10 +752,6 @@ fn compute_point_winding(polygon: &[Vector], tested_point: Vector) -> i32 {
752752

753753
fn compute_winding(face: &DualGraphVertex, edges: &SlotMap<DualEdgeKey, DualGraphHalfEdge>) -> Option<i32> {
754754
let polygon = face_to_polygon(face, edges);
755-
#[cfg(feature = "logging")]
756-
for point in &polygon {
757-
eprintln!("[{}, {}]", point.x, point.y);
758-
}
759755

760756
for i in 0..polygon.len() {
761757
let a = polygon[i];
@@ -824,7 +820,7 @@ fn compute_dual(minor_graph: &MinorGraph) -> Result<DualGraph, BooleanError> {
824820

825821
loop {
826822
#[cfg(feature = "logging")]
827-
eprintln!("Processing edge: {}", (start_edge_key.0.as_ffi() & 0xFF) - 1);
823+
eprintln!("Processing edge: {}", (edge_key.0.as_ffi() & 0xFF));
828824
let twin = edge.twin.expect("Edge doesn't have a twin");
829825
let twin_dual_key = minor_to_dual_edge.get(&twin).copied();
830826

@@ -846,7 +842,7 @@ fn compute_dual(minor_graph: &MinorGraph) -> Result<DualGraph, BooleanError> {
846842

847843
edge_key = get_next_edge(edge_key, minor_graph);
848844
#[cfg(feature = "logging")]
849-
eprintln!("Next edge: {}", (start_edge_key.0.as_ffi() & 0xFF) - 1);
845+
eprintln!("Next edge: {}", (edge_key.0.as_ffi() & 0xFF));
850846
edge = &minor_graph.edges[edge_key];
851847

852848
if edge.incident_vertices[0] == start_edge.incident_vertices[0] {
@@ -940,10 +936,16 @@ fn compute_dual(minor_graph: &MinorGraph) -> Result<DualGraph, BooleanError> {
940936
.iter()
941937
.map(|face_key| compute_winding(&dual_vertices[*face_key], &dual_edges).map(|w| (face_key, w)))
942938
.collect();
943-
let Some(mut windings) = windings else {
939+
let Some(windings) = windings else {
944940
return Err(BooleanError::NoEarInPolygon);
945941
};
946942

943+
let areas: Vec<_> = component_vertices
944+
.iter()
945+
.map(|face_key| (face_key, compute_signed_area(&dual_vertices[*face_key], &dual_edges)))
946+
.collect();
947+
dbg!(&areas);
948+
947949
#[cfg(feature = "logging")]
948950
if cfg!(feature = "logging") {
949951
eprintln!(
@@ -966,11 +968,13 @@ fn compute_dual(minor_graph: &MinorGraph) -> Result<DualGraph, BooleanError> {
966968
count = 1;
967969
reverse_winding = true;
968970
}
969-
if count != 1 {
970-
return Err(BooleanError::MultipleOuterFaces);
971-
}
972-
let outer_face_key = *windings.iter().find(|(&_, winding)| (winding < &0) ^ reverse_winding).expect("No outer face of a component found.").0;
973-
// TODO: merge with previous iter
971+
let outer_face_key = if count != 1 {
972+
// return Err(BooleanError::MultipleOuterFaces);
973+
*areas.iter().max_by_key(|(_, area)| ((area.abs() * 1000.) as u64)).unwrap().0
974+
} else {
975+
*windings.iter().find(|(&_, winding)| (winding < &0) ^ reverse_winding).expect("No outer face of a component found.").0
976+
};
977+
dbg!(outer_face_key);
974978

975979
components.push(DualGraphComponent {
976980
vertices: component_vertices,
@@ -989,6 +993,8 @@ fn compute_dual(minor_graph: &MinorGraph) -> Result<DualGraph, BooleanError> {
989993
fn get_next_edge(edge_key: MinorEdgeKey, graph: &MinorGraph) -> MinorEdgeKey {
990994
let edge = &graph.edges[edge_key];
991995
let vertex = &graph.vertices[edge.incident_vertices[1]];
996+
#[cfg(feature = "logging")]
997+
eprintln!("{edge_key:?}, twin: {:?}, {:?}", edge.twin, vertex.outgoing_edges);
992998
let index = vertex.outgoing_edges.iter().position(|&e| Some(edge_key) == graph.edges[e].twin).unwrap();
993999
vertex.outgoing_edges[(index + 1) % vertex.outgoing_edges.len()]
9941000
}
@@ -1319,6 +1325,12 @@ pub fn path_boolean(a: &Path, a_fill_rule: FillRule, b: &Path, b_fill_rule: Fill
13191325

13201326
let (split_edges, total_bounding_box) = split_at_intersections(&unsplit_edges);
13211327

1328+
#[cfg(feature = "logging")]
1329+
for (edge, _, _) in split_edges.iter() {
1330+
// eprintln!("{}", edge.format_path());
1331+
eprintln!("{}", path_to_path_data(&vec![*edge], 0.001));
1332+
}
1333+
13221334
let total_bounding_box = match total_bounding_box {
13231335
Some(bb) => bb,
13241336
None => return Ok(Vec::new()), // input geometry is empty

0 commit comments

Comments
 (0)