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