1use crate::spatial::{Aabb, Bvh};
2use crate::Vec3;
3use rustc_hash::FxHashMap;
4use serde::{Deserialize, Serialize};
5
6const PARALLEL_THRESHOLD: f64 = 1e-12;
7
8#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
9pub struct Vec2 {
10 pub x: f64,
11 pub y: f64,
12}
13
14impl Vec2 {
15 pub(crate) fn sub(self, other: Self) -> Self {
16 Self {
17 x: self.x - other.x,
18 y: self.y - other.y,
19 }
20 }
21 pub(crate) fn add(self, other: Self) -> Self {
22 Self {
23 x: self.x + other.x,
24 y: self.y + other.y,
25 }
26 }
27 pub(crate) fn scale(self, factor: f64) -> Self {
28 Self {
29 x: self.x * factor,
30 y: self.y * factor,
31 }
32 }
33 fn distance(self, other: Self) -> f64 {
34 self.sub(other).length()
35 }
36 pub(crate) fn length(self) -> f64 {
37 (self.x * self.x + self.y * self.y).sqrt()
38 }
39 fn cross(self, other: Self) -> f64 {
40 self.x * other.y - self.y * other.x
41 }
42 pub(crate) fn dot(self, other: Self) -> f64 {
43 self.x * other.x + self.y * other.y
44 }
45}
46
47#[derive(Clone, Debug, Deserialize, Serialize)]
48pub struct Segment2 {
49 pub a: Vec2,
50 pub b: Vec2,
51 #[serde(default)]
52 pub tag: serde_json::Value,
53}
54
55#[derive(Clone, Debug, Serialize)]
56pub struct ArrangementPiece {
57 pub a: Vec2,
58 pub b: Vec2,
59 pub parent: Segment2,
60}
61
62#[derive(Clone, Debug, Serialize)]
63pub struct CycleUse {
64 pub piece: ArrangementPiece,
65 pub forward: bool,
66}
67
68#[derive(Clone, Debug, Serialize)]
69pub struct ArrangementRegion {
70 pub outer: Vec<CycleUse>,
71 pub holes: Vec<Vec<CycleUse>>,
72 pub area: f64,
73}
74
75#[derive(Clone)]
76struct Node {
77 point: Vec2,
78 outgoing: Vec<usize>,
79}
80
81struct NodeLookup {
85 tolerance: f64,
86 width: f64,
87 cells: Option<FxHashMap<[i64; 2], Vec<usize>>>,
88}
89
90impl NodeLookup {
91 fn new(tolerance: f64, indexed: bool) -> Self {
92 let width = tolerance * 2.0;
93 Self {
94 tolerance,
95 width,
96 cells: (indexed && tolerance >= 1e-150 && width.is_finite()).then(FxHashMap::default),
99 }
100 }
101
102 fn key(&self, point: Vec2) -> Option<[i64; 2]> {
103 let q = [point.x / self.width, point.y / self.width];
104 q.iter()
108 .all(|v| v.is_finite() && v.abs() <= (1u64 << 48) as f64)
109 .then(|| q.map(|v| v.floor() as i64))
110 }
111
112 fn find_or_insert(&mut self, point: Vec2, nodes: &mut Vec<Node>) -> usize {
113 let key = self.cells.as_ref().and_then(|_| self.key(point));
114 if key.is_none() {
115 self.cells = None;
118 }
119 let found = if let (Some(cells), Some([x, y])) = (&self.cells, key) {
120 let mut first = None;
121 for dx in -1..=1 {
122 for dy in -1..=1 {
123 if let Some(indices) = cells.get(&[x + dx, y + dy]) {
124 for &index in indices {
127 if first.is_some_and(|first| index >= first) {
128 break;
129 }
130 if nodes[index].point.distance(point) <= self.tolerance {
131 first = Some(index);
132 break;
133 }
134 }
135 }
136 }
137 }
138 first
139 } else {
140 nodes
141 .iter()
142 .position(|node| node.point.distance(point) <= self.tolerance)
143 };
144 if let Some(index) = found {
145 return index;
146 }
147 let index = nodes.len();
148 nodes.push(Node {
149 point,
150 outgoing: Vec::new(),
151 });
152 if let (Some(cells), Some(key)) = (&mut self.cells, key) {
153 cells.entry(key).or_default().push(index);
154 }
155 index
156 }
157}
158
159fn tolerance_parameter(segment: &Segment2, tolerance: f64) -> f64 {
160 let length = segment.a.distance(segment.b);
161 if length <= tolerance {
162 0.5
163 } else {
164 tolerance / length
165 }
166}
167
168fn interpolate(a: Vec2, b: Vec2, parameter: f64) -> Vec2 {
169 Vec2 {
170 x: a.x + (b.x - a.x) * parameter,
171 y: a.y + (b.y - a.y) * parameter,
172 }
173}
174
175pub fn segment_intersection(
176 a1: Vec2,
177 b1: Vec2,
178 a2: Vec2,
179 b2: Vec2,
180 tolerance: f64,
181) -> Option<[f64; 2]> {
182 let d1 = b1.sub(a1);
183 let d2 = b2.sub(a2);
184 let denominator = d1.cross(d2);
185 let length1 = d1.length();
186 let length2 = d2.length();
187 if denominator.abs() <= PARALLEL_THRESHOLD * length1 * length2 {
188 return None;
189 }
190 let offset = a2.sub(a1);
191 let t1 = offset.cross(d2) / denominator;
192 let t2 = offset.cross(d1) / denominator;
193 let epsilon1 = tolerance / length1.max(tolerance);
194 let epsilon2 = tolerance / length2.max(tolerance);
195 if t1 < -epsilon1 || t1 > 1.0 + epsilon1 || t2 < -epsilon2 || t2 > 1.0 + epsilon2 {
196 None
197 } else {
198 Some([t1.clamp(0.0, 1.0), t2.clamp(0.0, 1.0)])
199 }
200}
201
202#[derive(Clone, Copy, PartialEq)]
203enum PolygonClass {
204 Inside,
205 Outside,
206 Boundary,
207}
208
209fn point_segment_distance(point: Vec2, a: Vec2, b: Vec2) -> f64 {
210 let segment = b.sub(a);
211 let length_squared = segment.dot(segment);
212 if length_squared <= 1e-300 {
213 return point.distance(a);
214 }
215 let parameter = (point.sub(a).dot(segment) / length_squared).clamp(0.0, 1.0);
216 point.distance(Vec2 {
217 x: a.x + segment.x * parameter,
218 y: a.y + segment.y * parameter,
219 })
220}
221
222fn point_in_polygon_class(point: Vec2, polygon: &[Vec2], tolerance: f64) -> PolygonClass {
223 for index in 0..polygon.len() {
224 if point_segment_distance(point, polygon[index], polygon[(index + 1) % polygon.len()])
225 <= tolerance
226 {
227 return PolygonClass::Boundary;
228 }
229 }
230 let mut inside = false;
231 for index in 0..polygon.len() {
232 let a = polygon[index];
233 let b = polygon[(index + 1) % polygon.len()];
234 if (a.y > point.y) != (b.y > point.y) {
235 let crossing = a.x + (point.y - a.y) / (b.y - a.y) * (b.x - a.x);
236 if crossing > point.x {
237 inside = !inside;
238 }
239 }
240 }
241 if inside {
242 PolygonClass::Inside
243 } else {
244 PolygonClass::Outside
245 }
246}
247
248pub fn point_in_polygon(point: Vec2, polygon: &[Vec2], tolerance: f64) -> &'static str {
249 match point_in_polygon_class(point, polygon, tolerance) {
250 PolygonClass::Inside => "in",
251 PolygonClass::Outside => "out",
252 PolygonClass::Boundary => "boundary",
253 }
254}
255
256#[derive(Clone)]
257struct Cycle {
258 uses: Vec<CycleUse>,
259 area: f64,
260 node_indices: Vec<usize>,
261}
262
263fn visit_segment_pairs(segments: &[Segment2], tolerance: f64, mut visit: impl FnMut(usize, usize)) {
266 let all_pairs = |visit: &mut dyn FnMut(usize, usize)| {
267 for first in 0..segments.len() {
268 for second in first + 1..segments.len() {
269 visit(first, second);
270 }
271 }
272 };
273 if segments.len() <= 32
274 || segments.len() > u32::MAX as usize
275 || !tolerance.is_finite()
276 || tolerance < 0.0
277 || tolerance > 1e100
278 {
279 all_pairs(&mut visit);
280 return;
281 }
282 let mut scale = 0.0f64;
283 let mut boxes = Vec::with_capacity(segments.len());
284 let mut extent = Aabb::empty();
285 for segment in segments {
286 let coordinates = [segment.a.x, segment.a.y, segment.b.x, segment.b.y];
287 let length = segment.a.distance(segment.b);
288 if coordinates
291 .iter()
292 .any(|v| !v.is_finite() || v.abs() > 1e100)
293 || !(1e-100..=1e100).contains(&length)
294 {
295 all_pairs(&mut visit);
296 return;
297 }
298 for coordinate in coordinates {
299 scale = scale.max(coordinate.abs());
300 }
301 let bounds = Aabb {
302 minimum: Vec3::new(
303 segment.a.x.min(segment.b.x),
304 segment.a.y.min(segment.b.y),
305 0.0,
306 ),
307 maximum: Vec3::new(
308 segment.a.x.max(segment.b.x),
309 segment.a.y.max(segment.b.y),
310 0.0,
311 ),
312 };
313 extent.include(bounds);
314 boxes.push(bounds);
315 }
316 let padding = 2.0 * tolerance + 128.0 * f64::EPSILON / PARALLEL_THRESHOLD * scale;
324 if padding >= extent.diagonal() {
327 all_pairs(&mut visit);
328 return;
329 }
330 let tree = Bvh::build(&boxes);
331 let mut candidates = Vec::new();
332 for (first, &bounds) in boxes.iter().enumerate() {
333 candidates.clear();
334 tree.overlapping(bounds, padding, &mut candidates);
335 candidates.retain(|&second| second > first);
336 candidates.sort_unstable();
337 for &second in &candidates {
338 visit(first, second);
339 }
340 }
341}
342
343pub fn arrange_segments(
344 segments: &[Segment2],
345 tolerance: f64,
346) -> Result<Vec<ArrangementRegion>, String> {
347 arrange_segments_impl(segments, tolerance, true, true)
348}
349
350fn arrange_segments_impl(
351 segments: &[Segment2],
352 tolerance: f64,
353 indexed: bool,
354 indexed_nodes: bool,
355) -> Result<Vec<ArrangementRegion>, String> {
356 let mut cuts = vec![Vec::<f64>::new(); segments.len()];
357 let mut intersect_pair = |first: usize, second: usize| {
358 let Some([first_parameter, second_parameter]) = segment_intersection(
359 segments[first].a,
360 segments[first].b,
361 segments[second].a,
362 segments[second].b,
363 tolerance,
364 ) else {
365 return;
366 };
367 let first_tolerance = tolerance_parameter(&segments[first], tolerance);
368 let second_tolerance = tolerance_parameter(&segments[second], tolerance);
369 if first_parameter > first_tolerance && first_parameter < 1.0 - first_tolerance {
370 cuts[first].push(first_parameter);
371 }
372 if second_parameter > second_tolerance && second_parameter < 1.0 - second_tolerance {
373 cuts[second].push(second_parameter);
374 }
375 };
376 if indexed {
377 visit_segment_pairs(segments, tolerance, &mut intersect_pair);
378 } else {
379 for first in 0..segments.len() {
380 for second in first + 1..segments.len() {
381 intersect_pair(first, second);
382 }
383 }
384 }
385
386 let mut pieces = Vec::new();
387 for (index, segment) in segments.iter().enumerate() {
388 let mut parameters = vec![0.0];
389 cuts[index].sort_by(f64::total_cmp);
390 parameters.extend(cuts[index].iter().copied());
391 parameters.push(1.0);
392 let parameter_tolerance = tolerance_parameter(segment, tolerance);
393 let mut deduplicated: Vec<f64> = Vec::new();
394 for parameter in parameters {
395 if deduplicated
396 .last()
397 .is_none_or(|previous| parameter - *previous > parameter_tolerance)
398 {
399 deduplicated.push(parameter);
400 } else if parameter == 1.0 {
401 *deduplicated.last_mut().unwrap() = 1.0;
402 }
403 }
404 for pair in deduplicated.windows(2) {
405 let a = interpolate(segment.a, segment.b, pair[0]);
406 let b = interpolate(segment.a, segment.b, pair[1]);
407 if a.distance(b) <= tolerance {
408 continue;
409 }
410 pieces.push(ArrangementPiece {
411 a,
412 b,
413 parent: segment.clone(),
414 });
415 }
416 }
417
418 let mut nodes: Vec<Node> = Vec::new();
419 let mut lookup = NodeLookup::new(tolerance, indexed_nodes && pieces.len() > 64);
420 let mut piece_start = Vec::with_capacity(pieces.len());
421 let mut piece_end = Vec::with_capacity(pieces.len());
422 for piece in &pieces {
423 piece_start.push(lookup.find_or_insert(piece.a, &mut nodes));
424 piece_end.push(lookup.find_or_insert(piece.b, &mut nodes));
425 }
426
427 let mut alive = vec![true; pieces.len()];
428 loop {
429 let mut pruned = false;
430 let mut degree = vec![0usize; nodes.len()];
431 for index in 0..pieces.len() {
432 if !alive[index] {
433 continue;
434 }
435 if piece_start[index] == piece_end[index] {
436 alive[index] = false;
437 pruned = true;
438 continue;
439 }
440 degree[piece_start[index]] += 1;
441 degree[piece_end[index]] += 1;
442 }
443 for index in 0..pieces.len() {
444 if alive[index] && (degree[piece_start[index]] == 1 || degree[piece_end[index]] == 1) {
445 alive[index] = false;
446 pruned = true;
447 }
448 }
449 if !pruned {
450 break;
451 }
452 }
453
454 let half_tail = |half_edge: usize| {
455 if half_edge % 2 == 0 {
456 piece_start[half_edge >> 1]
457 } else {
458 piece_end[half_edge >> 1]
459 }
460 };
461 let half_head = |half_edge: usize| {
462 if half_edge % 2 == 0 {
463 piece_end[half_edge >> 1]
464 } else {
465 piece_start[half_edge >> 1]
466 }
467 };
468 for node in &mut nodes {
469 node.outgoing.clear();
470 }
471 for index in 0..pieces.len() {
472 if !alive[index] {
473 continue;
474 }
475 nodes[piece_start[index]].outgoing.push(index * 2);
476 nodes[piece_end[index]].outgoing.push(index * 2 + 1);
477 }
478 let angles: Vec<f64> = (0..pieces.len() * 2)
479 .map(|half_edge| {
480 let tail = nodes[half_tail(half_edge)].point;
481 let head = nodes[half_head(half_edge)].point;
482 (head.y - tail.y).atan2(head.x - tail.x)
483 })
484 .collect();
485 for node in &mut nodes {
486 node.outgoing
487 .sort_by(|a, b| angles[*a].total_cmp(&angles[*b]));
488 }
489
490 let mut visited = rustc_hash::FxHashSet::default();
491 let mut positive = Vec::new();
492 let mut negative = Vec::new();
493 for index in 0..pieces.len() {
494 if !alive[index] {
495 continue;
496 }
497 for start in [index * 2, index * 2 + 1] {
498 if visited.contains(&start) {
499 continue;
500 }
501 let mut uses = Vec::new();
502 let mut node_indices = Vec::new();
503 let mut area = 0.0;
504 let mut half_edge = start;
505 let mut guard = 0;
506 loop {
507 visited.insert(half_edge);
508 let piece_index = half_edge >> 1;
509 uses.push(CycleUse {
510 piece: pieces[piece_index].clone(),
511 forward: half_edge % 2 == 0,
512 });
513 let tail_index = half_tail(half_edge);
514 let head_index = half_head(half_edge);
515 let tail = nodes[tail_index].point;
516 let head = nodes[head_index].point;
517 area += 0.5 * (tail.x * head.y - head.x * tail.y);
518 node_indices.push(tail_index);
519 let reverse = half_edge ^ 1;
520 let outgoing = &nodes[head_index].outgoing;
521 let reverse_index = outgoing
522 .iter()
523 .position(|candidate| *candidate == reverse)
524 .ok_or_else(|| "arrangeSegments: reverse half-edge missing".to_string())?;
525 half_edge = outgoing[(reverse_index + outgoing.len() - 1) % outgoing.len()];
526 guard += 1;
527 if guard > pieces.len() * 4 + 8 {
528 return Err("arrangeSegments: face walk did not terminate".into());
529 }
530 if half_edge == start {
531 break;
532 }
533 }
534 let cycle = Cycle {
535 uses,
536 area,
537 node_indices,
538 };
539 if area > tolerance * tolerance {
540 positive.push(cycle);
541 } else if area < -tolerance * tolerance {
542 negative.push(cycle);
543 }
544 }
545 }
546
547 positive.sort_by(|a, b| a.area.total_cmp(&b.area));
548 let mut regions: Vec<(ArrangementRegion, Vec<Vec2>)> = positive
549 .into_iter()
550 .map(|cycle| {
551 let polygon = cycle
552 .node_indices
553 .iter()
554 .map(|index| nodes[*index].point)
555 .collect();
556 (
557 ArrangementRegion {
558 outer: cycle.uses,
559 holes: Vec::new(),
560 area: cycle.area,
561 },
562 polygon,
563 )
564 })
565 .collect();
566 for cycle in negative {
567 for (region, polygon) in &mut regions {
568 let mut inside = false;
569 let mut on_boundary = false;
570 for node_index in &cycle.node_indices {
571 match point_in_polygon_class(nodes[*node_index].point, polygon, tolerance) {
572 PolygonClass::Boundary => {
573 on_boundary = true;
574 continue;
575 }
576 PolygonClass::Inside => {
577 inside = true;
578 on_boundary = false;
579 break;
580 }
581 PolygonClass::Outside => {
582 inside = false;
583 on_boundary = false;
584 break;
585 }
586 }
587 }
588 if on_boundary {
589 continue;
590 }
591 if inside {
592 region.holes.push(cycle.uses.clone());
593 region.area += cycle.area;
594 break;
595 }
596 }
597 }
598 Ok(regions.into_iter().map(|(region, _)| region).collect())
599}
600
601