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    /// The outer cycle's area alone, the key regions are returned by
74    /// (ascending); `area` nets the holes out.
75    pub outer_area: f64,
76}
77
78#[derive(Clone)]
79struct Node {
80    point: Vec2,
81    outgoing: Vec<usize>,
82}
83
84/// Append-only node lookup. A match always selects the lowest existing node
85/// index, exactly like the original linear `.position` scan, not the nearest
86/// point or the first occupied neighboring cell.
87struct 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            // Squared distances can underflow and accept farther points at
100            // tiny tolerances. Preserve that legacy behavior with a scan.
101            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        // Two-radius cells leave half a cell of rounding headroom. Bound the
108        // quotient so division rounding cannot consume it, or integer neighbors
109        // overflow. This also rejects non-finite coordinates.
110        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            // Once a point cannot be indexed, retain the scan for the rest of
119            // the stream, including comparisons against this newly added node.
120            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                        // Each bucket is in insertion order. Later nodes in
128                        // this bucket cannot beat an already earlier match.
129                        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
266/// Visit a conservative subset of pairs in original input order. The narrow
267/// phase remains authoritative, including its endpoint tolerance and rounding.
268fn 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        // Keep the legacy scan for numeric extremes: underflow/overflow or NaN
292        // in the narrow phase need not behave like geometric intersection.
293        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    // Each accepted parameter may extend its segment by up to tolerance.
320    // Cross-product roundoff is amplified by up to 1/PARALLEL_THRESHOLD when
321    // solving the parameters. Bound it using the global coordinate magnitude
322    // (including translation), with headroom for subtraction, products, length
323    // and division rounding. This deliberately admits extra pairs near parallel
324    // lines instead of treating exact endpoint boxes as a bound on an inexact
325    // solve. The numeric guards above keep these operations in the normal range.
326    let padding = 2.0 * tolerance + 128.0 * f64::EPSILON / PARALLEL_THRESHOLD * scale;
327    // A large translation or tolerance can swallow the entire arrangement.
328    // An index cannot reject anything then, so avoid its query/sort overhead.
329    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