Skip to main content

brep_kernel/meshing/watertight_tessellation/
stride_encode.rs

1use super::*;
2
3/// Tessellate only the faces whose global sequential index `i` satisfies
4/// `i % stride == offset`, leaving every other face out of the returned mesh.
5///
6/// This exists so a large solid can be meshed in parallel across several
7/// worker WASM instances: dispatch `stride` calls with `offset` 0..stride-1
8/// and concatenate the fragments. Two properties make that safe:
9///  - Edge samples are recomputed here from the solid and are DETERMINISTIC
10///    (interior points from `curve.evaluate`, endpoints pinned to the exact
11///    shared vertex-record points), so two workers meshing disjoint face sets
12///    produce byte-identical boundary vertices that weld into a watertight
13///    whole — the same guarantee the single-call path relies on.
14///  - The union over `offset` 0..stride-1 is exactly the set of all faces, so
15///    coverage is complete even when the caller's face count is a stale
16///    estimate (the caller only needs to pick `stride`, never an exact range).
17///
18/// Sequential face ids are assigned over ALL faces (not just the meshed ones),
19/// so a fragment's `face_ids` match the single-call output for the same solid.
20/// Unlike [`tessellate_brep_watertight`] this does NOT validate the mesh — a
21/// face subset is not a closed shell; validate after concatenation.
22pub fn tessellate_brep_watertight_face_stride(
23    solid: &BrepSolid,
24    chord_tolerance: f64,
25    stride: usize,
26    offset: usize,
27) -> Result<Mesh, String> {
28    tessellate_brep_watertight_face_stride_keyed(solid, chord_tolerance, stride, offset).map(|(mesh, _)| mesh)
29}
30
31/// [`tessellate_brep_watertight_face_stride`] handing back, beside the mesh,
32/// each meshed face's content key (`face_memo::key`: the face record, its
33/// coedges' shared samples and the chord), in the order the faces were meshed.
34/// Two faces with equal keys mesh to the same bits; the soundness scan keys
35/// its face-pair memo on them.
36pub(crate) fn tessellate_brep_watertight_face_stride_keyed(
37    solid: &BrepSolid,
38    chord_tolerance: f64,
39    stride: usize,
40    offset: usize,
41) -> Result<(Mesh, Vec<[u64; 2]>), String> {
42    if !(chord_tolerance > 0.0) || !chord_tolerance.is_finite() {
43        return Err("tessellate_brep_watertight: chord tolerance must be positive".into());
44    }
45    let armed_here = arm_for_face_lines();
46    let started = profile_started();
47    let samples = sample_all_edges(solid, chord_tolerance)?;
48    let sampling_ms = elapsed_ms(started);
49    tess_profile(|p| p.edge_sampling_ms += sampling_ms);
50    let mut keys = Vec::new();
51    let mesh = tessellate_faces_stride_keyed(solid, &samples, chord_tolerance, stride, offset, Some(&mut keys))?;
52    profile::end(armed_here, chord_tolerance, mesh.indices.len() / 3);
53    Ok((mesh, keys))
54}
55
56/// As [`tessellate_brep_watertight_face_stride`], but the caller supplies edge
57/// samples already computed (and serialized) by [`sample_edges_encoded`]. The
58/// worker-pool split uses this so `sample_all_edges` runs ONCE (in the prepare
59/// step) instead of redundantly in every face-range worker. The samples are
60/// deterministic, so meshing against a shared copy is identical to recomputing
61/// them locally.
62pub fn tessellate_brep_watertight_face_stride_with_samples(
63    solid: &BrepSolid,
64    chord_tolerance: f64,
65    stride: usize,
66    offset: usize,
67    encoded_samples: &[f64],
68) -> Result<Mesh, String> {
69    if !(chord_tolerance > 0.0) || !chord_tolerance.is_finite() {
70        return Err("tessellate_brep_watertight: chord tolerance must be positive".into());
71    }
72    let armed_here = arm_for_face_lines();
73    let samples = decode_edge_samples(encoded_samples)?;
74    let mesh = tessellate_faces_stride(solid, &samples, chord_tolerance, stride, offset)?;
75    profile::end(armed_here, chord_tolerance, mesh.indices.len() / 3);
76    Ok(mesh)
77}
78
79/// A `(face_id, &FaceRecord)` that can cross rayon worker threads.
80///
81/// SAFETY: `FaceRecord` is `!Sync` only because its owned `NurbsSurface`
82/// memoizes derived data (analytic carrier, seam closure, projection grid) in
83/// `Cell`/`OnceCell` acceleration caches. Those caches live INSIDE each face's
84/// own surface (surfaces are stored inline per face, never shared or `Rc`'d),
85/// and `tessellate_faces_stride` hands each face to exactly one rayon task, so
86/// no cache is ever read or written by two threads at once. The only other
87/// shared input is the edge-sample map, which is immutable and free of interior
88/// mutability. Under those invariants sharing `&FaceRecord` across threads is
89/// sound; if faces ever come to share a surface, revisit this.
90#[cfg(feature = "parallel")]
91pub(super) struct SyncFace<'a>(u32, &'a FaceRecord);
92#[cfg(feature = "parallel")]
93unsafe impl Sync for SyncFace<'_> {}
94
95/// Mesh the faces of the stride/offset class against a shared edge-sample map.
96///
97/// Each face is meshed into its OWN fragment and the fragments are concatenated
98/// in face order — bit-identical to folding them one after another into a
99/// single mesh (a face's triangle indices are relative to its fragment's own
100/// vertex base, which the concat re-bases exactly as the sequential fold did).
101/// Because the fragments are independent, the map is a rayon `par_iter` under
102/// the `parallel` feature (shared-memory in-process threading), and an ordinary
103/// serial map otherwise — the DEFAULT build is unchanged.
104pub(super) fn tessellate_faces_stride(
105    solid: &BrepSolid,
106    samples: &HashMap<u64, EdgeSamples>,
107    chord_tolerance: f64,
108    stride: usize,
109    offset: usize,
110) -> Result<Mesh, String> {
111    tessellate_faces_stride_keyed(solid, samples, chord_tolerance, stride, offset, None)
112}
113
114/// [`tessellate_faces_stride`], filling `keys` (when asked) with each meshed
115/// face's content key in meshing order.
116pub(super) fn tessellate_faces_stride_keyed(
117    solid: &BrepSolid,
118    samples: &HashMap<u64, EdgeSamples>,
119    chord_tolerance: f64,
120    stride: usize,
121    offset: usize,
122    keys: Option<&mut Vec<[u64; 2]>>,
123) -> Result<Mesh, String> {
124    let stride = stride.max(1);
125    let offset = offset % stride;
126    let mut work: Vec<(u32, &FaceRecord)> = Vec::new();
127    let mut sequential_face_id = 0u32;
128    for shell in &solid.shells {
129        for face in &shell.faces {
130            if (sequential_face_id as usize) % stride == offset {
131                work.push((sequential_face_id, face));
132            }
133            sequential_face_id += 1;
134        }
135    }
136    // `BREP_PROFILE_TESS_FACES`: arm the counters when nothing outer did (the
137    // soundness scan enters here, not through the orient entry) and print one
138    // `tess.face` line per face; the whole call's `tess.profile` line follows
139    // at the end when this call armed them. Nothing here touches the mesh.
140    let armed_here = arm_for_face_lines();
141    let face_lines = face_lines_enabled();
142    tess_profile(|p| p.faces += work.len() as u64);
143    let mesh_fresh = |face_id: u32, face: &FaceRecord| -> Result<Mesh, String> {
144        let mut fragment = Mesh::default();
145        if face_lines {
146            let before = snapshot();
147            let started = web_time::Instant::now();
148            tess_profile(|p| p.route = 0);
149            tessellate_face_watertight(face, samples, chord_tolerance, face_id, &mut fragment)?;
150            report_face(
151                face_id,
152                face,
153                fragment.indices.len() / 3,
154                started.elapsed().as_secs_f64() * 1_000.0,
155                before,
156            );
157            return Ok(fragment);
158        }
159        tessellate_face_watertight(face, samples, chord_tolerance, face_id, &mut fragment)?;
160        Ok(fragment)
161    };
162    let memo = super::face_memo::enabled();
163    // The faces' content keys, computed once here when the memo or the caller
164    // wants them (the key is a hash of the face and its samples, cheap next to
165    // a mesh).
166    let face_keys: Option<Vec<[u64; 2]>> = (memo || keys.is_some())
167        .then(|| work.iter().map(|&(_, face)| super::face_memo::key(face, samples, chord_tolerance)).collect());
168    if let Some(keys) = keys {
169        *keys = face_keys.clone().unwrap_or_default();
170    }
171    let scan_mesh = super::scan_mesh::active();
172    let mesh_one = |index: usize, face_id: u32, face: &FaceRecord| -> Result<Mesh, String> {
173        let _scan_mesh = super::scan_mesh::ScanMeshScope::set(scan_mesh);
174        // The content memo (`face_memo.rs`): a face meshed before under the
175        // same carrier, trims, shared samples and chord is served its stored
176        // fragment, restamped. A miss meshes as before and stores the result.
177        let key = if memo { face_keys.as_ref().map(|keys| keys[index]) } else { None };
178        if let Some(key) = key {
179            if face_lines { super::face_memo::trace_key(face_id, face, samples, chord_tolerance, key); }
180            if let Some(fragment) = super::face_memo::lookup(key, face_id) {
181                if face_lines {
182                    eprintln!(
183                        "tess.face face={face_id} id={} memo=hit tris={}",
184                        face.id,
185                        fragment.indices.len() / 3
186                    );
187                }
188                return Ok(fragment);
189            }
190        }
191        let fragment = mesh_fresh(face_id, face)?;
192        if let Some(key) = key {
193            super::face_memo::store(key, &fragment);
194        }
195        Ok(fragment)
196    };
197    #[cfg(feature = "parallel")]
198    let fragments = {
199        use rayon::prelude::*;
200        let sync_work: Vec<SyncFace> = work.iter().map(|&(id, face)| SyncFace(id, face)).collect();
201        sync_work
202            .par_iter()
203            .enumerate()
204            .map(|(index, face)| mesh_one(index, face.0, face.1))
205            .collect::<Result<Vec<Mesh>, String>>()?
206    };
207    #[cfg(not(feature = "parallel"))]
208    let fragments = work
209        .iter()
210        .enumerate()
211        .map(|(index, &(id, face))| mesh_one(index, id, face))
212        .collect::<Result<Vec<Mesh>, String>>()?;
213    let mesh = concatenate_meshes(fragments);
214    if tess_profile_enabled() {
215        let (hits, misses, entries, triangles) = super::face_memo::take_counts();
216        profile_line!("tess.memo enabled={memo} hits={hits} misses={misses} entries={entries} stored_tris={triangles}");
217    }
218    if armed_here.is_some() {
219        profile::end(armed_here, chord_tolerance, mesh.indices.len() / 3);
220    }
221    Ok(mesh)
222}
223
224/// Concatenate mesh fragments in order, re-basing each fragment's triangle
225/// indices by the running vertex count (same math as the worker-split weld).
226pub(super) fn concatenate_meshes(fragments: Vec<Mesh>) -> Mesh {
227    let mut mesh = Mesh::default();
228    for fragment in fragments {
229        let base = (mesh.positions.len() / 3) as u32;
230        mesh.positions.extend_from_slice(&fragment.positions);
231        mesh.normals.extend_from_slice(&fragment.normals);
232        mesh.indices
233            .extend(fragment.indices.iter().map(|index| index + base));
234        mesh.face_ids.extend_from_slice(&fragment.face_ids);
235    }
236    mesh
237}
238
239/// Display edge polylines for native in-process consumers (brep-render): every
240/// NON-degenerate edge's chord-tolerance samples as `(edge_id, points)`, sorted
241/// by edge id so the output is deterministic (the sample map is a HashMap).
242/// The samples are the same shared samples the watertight tessellation uses, so
243/// the displayed edges lie exactly on the mesh's face boundaries.
244pub fn sample_edge_polylines(
245    solid: &BrepSolid,
246    chord_tolerance: f64,
247) -> Result<Vec<(u64, Vec<Vec3>)>, String> {
248    if !(chord_tolerance > 0.0) || !chord_tolerance.is_finite() {
249        return Err("tessellate_brep_watertight: chord tolerance must be positive".into());
250    }
251    let samples = sample_all_edges(solid, chord_tolerance)?;
252    let degenerate: std::collections::HashSet<u64> = solid
253        .edges
254        .iter()
255        .filter(|edge| edge.degenerate)
256        .map(|edge| edge.id)
257        .collect();
258    let mut out: Vec<(u64, Vec<Vec3>)> = samples
259        .into_iter()
260        .filter(|(id, samples)| !degenerate.contains(id) && samples.positions.len() >= 2)
261        .map(|(id, samples)| (id, samples.positions))
262        .collect();
263    out.sort_by_key(|(id, _)| *id);
264    Ok(out)
265}
266
267/// Compute every edge's shared samples and serialize them into a flat f64
268/// buffer for transfer to face-range workers. Layout:
269/// `[edge_count, (edge_id, sample_count, frac_0..n, x_0,y_0,z_0, ...), ...]`
270/// (`fractions.len() == positions.len()`, so one count per edge). Edge ids ride
271/// as f64 exactly as the solid codec already carries them.
272pub fn sample_edges_encoded(solid: &BrepSolid, chord_tolerance: f64) -> Result<Vec<f64>, String> {
273    if !(chord_tolerance > 0.0) || !chord_tolerance.is_finite() {
274        return Err("tessellate_brep_watertight: chord tolerance must be positive".into());
275    }
276    let armed_here = arm_for_face_lines();
277    let started = profile_started();
278    let samples = sample_all_edges(solid, chord_tolerance)?;
279    tess_profile(|p| p.edge_sampling_ms += elapsed_ms(started));
280    let encoded = encode_edge_samples(&samples);
281    profile::end(armed_here, chord_tolerance, 0);
282    Ok(encoded)
283}
284
285pub(super) fn encode_edge_samples(samples: &HashMap<u64, EdgeSamples>) -> Vec<f64> {
286    let _timer = stage_timer(Stage::Encode);
287    let mut out = Vec::new();
288    out.push(samples.len() as f64);
289    for (edge_id, edge) in samples {
290        out.push(*edge_id as f64);
291        out.push(edge.fractions.len() as f64);
292        out.push(edge.worst_sag);
293        out.extend_from_slice(&edge.fractions);
294        for position in &edge.positions {
295            out.push(position.x);
296            out.push(position.y);
297            out.push(position.z);
298        }
299    }
300    out
301}
302
303pub(super) fn decode_edge_samples(data: &[f64]) -> Result<HashMap<u64, EdgeSamples>, String> {
304    let _timer = stage_timer(Stage::Decode);
305    let mut cursor = 0usize;
306    let mut take = |count: usize| -> Result<&[f64], String> {
307        let end = cursor
308            .checked_add(count)
309            .filter(|end| *end <= data.len())
310            .ok_or("watertight tessellation: truncated edge-sample buffer")?;
311        let slice = &data[cursor..end];
312        cursor = end;
313        Ok(slice)
314    };
315    let edge_count = take(1)?[0] as usize;
316    let mut samples = HashMap::with_capacity(edge_count);
317    for _ in 0..edge_count {
318        let header = take(3)?;
319        let edge_id = header[0] as u64;
320        let sample_count = header[1] as usize;
321        let worst_sag = header[2];
322        let fractions = take(sample_count)?.to_vec();
323        let position_values = take(sample_count * 3)?;
324        let positions = position_values
325            .chunks_exact(3)
326            .map(|chunk| Vec3::new(chunk[0], chunk[1], chunk[2]))
327            .collect();
328        samples.insert(
329            edge_id,
330            EdgeSamples {
331                fractions,
332                positions,
333                worst_sag,
334            },
335        );
336    }
337    Ok(samples)
338}