Skip to main content

ifc_lite_clash/
session.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! Public clash session: ingest element geometry, then run rules.
6//!
7//! The broad phase mirrors `packages/clash/src/engine-ts/broad.ts` (BVH over
8//! group_a element AABBs, query each group_b element inflated by `margin`); the
9//! narrow phase delegates to [`crate::narrow::test_pair`]. Records carry GLOBAL
10//! element indices.
11
12use std::cell::RefCell;
13use std::collections::HashSet;
14
15use crate::aabb::Aabb;
16use crate::bvh::Bvh;
17use crate::narrow::{test_pair, ClashStatus};
18use crate::tri_mesh::TriMesh;
19
20/// One classified clash between two elements (GLOBAL element indices).
21pub struct ClashRecord {
22    pub a: u32,
23    pub b: u32,
24    pub status: ClashStatus,
25    pub distance: f64,
26    pub point: [f64; 3],
27    /// `[minx, miny, minz, maxx, maxy, maxz]`.
28    pub bounds: [f64; 6],
29}
30
31/// The records produced by a single rule run.
32pub struct RuleResult {
33    pub records: Vec<ClashRecord>,
34}
35
36/// Per-element geometry plus a lazily built per-triangle BVH.
37struct Element {
38    aabb: Aabb,
39    positions: Vec<f64>,
40    indices: Vec<u32>,
41    /// Built on first narrow-phase use, then cached for the session lifetime.
42    mesh: RefCell<Option<TriMesh>>,
43}
44
45/// A clash session: stores per-element geometry, element AABBs, and caches the
46/// per-element triangle BVHs that the narrow phase needs.
47pub struct ClashSession {
48    elements: Vec<Element>,
49}
50
51impl Default for ClashSession {
52    fn default() -> Self {
53        Self::new()
54    }
55}
56
57impl ClashSession {
58    pub fn new() -> Self {
59        Self {
60            elements: Vec::new(),
61        }
62    }
63
64    /// Ingest `N` elements from flat arenas.
65    ///
66    /// - `positions`: concatenated per-element vertex coords (`x, y, z, ...`).
67    /// - `pos_ranges`: 2 per element = `[float_offset, float_len]`.
68    /// - `indices`: concatenated per-element LOCAL (0-based within that
69    ///   element's vertices) triangle indices.
70    /// - `idx_ranges`: 2 per element = `[idx_offset, idx_len]`.
71    /// - `aabbs`: 6 per element = `[minx, miny, minz, maxx, maxy, maxz]`.
72    ///
73    /// Vertex/AABB coords are `f32`-sourced; they are stored and computed in
74    /// `f64`.
75    pub fn ingest(
76        &mut self,
77        positions: &[f32],
78        pos_ranges: &[u32],
79        indices: &[u32],
80        idx_ranges: &[u32],
81        aabbs: &[f32],
82    ) {
83        // Reset first, so a reused session does not accumulate stale elements.
84        self.elements.clear();
85        let n = pos_ranges.len() / 2;
86        self.elements.reserve(n);
87        for e in 0..n {
88            // Default to an empty element so global indices stay aligned with the
89            // caller's arena even if this element's slices are malformed — no
90            // panic (which under `panic = abort` would poison the shared wasm
91            // module); it simply never produces a clash.
92            let mut element_positions: Vec<f64> = Vec::new();
93            let mut element_indices: Vec<u32> = Vec::new();
94            let mut aabb = Aabb::new([0.0; 3], [0.0; 3]);
95
96            let ranges_ok = e * 2 + 1 < idx_ranges.len() && e * 6 + 5 < aabbs.len();
97            if ranges_ok {
98                let pos_off = pos_ranges[e * 2] as usize;
99                let pos_len = pos_ranges[e * 2 + 1] as usize;
100                let idx_off = idx_ranges[e * 2] as usize;
101                let idx_len = idx_ranges[e * 2 + 1] as usize;
102                if pos_off
103                    .checked_add(pos_len)
104                    .is_some_and(|end| end <= positions.len())
105                    && idx_off
106                        .checked_add(idx_len)
107                        .is_some_and(|end| end <= indices.len())
108                {
109                    element_positions = positions[pos_off..pos_off + pos_len]
110                        .iter()
111                        .map(|&v| v as f64)
112                        .collect();
113                    element_indices = indices[idx_off..idx_off + idx_len].to_vec();
114                    let ab = e * 6;
115                    aabb = Aabb::new(
116                        [aabbs[ab] as f64, aabbs[ab + 1] as f64, aabbs[ab + 2] as f64],
117                        [aabbs[ab + 3] as f64, aabbs[ab + 4] as f64, aabbs[ab + 5] as f64],
118                    );
119                }
120            }
121
122            self.elements.push(Element {
123                aabb,
124                positions: element_positions,
125                indices: element_indices,
126                mesh: RefCell::new(None),
127            });
128        }
129    }
130
131    /// Build (or reuse) the cached triangle mesh for global element `idx` and
132    /// run `f` against it. Avoids re-borrowing the `RefCell` across the call.
133    fn with_mesh<R>(&self, idx: u32, f: impl FnOnce(&TriMesh) -> R) -> R {
134        let element = &self.elements[idx as usize];
135        {
136            let mut slot = element.mesh.borrow_mut();
137            if slot.is_none() {
138                *slot = Some(TriMesh::new(
139                    element.positions.clone(),
140                    element.indices.clone(),
141                ));
142            }
143        }
144        let slot = element.mesh.borrow();
145        f(slot.as_ref().expect("mesh built above"))
146    }
147
148    /// Run one rule.
149    ///
150    /// `group_a` / `group_b` are GLOBAL element indices. An empty `group_b`
151    /// requests a self-clash within `group_a` (pairs with `i < j` by position
152    /// in `group_a`). `mode`: `0` = hard, `1` = clearance. Records carry GLOBAL
153    /// element indices.
154    #[allow(clippy::too_many_arguments)]
155    pub fn run_rule(
156        &self,
157        group_a: &[u32],
158        group_b: &[u32],
159        mode: u8,
160        tolerance: f64,
161        clearance: f64,
162        report_touch: bool,
163    ) -> RuleResult {
164        let is_clearance = mode == 1;
165        let margin = tolerance.max(if is_clearance { clearance } else { 0.0 });
166
167        let pairs = self.candidate_pairs(group_a, group_b, margin);
168
169        let mut records = Vec::new();
170        for (a_global, b_global) in pairs {
171            let result = self.with_mesh(a_global, |mesh_a| {
172                self.with_mesh(b_global, |mesh_b| {
173                    test_pair(
174                        &self.elements[a_global as usize].aabb,
175                        mesh_a,
176                        &self.elements[b_global as usize].aabb,
177                        mesh_b,
178                        mode,
179                        tolerance,
180                        clearance,
181                        report_touch,
182                    )
183                })
184            });
185            if let Some(r) = result {
186                records.push(ClashRecord {
187                    a: a_global,
188                    b: b_global,
189                    status: r.status,
190                    distance: r.distance,
191                    point: r.point,
192                    bounds: [
193                        r.bounds.min[0],
194                        r.bounds.min[1],
195                        r.bounds.min[2],
196                        r.bounds.max[0],
197                        r.bounds.max[1],
198                        r.bounds.max[2],
199                    ],
200                });
201            }
202        }
203
204        RuleResult { records }
205    }
206
207    /// Broad-phase candidate global-index pairs.
208    ///
209    /// Builds a BVH over `group_a`'s element AABBs. For a group pair, each
210    /// `group_b` element queries inflated by `margin`; duplicates are removed
211    /// and identical element indices are skipped. For self-clash (`group_b`
212    /// empty) each `group_a` element queries the BVH, keeping pairs whose
213    /// position in `group_a` satisfies `i < j`.
214    fn candidate_pairs(
215        &self,
216        group_a_in: &[u32],
217        group_b_in: &[u32],
218        margin: f64,
219    ) -> Vec<(u32, u32)> {
220        // Defensively drop any out-of-range global indices at the public boundary.
221        let n = self.elements.len() as u32;
222        let group_a: Vec<u32> = group_a_in.iter().copied().filter(|&g| g < n).collect();
223        let group_b: Vec<u32> = group_b_in.iter().copied().filter(|&g| g < n).collect();
224        if group_a.is_empty() {
225            return Vec::new();
226        }
227
228        // BVH item id is the POSITION in group_a, so query hits map back to the
229        // group_a slot (and thus the global element index).
230        let items: Vec<(u32, Aabb)> = group_a
231            .iter()
232            .enumerate()
233            .map(|(i, &g)| (i as u32, self.elements[g as usize].aabb))
234            .collect();
235        let bvh = Bvh::build(&items);
236
237        let mut pairs: Vec<(u32, u32)> = Vec::new();
238
239        if !group_b.is_empty() {
240            let mut seen: HashSet<(u32, u32)> = HashSet::new();
241            for &b_global in &group_b {
242                let b_aabb = self.elements[b_global as usize].aabb;
243                let hits = bvh.query_aabb(&b_aabb.inflate(margin));
244                for i in hits {
245                    let a_global = group_a[i as usize];
246                    // Skip identical element index (same entity).
247                    if a_global == b_global {
248                        continue;
249                    }
250                    let dedup = if a_global < b_global {
251                        (a_global, b_global)
252                    } else {
253                        (b_global, a_global)
254                    };
255                    if !seen.insert(dedup) {
256                        continue;
257                    }
258                    pairs.push((a_global, b_global));
259                }
260            }
261        } else {
262            for (i, &a_global) in group_a.iter().enumerate() {
263                let a_aabb = self.elements[a_global as usize].aabb;
264                let hits = bvh.query_aabb(&a_aabb.inflate(margin));
265                for j in hits {
266                    let j = j as usize;
267                    if j <= i {
268                        continue;
269                    }
270                    let b_global = group_a[j];
271                    // Skip identical element index (same entity).
272                    if a_global == b_global {
273                        continue;
274                    }
275                    pairs.push((a_global, b_global));
276                }
277            }
278        }
279
280        pairs
281    }
282}