Skip to main content

brep_kernel/intersect/
arrangement.rs

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
81/// Append-only node lookup. A match always selects the lowest existing node
82/// index, exactly like the original linear `.position` scan, not the nearest
83/// point or the first occupied neighboring cell.
84struct 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            // Squared distances can underflow and accept farther points at
97            // tiny tolerances. Preserve that legacy behavior with a scan.
98            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        // Two-radius cells leave half a cell of rounding headroom. Bound the
105        // quotient so division rounding cannot consume it, or integer neighbors
106        // overflow. This also rejects non-finite coordinates.
107        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            // Once a point cannot be indexed, retain the scan for the rest of
116            // the stream, including comparisons against this newly added node.
117            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                        // Each bucket is in insertion order. Later nodes in
125                        // this bucket cannot beat an already earlier match.
126                        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
263/// Visit a conservative subset of pairs in original input order. The narrow
264/// phase remains authoritative, including its endpoint tolerance and rounding.
265fn 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        // Keep the legacy scan for numeric extremes: underflow/overflow or NaN
289        // in the narrow phase need not behave like geometric intersection.
290        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    // Each accepted parameter may extend its segment by up to tolerance.
317    // Cross-product roundoff is amplified by up to 1/PARALLEL_THRESHOLD when
318    // solving the parameters. Bound it using the global coordinate magnitude
319    // (including translation), with headroom for subtraction, products, length
320    // and division rounding. This deliberately admits extra pairs near parallel
321    // lines instead of treating exact endpoint boxes as a bound on an inexact
322    // solve. The numeric guards above keep these operations in the normal range.
323    let padding = 2.0 * tolerance + 128.0 * f64::EPSILON / PARALLEL_THRESHOLD * scale;
324    // A large translation or tolerance can swallow the entire arrangement.
325    // An index cannot reject anything then, so avoid its query/sort overhead.
326    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// BREP private tests: fab715d03529103e