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#[cfg(test)]
602mod tests {
603 use super::*;
604 use serde_json::json;
605
606 fn segment(a: [f64; 2], b: [f64; 2], tag: &str) -> Segment2 {
607 Segment2 {
608 a: Vec2 { x: a[0], y: a[1] },
609 b: Vec2 { x: b[0], y: b[1] },
610 tag: json!(tag),
611 }
612 }
613
614 fn assert_pair_parity(segments: &[Segment2], tolerance: f64) {
615 let intersection = |i: usize, j: usize| {
616 segment_intersection(
617 segments[i].a,
618 segments[i].b,
619 segments[j].a,
620 segments[j].b,
621 tolerance,
622 )
623 .map(|parameters| (i, j, parameters.map(f64::to_bits)))
624 };
625 let mut expected = Vec::new();
626 for i in 0..segments.len() {
627 for j in i + 1..segments.len() {
628 if let Some(hit) = intersection(i, j) {
629 expected.push(hit);
630 }
631 }
632 }
633 let mut actual = Vec::new();
634 visit_segment_pairs(segments, tolerance, |i, j| {
635 if let Some(hit) = intersection(i, j) {
636 actual.push(hit);
637 }
638 });
639 assert_eq!(
640 actual, expected,
641 "ordered narrow-phase hits must be bit-identical"
642 );
643 }
644
645 #[test]
646 fn indexed_pairs_preserve_crossings_endpoints_and_near_parallel_hits() {
647 for scale in [1e-9, 1.0, 1e9] {
648 for translation in [0.0, -1000.0, 1e12] {
649 let mut segments = Vec::new();
650 let mut add = |a: [f64; 2], b: [f64; 2]| {
651 segments.push(segment(
652 a.map(|v| (v + translation) * scale),
653 b.map(|v| (v + translation) * scale),
654 "pair",
655 ));
656 };
657 for i in -10..10 {
658 let x = i as f64 * 4.0;
659 add([x, 0.0], [x + 1.0, 0.0]);
660 add([x + 1.0 + 5e-8, -1.0], [x + 1.0 + 5e-8, 1.0]);
662 add([x + 0.5, -1.0], [x + 0.5, 1.0]);
663 add([x + 1.0, 0.0], [x, 0.0]); for angle in [0.5e-12, 1.0e-12, 2.0e-12, 1e-10] {
665 add([x, -0.5 * angle], [x + 1.0, 0.5 * angle]);
666 }
667 }
668 assert_pair_parity(&segments, 1e-7 * scale);
669 }
670 }
671 let mut state = 42u64;
672 let mut sample = || {
673 state = state.wrapping_mul(6364136223846793005).wrapping_add(1);
674 ((state >> 32) % 10000) as f64 / 100.0 - 50.0
675 };
676 let segments: Vec<_> = (0..500)
677 .map(|_| segment([sample(), sample()], [sample(), sample()], "random"))
678 .collect();
679 assert_pair_parity(&segments, 1e-7);
680 }
681
682 #[test]
683 fn indexed_pairs_preserve_numeric_fallbacks() {
684 for scale in [0.0, 1e-170, 1e110, f64::INFINITY, f64::NAN] {
685 let segments: Vec<_> = (0..40)
686 .map(|i| {
687 segment(
688 [i as f64 * scale, 0.0],
689 [(i + 1) as f64 * scale, scale],
690 "extreme",
691 )
692 })
693 .collect();
694 assert_pair_parity(&segments, 1e-7);
695 }
696 let segments: Vec<_> = (0..40)
697 .map(|i| segment([i as f64, 0.0], [i as f64, 1.0], "vertical"))
698 .collect();
699 for tolerance in [-1.0, 0.0, 1e110, f64::INFINITY, f64::NAN] {
700 assert_pair_parity(&segments, tolerance);
701 }
702 assert_pair_parity(&[], 1e-7);
703 }
704
705 fn square_grid(count: usize) -> Vec<Segment2> {
706 let mut segments = Vec::new();
707 for i in 0..count {
708 let x = (i % 10) as f64 * 10.0;
709 let y = (i / 10) as f64 * 10.0;
710 for (a, b) in [
711 ([0.0, 0.0], [4.0, 0.0]),
712 ([4.0, 0.0], [4.0, 4.0]),
713 ([4.0, 4.0], [0.0, 4.0]),
714 ([0.0, 4.0], [0.0, 0.0]),
715 ([2.0, 0.0], [2.0, 4.0]),
716 ([0.5, 1.0], [1.5, 1.0]),
717 ([1.5, 1.0], [1.5, 2.0]),
718 ([1.5, 2.0], [0.5, 2.0]),
719 ([0.5, 2.0], [0.5, 1.0]),
720 ] {
721 segments.push(segment([a[0] + x, a[1] + y], [b[0] + x, b[1] + y], "grid"));
722 }
723 }
724 segments
725 }
726
727 fn assert_node_parity(points: &[Vec2], tolerance: f64) -> Vec<usize> {
728 let mut nodes = Vec::new();
729 let mut reference: Vec<Node> = Vec::new();
730 let mut lookup = NodeLookup::new(tolerance, true);
731 let mut ids = Vec::new();
732 for &point in points {
733 let expected = reference
734 .iter()
735 .position(|n| n.point.distance(point) <= tolerance)
736 .unwrap_or_else(|| {
737 reference.push(Node {
738 point,
739 outgoing: Vec::new(),
740 });
741 reference.len() - 1
742 });
743 let actual = lookup.find_or_insert(point, &mut nodes);
744 assert_eq!(actual, expected);
745 ids.push(actual);
746 }
747 let bits = |nodes: &[Node]| {
748 nodes
749 .iter()
750 .map(|n| [n.point.x.to_bits(), n.point.y.to_bits()])
751 .collect::<Vec<_>>()
752 };
753 assert_eq!(bits(&nodes), bits(&reference));
754 ids
755 }
756
757 #[test]
758 fn indexed_nodes_keep_first_match_not_nearest_or_transitive_cluster() {
759 let points = [
760 Vec2 { x: 2.1, y: 0.0 },
761 Vec2 { x: 0.9, y: 0.0 },
762 Vec2 { x: 1.1, y: 0.0 },
763 ];
764 assert_eq!(assert_node_parity(&points, 1.0), vec![0, 1, 0]);
767 let points = [
768 Vec2 { x: 0.0, y: 0.0 },
769 Vec2 { x: 0.75, y: 0.0 },
770 Vec2 { x: 1.5, y: 0.0 },
771 ];
772 assert_eq!(assert_node_parity(&points, 1.0), vec![0, 0, 1]);
774 }
775
776 #[test]
777 fn indexed_nodes_preserve_boundaries_scales_and_fallback_transitions() {
778 for scale in [1e-9, 1.0, 1e9] {
779 for translation in [0.0, -1000.0, 1e12] {
780 let mut points = Vec::new();
781 for i in -20..20 {
782 for offset in [-1e-12, 0.0, 1e-12, 0.5, 1.0] {
783 points.push(Vec2 {
784 x: (translation + i as f64 * 2.0 + offset) * scale,
785 y: (i % 3) as f64 * scale,
786 });
787 }
788 }
789 assert_node_parity(&points, scale);
790 }
791 }
792 for coordinate in [1e100, f64::INFINITY, f64::NAN] {
793 let mut points: Vec<_> = (0..100)
794 .map(|i| Vec2 {
795 x: i as f64,
796 y: 0.0,
797 })
798 .collect();
799 points.push(Vec2 {
800 x: coordinate,
801 y: 0.0,
802 });
803 points.push(Vec2 { x: 2.0, y: 0.0 }); points.push(Vec2 {
805 x: coordinate,
806 y: 0.0,
807 });
808 assert_node_parity(&points, 0.1);
809 }
810 for tolerance in [0.0, -1.0, 1e-180, f64::INFINITY, f64::NAN] {
811 let points: Vec<_> = (0..100)
812 .map(|i| Vec2 {
813 x: i as f64 * 1e-170,
814 y: 0.0,
815 })
816 .collect();
817 assert_node_parity(&points, tolerance);
818 }
819 let mut seed = 9u64;
820 let mut random = || {
821 seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1);
822 ((seed >> 32) % 1000) as f64 / 100.0 - 5.0
823 };
824 let points: Vec<_> = (0..1000)
825 .map(|_| Vec2 {
826 x: random(),
827 y: random(),
828 })
829 .collect();
830 assert_node_parity(&points, 0.2);
831 }
832
833 #[test]
834 fn indexed_nodes_preserve_complete_arrangements() {
835 for count in [1, 4, 40] {
836 let segments = square_grid(count);
837 let reference = arrange_segments_impl(&segments, 1e-7, true, false).unwrap();
838 let indexed = arrange_segments(&segments, 1e-7).unwrap();
839 assert_eq!(
840 serde_json::to_string(&indexed).unwrap(),
841 serde_json::to_string(&reference).unwrap()
842 );
843 }
844 }
845
846 #[test]
847 #[ignore = "manual benchmark: cargo test --release --lib indexed_node_benchmark -- --ignored --nocapture"]
848 fn indexed_node_benchmark() {
849 for count in [1, 4, 40, 200] {
850 let segments = square_grid(count);
851 let mut times = [Vec::new(), Vec::new()];
852 for repeat in 0..7 {
853 let mut outputs = [String::new(), String::new()];
854 for indexed in [repeat % 2 == 0, repeat % 2 != 0] {
856 let start = std::time::Instant::now();
857 let regions =
858 arrange_segments_impl(std::hint::black_box(&segments), 1e-7, true, indexed)
859 .unwrap();
860 times[indexed as usize].push(start.elapsed());
861 outputs[indexed as usize] = serde_json::to_string(®ions).unwrap();
862 }
863 assert_eq!(outputs[0], outputs[1]);
864 }
865 for values in &mut times {
866 values.sort();
867 }
868 eprintln!(
869 "{} segments, full arrangement median: linear {:?}, grid {:?}",
870 segments.len(),
871 times[0][3],
872 times[1][3]
873 );
874 }
875 let points: Vec<_> = square_grid(200)
876 .into_iter()
877 .flat_map(|s| [s.a, s.b])
878 .collect();
879 let mut results = Vec::new();
880 for indexed in [false, true] {
881 let mut lookup = NodeLookup::new(1e-7, indexed);
882 let mut nodes = Vec::new();
883 let start = std::time::Instant::now();
884 let ids: Vec<_> = points
885 .iter()
886 .map(|&p| lookup.find_or_insert(p, &mut nodes))
887 .collect();
888 eprintln!(
889 "node lookup indexed={indexed}: {:?}, {} endpoints, {} nodes",
890 start.elapsed(),
891 points.len(),
892 nodes.len()
893 );
894 results.push(ids);
895 }
896 assert_eq!(results[0], results[1]);
897 }
898
899 #[test]
900 fn indexed_arrangements_preserve_complete_regions_and_prune_pairs() {
901 let segments = square_grid(40);
902 let expected = arrange_segments_impl(&segments, 1e-7, false, false).unwrap();
903 let actual = arrange_segments(&segments, 1e-7).unwrap();
904 assert!(!expected.is_empty());
905 assert_eq!(
906 serde_json::to_string(&actual).unwrap(),
907 serde_json::to_string(&expected).unwrap()
908 );
909 let mut candidates = 0;
910 visit_segment_pairs(&segments, 1e-7, |_, _| candidates += 1);
911 let all = segments.len() * (segments.len() - 1) / 2;
912 assert!(candidates < all / 4, "{candidates} candidates out of {all}");
913 }
914
915 #[test]
916 #[ignore = "manual benchmark: cargo test --release --lib indexed_arrangement_benchmark -- --ignored --nocapture"]
917 fn indexed_arrangement_benchmark() {
918 let segments = square_grid(200);
919 let mut results = Vec::new();
920 for indexed in [false, true] {
921 let start = std::time::Instant::now();
922 let regions =
923 arrange_segments_impl(std::hint::black_box(&segments), 1e-7, indexed, false)
924 .unwrap();
925 eprintln!(
926 "arrangement indexed={indexed}: {:?}, {} segments",
927 start.elapsed(),
928 segments.len()
929 );
930 results.push(serde_json::to_string(®ions).unwrap());
931 }
932 assert_eq!(results[0], results[1]);
933 let mut candidates = 0;
934 visit_segment_pairs(&segments, 1e-7, |_, _| candidates += 1);
935 eprintln!(
936 "pair candidates: {} -> {candidates}",
937 segments.len() * (segments.len() - 1) / 2
938 );
939 }
940
941 #[test]
942 fn crossing_cut_splits_square_into_two_regions() {
943 let segments = vec![
944 segment([0.0, 0.0], [4.0, 0.0], "bottom"),
945 segment([4.0, 0.0], [4.0, 3.0], "right"),
946 segment([4.0, 3.0], [0.0, 3.0], "top"),
947 segment([0.0, 3.0], [0.0, 0.0], "left"),
948 segment([2.0, 0.0], [2.0, 3.0], "cut"),
949 ];
950 let regions = arrange_segments(&segments, 1e-7).unwrap();
951 assert_eq!(regions.len(), 2);
952 assert!(regions
953 .iter()
954 .all(|region| (region.area - 6.0).abs() < 1e-9));
955 }
956
957 #[test]
958 fn nested_cycles_assign_hole() {
959 let segments = vec![
960 segment([0.0, 0.0], [6.0, 0.0], "outer"),
961 segment([6.0, 0.0], [6.0, 6.0], "outer"),
962 segment([6.0, 6.0], [0.0, 6.0], "outer"),
963 segment([0.0, 6.0], [0.0, 0.0], "outer"),
964 segment([2.0, 2.0], [2.0, 4.0], "inner"),
965 segment([2.0, 4.0], [4.0, 4.0], "inner"),
966 segment([4.0, 4.0], [4.0, 2.0], "inner"),
967 segment([4.0, 2.0], [2.0, 2.0], "inner"),
968 ];
969 let regions = arrange_segments(&segments, 1e-7).unwrap();
970 assert!(regions
971 .iter()
972 .any(|region| { region.holes.len() == 1 && (region.area - 32.0).abs() < 1e-9 }));
973 }
974}