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    if !(chord_tolerance > 0.0) || !chord_tolerance.is_finite() {
29        return Err("tessellate_brep_watertight: chord tolerance must be positive".into());
30    }
31    let started = profile_started();
32    let samples = sample_all_edges(solid, chord_tolerance)?;
33    let sampling_ms = elapsed_ms(started);
34    tess_profile(|p| p.edge_sampling_ms += sampling_ms);
35    tessellate_faces_stride(solid, &samples, chord_tolerance, stride, offset)
36}
37
38/// As [`tessellate_brep_watertight_face_stride`], but the caller supplies edge
39/// samples already computed (and serialized) by [`sample_edges_encoded`]. The
40/// worker-pool split uses this so `sample_all_edges` runs ONCE (in the prepare
41/// step) instead of redundantly in every face-range worker. The samples are
42/// deterministic, so meshing against a shared copy is identical to recomputing
43/// them locally.
44pub fn tessellate_brep_watertight_face_stride_with_samples(
45    solid: &BrepSolid,
46    chord_tolerance: f64,
47    stride: usize,
48    offset: usize,
49    encoded_samples: &[f64],
50) -> Result<Mesh, String> {
51    if !(chord_tolerance > 0.0) || !chord_tolerance.is_finite() {
52        return Err("tessellate_brep_watertight: chord tolerance must be positive".into());
53    }
54    let samples = decode_edge_samples(encoded_samples)?;
55    tessellate_faces_stride(solid, &samples, chord_tolerance, stride, offset)
56}
57
58/// A `(face_id, &FaceRecord)` that can cross rayon worker threads.
59///
60/// SAFETY: `FaceRecord` is `!Sync` only because its owned `NurbsSurface`
61/// memoizes derived data (analytic carrier, seam closure, projection grid) in
62/// `Cell`/`OnceCell` acceleration caches. Those caches live INSIDE each face's
63/// own surface (surfaces are stored inline per face, never shared or `Rc`'d),
64/// and `tessellate_faces_stride` hands each face to exactly one rayon task, so
65/// no cache is ever read or written by two threads at once. The only other
66/// shared input is the edge-sample map, which is immutable and free of interior
67/// mutability. Under those invariants sharing `&FaceRecord` across threads is
68/// sound; if faces ever come to share a surface, revisit this.
69#[cfg(feature = "parallel")]
70pub(super) struct SyncFace<'a>(u32, &'a FaceRecord);
71#[cfg(feature = "parallel")]
72unsafe impl Sync for SyncFace<'_> {}
73
74/// Mesh the faces of the stride/offset class against a shared edge-sample map.
75///
76/// Each face is meshed into its OWN fragment and the fragments are concatenated
77/// in face order — bit-identical to folding them one after another into a
78/// single mesh (a face's triangle indices are relative to its fragment's own
79/// vertex base, which the concat re-bases exactly as the sequential fold did).
80/// Because the fragments are independent, the map is a rayon `par_iter` under
81/// the `parallel` feature (shared-memory in-process threading), and an ordinary
82/// serial map otherwise — the DEFAULT build is unchanged.
83pub(super) fn tessellate_faces_stride(
84    solid: &BrepSolid,
85    samples: &HashMap<u64, EdgeSamples>,
86    chord_tolerance: f64,
87    stride: usize,
88    offset: usize,
89) -> Result<Mesh, String> {
90    let stride = stride.max(1);
91    let offset = offset % stride;
92    let mut work: Vec<(u32, &FaceRecord)> = Vec::new();
93    let mut sequential_face_id = 0u32;
94    for shell in &solid.shells {
95        for face in &shell.faces {
96            if (sequential_face_id as usize) % stride == offset {
97                work.push((sequential_face_id, face));
98            }
99            sequential_face_id += 1;
100        }
101    }
102    // `BREP_PROFILE_TESS_FACES`: arm the counters when nothing outer did (the
103    // soundness scan enters here, not through the orient entry) and print one
104    // `tess.face` line per face; the whole call's `tess.profile` line follows
105    // at the end when this call armed them. Nothing here touches the mesh.
106    let armed_here = arm_for_face_lines();
107    let face_lines = face_lines_enabled();
108    tess_profile(|p| p.faces += work.len() as u64);
109    let mesh_one = |face_id: u32, face: &FaceRecord| -> Result<Mesh, String> {
110        let mut fragment = Mesh::default();
111        if face_lines {
112            let before = snapshot();
113            let started = web_time::Instant::now();
114            tess_profile(|p| p.route = 0);
115            tessellate_face_watertight(face, samples, chord_tolerance, face_id, &mut fragment)?;
116            report_face(
117                face_id,
118                face,
119                fragment.indices.len() / 3,
120                started.elapsed().as_secs_f64() * 1_000.0,
121                before,
122            );
123            return Ok(fragment);
124        }
125        tessellate_face_watertight(face, samples, chord_tolerance, face_id, &mut fragment)?;
126        Ok(fragment)
127    };
128    #[cfg(feature = "parallel")]
129    let fragments = {
130        use rayon::prelude::*;
131        let sync_work: Vec<SyncFace> = work.iter().map(|&(id, face)| SyncFace(id, face)).collect();
132        sync_work
133            .par_iter()
134            .map(|face| mesh_one(face.0, face.1))
135            .collect::<Result<Vec<Mesh>, String>>()?
136    };
137    #[cfg(not(feature = "parallel"))]
138    let fragments = work
139        .iter()
140        .map(|&(id, face)| mesh_one(id, face))
141        .collect::<Result<Vec<Mesh>, String>>()?;
142    let mesh = concatenate_meshes(fragments);
143    if armed_here.is_some() {
144        profile::end(armed_here, chord_tolerance, mesh.indices.len() / 3);
145    }
146    Ok(mesh)
147}
148
149/// Concatenate mesh fragments in order, re-basing each fragment's triangle
150/// indices by the running vertex count (same math as the worker-split weld).
151pub(super) fn concatenate_meshes(fragments: Vec<Mesh>) -> Mesh {
152    let mut mesh = Mesh::default();
153    for fragment in fragments {
154        let base = (mesh.positions.len() / 3) as u32;
155        mesh.positions.extend_from_slice(&fragment.positions);
156        mesh.normals.extend_from_slice(&fragment.normals);
157        mesh.indices
158            .extend(fragment.indices.iter().map(|index| index + base));
159        mesh.face_ids.extend_from_slice(&fragment.face_ids);
160    }
161    mesh
162}
163
164/// Display edge polylines for native in-process consumers (brep-render): every
165/// NON-degenerate edge's chord-tolerance samples as `(edge_id, points)`, sorted
166/// by edge id so the output is deterministic (the sample map is a HashMap).
167/// The samples are the same shared samples the watertight tessellation uses, so
168/// the displayed edges lie exactly on the mesh's face boundaries.
169pub fn sample_edge_polylines(
170    solid: &BrepSolid,
171    chord_tolerance: f64,
172) -> Result<Vec<(u64, Vec<Vec3>)>, String> {
173    if !(chord_tolerance > 0.0) || !chord_tolerance.is_finite() {
174        return Err("tessellate_brep_watertight: chord tolerance must be positive".into());
175    }
176    let samples = sample_all_edges(solid, chord_tolerance)?;
177    let degenerate: std::collections::HashSet<u64> = solid
178        .edges
179        .iter()
180        .filter(|edge| edge.degenerate)
181        .map(|edge| edge.id)
182        .collect();
183    let mut out: Vec<(u64, Vec<Vec3>)> = samples
184        .into_iter()
185        .filter(|(id, samples)| !degenerate.contains(id) && samples.positions.len() >= 2)
186        .map(|(id, samples)| (id, samples.positions))
187        .collect();
188    out.sort_by_key(|(id, _)| *id);
189    Ok(out)
190}
191
192/// Compute every edge's shared samples and serialize them into a flat f64
193/// buffer for transfer to face-range workers. Layout:
194/// `[edge_count, (edge_id, sample_count, frac_0..n, x_0,y_0,z_0, ...), ...]`
195/// (`fractions.len() == positions.len()`, so one count per edge). Edge ids ride
196/// as f64 exactly as the solid codec already carries them.
197pub fn sample_edges_encoded(solid: &BrepSolid, chord_tolerance: f64) -> Result<Vec<f64>, String> {
198    if !(chord_tolerance > 0.0) || !chord_tolerance.is_finite() {
199        return Err("tessellate_brep_watertight: chord tolerance must be positive".into());
200    }
201    let samples = sample_all_edges(solid, chord_tolerance)?;
202    Ok(encode_edge_samples(&samples))
203}
204
205pub(super) fn encode_edge_samples(samples: &HashMap<u64, EdgeSamples>) -> Vec<f64> {
206    let mut out = Vec::new();
207    out.push(samples.len() as f64);
208    for (edge_id, edge) in samples {
209        out.push(*edge_id as f64);
210        out.push(edge.fractions.len() as f64);
211        out.push(edge.worst_sag);
212        out.extend_from_slice(&edge.fractions);
213        for position in &edge.positions {
214            out.push(position.x);
215            out.push(position.y);
216            out.push(position.z);
217        }
218    }
219    out
220}
221
222pub(super) fn decode_edge_samples(data: &[f64]) -> Result<HashMap<u64, EdgeSamples>, String> {
223    let mut cursor = 0usize;
224    let mut take = |count: usize| -> Result<&[f64], String> {
225        let end = cursor
226            .checked_add(count)
227            .filter(|end| *end <= data.len())
228            .ok_or("watertight tessellation: truncated edge-sample buffer")?;
229        let slice = &data[cursor..end];
230        cursor = end;
231        Ok(slice)
232    };
233    let edge_count = take(1)?[0] as usize;
234    let mut samples = HashMap::with_capacity(edge_count);
235    for _ in 0..edge_count {
236        let header = take(3)?;
237        let edge_id = header[0] as u64;
238        let sample_count = header[1] as usize;
239        let worst_sag = header[2];
240        let fractions = take(sample_count)?.to_vec();
241        let position_values = take(sample_count * 3)?;
242        let positions = position_values
243            .chunks_exact(3)
244            .map(|chunk| Vec3::new(chunk[0], chunk[1], chunk[2]))
245            .collect();
246        samples.insert(
247            edge_id,
248            EdgeSamples {
249                fractions,
250                positions,
251                worst_sag,
252            },
253        );
254    }
255    Ok(samples)
256}