Skip to main content

brep_kernel/meshing/mesh_segment/
brep_builder.rs

1use super::*;
2
3// ---------------------------------------------------------------------------
4// Stage 3: rebuild a BREP whose faces are the segmented regions
5// ---------------------------------------------------------------------------
6//
7// v1 scope (anything else returns an honest `Err`):
8//   * PLANE regions   → one trimmed planar face each; boundary chains between
9//     two planar carriers are straight (both carriers are flat, so the shared
10//     boundary lies on their intersection line) and become single line edges;
11//     non-straight chains fall back to an interpolated polyline curve.
12//   * CYLINDER / CONE regions bounded by exactly two axis-perpendicular
13//     closed rings → one full revolve wall (seam edge + two exact circle
14//     edges, the `make_cylinder_brep` layout).  Outward walls only
15//     (`sense == +1`); cavity walls are refused.
16//   * CYLINDER regions bounded by one four-chain loop (two axis-perpendicular
17//     arcs + two rulings, the fillet-blend patch) → one exact extruded-arc
18//     face with iso-parameter edges.
19//   * FREEFORM regions: if EVERY region is freeform the whole mesh delegates
20//     to `mesh_to_faceted_brep` (triangle-per-face, documented fallback);
21//     a MIX of freeform and analytic regions is refused rather than risk an
22//     inconsistent shell.
23//   * SPHERE / TORUS regions are refused (their face topology needs pole /
24//     seam handling not built here yet).
25//
26// The exact circle / arc edges are the closed-form plane×revolve
27// intersections, constructed directly from the carriers (iso curves of the
28// revolve / `make_arc` about the axis) so their parameterization lines up
29// with the wall's uv space; `intersect_analytic_pair` yields the same loci
30// but with an uncontrolled seam start, which would force fitted pcurves
31// where an exact parameter line is available.
32
33/// One maximal run of region-boundary mesh edges between corner vertices
34/// (vertices where the adjacent region pair changes or more than two
35/// boundary edges meet).  `vertices` are welded mesh vertex indices; for
36/// closed chains the cycle is stored without repeating the first vertex.
37pub(super) struct Chain {
38    pub(super) vertices: Vec<usize>,
39    pub(super) closed: bool,
40}
41
42/// Ring chain claimed by a full revolve wall: the wall fixed the exact
43/// circle edge and its own traversal direction; the cap side must use the
44/// opposite direction (checked against the cap's mesh circulation).
45#[derive(Clone)]
46pub(super) struct RingClaim {
47    pub(super) axis_point: Vec3,
48    pub(super) axis: Vec3,
49    pub(super) wall_forward: bool,
50}
51
52/// Shared edge built for a chain, plus how its curve direction relates to
53/// the chain's stored vertex order.
54pub(super) struct ChainEdgeInfo {
55    pub(super) edge_id: u64,
56    pub(super) curve_along_chain: bool,
57    pub(super) ring: Option<RingClaim>,
58}
59
60/// One traversal of a chain inside a region's ordered boundary walk.
61pub(super) struct Traversal {
62    pub(super) chain: usize,
63    pub(super) forward_along_chain: bool,
64}
65
66pub(super) struct RegionBrepBuilder<'a> {
67    pub(super) data: &'a MeshData,
68    pub(super) tol: f64,
69    pub(super) chains: Vec<Chain>,
70    edge_chain: HashMap<(usize, usize), usize>,
71    pub(super) chain_edges: Vec<Option<ChainEdgeInfo>>,
72    pub(super) vertices: Vec<VertexRecord>,
73    vertex_ids: HashMap<usize, u64>,
74    edges: Vec<EdgeRecord>,
75    edge_index: HashMap<u64, usize>,
76    next_id: u64,
77}
78
79pub(super) fn wrap_to_pi(mut angle: f64) -> f64 {
80    while angle > std::f64::consts::PI {
81        angle -= std::f64::consts::TAU;
82    }
83    while angle < -std::f64::consts::PI {
84        angle += std::f64::consts::TAU;
85    }
86    angle
87}
88
89pub(super) fn max_deviation_from_segment(points: &[Vec3]) -> f64 {
90    if points.len() < 3 {
91        return 0.0;
92    }
93    let start = points[0];
94    let axis = points[points.len() - 1].sub(start);
95    let length_squared = axis.length_squared();
96    let mut worst = 0.0_f64;
97    for &p in &points[1..points.len() - 1] {
98        let d = p.sub(start);
99        let t = if length_squared > 0.0 {
100            (d.dot(axis) / length_squared).clamp(0.0, 1.0)
101        } else {
102            0.0
103        };
104        worst = worst.max(d.sub(axis.scale(t)).length());
105    }
106    worst
107}
108
109fn chord_parameters(points: &[Vec3]) -> Result<Vec<f64>, String> {
110    let mut cumulative = vec![0.0_f64];
111    for pair in points.windows(2) {
112        let step = pair[1].sub(pair[0]).length();
113        if !(step > 0.0) {
114            return Err("mesh_regions_to_brep: repeated point in boundary chain".into());
115        }
116        cumulative.push(cumulative.last().unwrap() + step);
117    }
118    let total = *cumulative.last().unwrap();
119    Ok(cumulative.into_iter().map(|value| value / total).collect())
120}
121
122/// Map a curve lying in a plane into the plane's parameter space (exact for
123/// the affine `make_plane` patches: unit frame directions, arc-length uv).
124pub(super) fn affine_pcurve(
125    curve: &NurbsCurve,
126    origin: Vec3,
127    x_axis: Vec3,
128    y_axis: Vec3,
129) -> Result<NurbsCurve, String> {
130    let points = curve
131        .control_points
132        .iter()
133        .map(|control| {
134            let delta = control.point()?.sub(origin);
135            Ok(Vec4 {
136                x: delta.dot(x_axis) * control.w,
137                y: delta.dot(y_axis) * control.w,
138                z: 0.0,
139                w: control.w,
140            })
141        })
142        .collect::<Result<Vec<_>, String>>()?;
143    NurbsCurve::new(curve.degree, curve.knots.clone(), points)
144}
145
146pub(super) fn parameter_segment(u0: f64, v0: f64, u1: f64, v1: f64) -> Result<NurbsCurve, String> {
147    make_line(Vec3::new(u0, v0, 0.0), Vec3::new(u1, v1, 0.0))
148}
149
150pub(super) fn translate_curve(curve: &NurbsCurve, delta: Vec3) -> Result<NurbsCurve, String> {
151    let points = curve
152        .control_points
153        .iter()
154        .map(|control| Vec4 {
155            x: control.x + control.w * delta.x,
156            y: control.y + control.w * delta.y,
157            z: control.z + control.w * delta.z,
158            w: control.w,
159        })
160        .collect();
161    NurbsCurve::new(curve.degree, curve.knots.clone(), points)
162}
163
164impl<'a> RegionBrepBuilder<'a> {
165    pub(super) fn allocate_id(&mut self) -> u64 {
166        let id = self.next_id;
167        self.next_id += 1;
168        id
169    }
170
171    pub(super) fn chain_points(&self, chain: &Chain) -> Vec<Vec3> {
172        chain.vertices.iter().map(|&v| self.data.verts[v]).collect()
173    }
174
175    pub(super) fn welded_vertex_id(&mut self, welded: usize) -> u64 {
176        if let Some(&id) = self.vertex_ids.get(&welded) {
177            return id;
178        }
179        let id = self.next_id;
180        self.next_id += 1;
181        self.vertices.push(VertexRecord {
182            id,
183            point: self.data.verts[welded],
184        });
185        self.vertex_ids.insert(welded, id);
186        id
187    }
188
189    pub(super) fn push_edge(&mut self, mut edge: EdgeRecord) -> u64 {
190        edge.id = self.next_id;
191        self.next_id += 1;
192        self.edge_index.insert(edge.id, self.edges.len());
193        let id = edge.id;
194        self.edges.push(edge);
195        id
196    }
197
198    pub(super) fn edge_curve(&self, edge_id: u64) -> &NurbsCurve {
199        &self.edges[self.edge_index[&edge_id]].curve
200    }
201
202    /// Direction of the chain step `(a, b)` relative to the chain's stored
203    /// vertex order (`Some(true)` = along the stored order).
204    fn chain_step_direction(chain: &Chain, a: usize, b: usize) -> Option<bool> {
205        let n = chain.vertices.len();
206        let last = if chain.closed { n } else { n - 1 };
207        for index in 0..last {
208            let s = chain.vertices[index];
209            let e = chain.vertices[(index + 1) % n];
210            if s == a && e == b {
211                return Some(true);
212            }
213            if s == b && e == a {
214                return Some(false);
215            }
216        }
217        None
218    }
219
220    /// Split a region's ordered boundary cycle into chain traversals.
221    pub(super) fn cycle_traversals(&self, cycle: &[usize]) -> Result<Vec<Traversal>, String> {
222        let n = cycle.len();
223        if n < 2 {
224            return Err("mesh_regions_to_brep: degenerate boundary cycle".into());
225        }
226        let step_chain = (0..n)
227            .map(|i| {
228                self.edge_chain
229                    .get(&edge_key(cycle[i], cycle[(i + 1) % n]))
230                    .copied()
231                    .ok_or_else(|| {
232                        "mesh_regions_to_brep: boundary step outside every chain".to_string()
233                    })
234            })
235            .collect::<Result<Vec<_>, _>>()?;
236        if step_chain.iter().all(|&c| c == step_chain[0]) {
237            let chain = &self.chains[step_chain[0]];
238            if !chain.closed {
239                return Err("mesh_regions_to_brep: cycle traverses an open chain only".into());
240            }
241            let forward = Self::chain_step_direction(chain, cycle[0], cycle[1])
242                .ok_or("mesh_regions_to_brep: cycle step not found in its ring chain")?;
243            return Ok(vec![Traversal {
244                chain: step_chain[0],
245                forward_along_chain: forward,
246            }]);
247        }
248        let start = (0..n)
249            .find(|&i| step_chain[i] != step_chain[(i + n - 1) % n])
250            .expect("mixed chains imply a boundary between them");
251        let mut traversals: Vec<Traversal> = Vec::new();
252        let mut index = 0;
253        while index < n {
254            let at = (start + index) % n;
255            let chain_id = step_chain[at];
256            let mut run = 0;
257            while index + run < n && step_chain[(start + index + run) % n] == chain_id {
258                run += 1;
259            }
260            let chain = &self.chains[chain_id];
261            if chain.closed || run != chain.vertices.len() - 1 {
262                return Err(
263                    "mesh_regions_to_brep: partial chain traversal (pinched region boundary)"
264                        .into(),
265                );
266            }
267            let a = cycle[at];
268            let b = cycle[(at + 1) % n];
269            let forward = Self::chain_step_direction(chain, a, b)
270                .ok_or("mesh_regions_to_brep: traversal step not found in its chain")?;
271            traversals.push(Traversal {
272                chain: chain_id,
273                forward_along_chain: forward,
274            });
275            index += run;
276        }
277        Ok(traversals)
278    }
279
280    /// Build (or reuse) the shared edge of an open chain between planar
281    /// carriers: a single line edge when the polyline is straight (the exact
282    /// plane×plane case), an interpolated cubic through the polyline
283    /// otherwise.
284    pub(super) fn ensure_open_chain_edge(&mut self, chain_id: usize) -> Result<(), String> {
285        if self.chain_edges[chain_id].is_some() {
286            return Ok(());
287        }
288        let chain = &self.chains[chain_id];
289        if chain.closed {
290            return Err(
291                "mesh_regions_to_brep: closed boundary ring not adjacent to any revolve wall"
292                    .into(),
293            );
294        }
295        let points = self.chain_points(chain);
296        let (first, last) = (chain.vertices[0], *chain.vertices.last().unwrap());
297        let curve = if max_deviation_from_segment(&points) <= self.tol {
298            make_line(points[0], *points.last().unwrap())?
299        } else {
300            let parameters = chord_parameters(&points)?;
301            interpolate_curve(&points, 3.min(points.len() - 1), &parameters)?
302        };
303        let start_vertex_id = self.welded_vertex_id(first);
304        let end_vertex_id = self.welded_vertex_id(last);
305        let edge_id = self.push_edge(EdgeRecord {
306            id: 0,
307            curve,
308            t0: 0.0,
309            t1: 1.0,
310            start_vertex_id,
311            end_vertex_id,
312            degenerate: false,
313            name: None,
314        });
315        self.chain_edges[chain_id] = Some(ChainEdgeInfo {
316            edge_id,
317            curve_along_chain: true,
318            ring: None,
319        });
320        Ok(())
321    }
322
323    /// Signed circulation (in radians, ±2π for a ring) of an ordered vertex
324    /// cycle around an axis.
325    pub(super) fn cycle_circulation(
326        &self,
327        cycle: &[usize],
328        axis_point: Vec3,
329        axis: Vec3,
330    ) -> Result<f64, String> {
331        let x_axis = axis.perpendicular()?;
332        let y_axis = axis.cross(x_axis);
333        let angle_of = |p: Vec3| -> f64 {
334            let d = p.sub(axis_point);
335            let radial = d.sub(axis.scale(d.dot(axis)));
336            radial.dot(y_axis).atan2(radial.dot(x_axis))
337        };
338        let mut total = 0.0;
339        for index in 0..cycle.len() {
340            let a = angle_of(self.data.verts[cycle[index]]);
341            let b = angle_of(self.data.verts[cycle[(index + 1) % cycle.len()]]);
342            total += wrap_to_pi(b - a);
343        }
344        Ok(total)
345    }
346}
347
348/// Rebuild a `BrepSolid` whose faces are the mesh's segmented regions.  The
349/// result always passes full topology validation (watertight edge pairing,
350/// Euler accounting, pcurve consistency) or the call returns `Err` — an
351/// invalid solid is never returned.  See the module-level v1 scope notes.
352pub fn mesh_regions_to_brep(
353    positions: &[f64],
354    indices: &[u32],
355    options: &SegmentOptions,
356) -> Result<BrepSolid, String> {
357    if !(options.deflection_angle_deg > 0.0) || !options.deflection_angle_deg.is_finite() {
358        return Err("mesh_regions_to_brep: deflection angle must be positive".into());
359    }
360    if !(options.fit_tolerance > 0.0) || !options.fit_tolerance.is_finite() {
361        return Err("mesh_regions_to_brep: fit tolerance must be positive".into());
362    }
363    let data = build_mesh_data(positions, indices, options)?;
364    let seg = segment_prepared(&data, options);
365
366    // Documented fallback: a mesh with NO recognized carrier at all is the
367    // faceted importer's business (triangle-per-face, exact for the mesh).
368    if seg
369        .regions
370        .iter()
371        .all(|region| matches!(region.carrier, RegionCarrier::Freeform))
372    {
373        let weld = if options.weld_tolerance > 0.0 {
374            options.weld_tolerance
375        } else {
376            -1.0
377        };
378        let index_arg = (!indices.is_empty()).then_some(indices);
379        return mesh_to_faceted_brep(positions, index_arg, weld);
380    }
381    if let Some(region) = seg
382        .regions
383        .iter()
384        .find(|region| matches!(region.carrier, RegionCarrier::Freeform))
385    {
386        return Err(format!(
387            "mesh_regions_to_brep: region {} is freeform; mixing freeform and analytic \
388             regions in one shell is not supported (v1) — import via mesh_to_faceted_brep",
389            region.id
390        ));
391    }
392    if let Some(region) = seg.regions.iter().find(|region| {
393        matches!(
394            region.carrier,
395            RegionCarrier::Sphere { .. } | RegionCarrier::Torus { .. }
396        )
397    }) {
398        return Err(format!(
399            "mesh_regions_to_brep: region {} is a {} — sphere/torus faces need pole/seam \
400             topology not supported in v1",
401            region.id,
402            region.carrier.kind()
403        ));
404    }
405    if seg
406        .triangle_region_ids
407        .iter()
408        .any(|&id| id == UNASSIGNED_REGION)
409    {
410        return Err("mesh_regions_to_brep: mesh has unassignable degenerate triangles".into());
411    }
412
413    // Region-boundary graph over welded mesh edges.
414    let region_of = &seg.triangle_region_ids;
415    let mut boundary: HashMap<(usize, usize), (u32, u32)> = HashMap::default();
416    for (&key, incident) in &data.edges {
417        if incident.len() != 2 {
418            return Err(format!(
419                "mesh_regions_to_brep: mesh is not closed-manifold (edge with {} incident \
420                 triangles)",
421                incident.len()
422            ));
423        }
424        let (ra, rb) = (
425            region_of[incident[0] as usize],
426            region_of[incident[1] as usize],
427        );
428        if ra != rb {
429            boundary.insert(key, (ra.min(rb), ra.max(rb)));
430        }
431    }
432    if boundary.is_empty() {
433        return Err(
434            "mesh_regions_to_brep: single boundary-less region cannot form a face loop".into(),
435        );
436    }
437
438    let mut vertex_boundary: HashMap<usize, Vec<(usize, usize)>> = HashMap::default();
439    for &key in boundary.keys() {
440        vertex_boundary.entry(key.0).or_default().push(key);
441        vertex_boundary.entry(key.1).or_default().push(key);
442    }
443    for list in vertex_boundary.values_mut() {
444        list.sort_unstable();
445    }
446    let is_corner = |vertex: usize| -> bool {
447        let list = &vertex_boundary[&vertex];
448        list.len() != 2 || boundary[&list[0]] != boundary[&list[1]]
449    };
450
451    // Chains: walk from corners through valence-2 vertices; leftovers are
452    // closed rings.
453    let mut chains: Vec<Chain> = Vec::new();
454    let mut edge_chain: HashMap<(usize, usize), usize> = HashMap::default();
455    let other_end = |key: (usize, usize), vertex: usize| -> usize {
456        if key.0 == vertex {
457            key.1
458        } else {
459            key.0
460        }
461    };
462    let mut corner_vertices: Vec<usize> = vertex_boundary
463        .keys()
464        .copied()
465        .filter(|&v| is_corner(v))
466        .collect();
467    corner_vertices.sort_unstable();
468    for &corner in &corner_vertices {
469        let incident = vertex_boundary[&corner].clone();
470        for start_edge in incident {
471            if edge_chain.contains_key(&start_edge) {
472                continue;
473            }
474            let chain_id = chains.len();
475            let mut vertices = vec![corner];
476            let mut current_edge = start_edge;
477            let mut current = other_end(start_edge, corner);
478            edge_chain.insert(start_edge, chain_id);
479            while !is_corner(current) {
480                vertices.push(current);
481                let next_edge = vertex_boundary[&current]
482                    .iter()
483                    .copied()
484                    .find(|&e| e != current_edge)
485                    .expect("valence-2 vertex has a continuation");
486                edge_chain.insert(next_edge, chain_id);
487                current_edge = next_edge;
488                current = other_end(next_edge, current);
489            }
490            vertices.push(current);
491            chains.push(Chain {
492                vertices,
493                closed: false,
494            });
495        }
496    }
497    let mut remaining: Vec<(usize, usize)> = boundary
498        .keys()
499        .copied()
500        .filter(|key| !edge_chain.contains_key(key))
501        .collect();
502    remaining.sort_unstable();
503    for start_edge in remaining {
504        if edge_chain.contains_key(&start_edge) {
505            continue;
506        }
507        let chain_id = chains.len();
508        let start = start_edge.0;
509        let mut vertices = vec![start];
510        let mut current_edge = start_edge;
511        let mut current = start_edge.1;
512        edge_chain.insert(start_edge, chain_id);
513        while current != start {
514            vertices.push(current);
515            let next_edge = vertex_boundary[&current]
516                .iter()
517                .copied()
518                .find(|&e| e != current_edge)
519                .expect("ring vertex has a continuation");
520            edge_chain.insert(next_edge, chain_id);
521            current_edge = next_edge;
522            current = other_end(next_edge, current);
523        }
524        chains.push(Chain {
525            vertices,
526            closed: true,
527        });
528    }
529
530    // Ordered boundary cycles per region (directed walks; the triangle
531    // winding makes them counterclockwise seen from outside the solid).
532    let region_count = seg.regions.len();
533    let mut region_tris: Vec<Vec<u32>> = vec![Vec::new(); region_count];
534    for (t, &r) in region_of.iter().enumerate() {
535        region_tris[r as usize].push(t as u32);
536    }
537    let mut region_cycles: Vec<Vec<Vec<usize>>> = Vec::with_capacity(region_count);
538    for tris in &region_tris {
539        let mut out: HashMap<usize, usize> = HashMap::default();
540        for &t in tris {
541            let tri = &data.tris[t as usize];
542            for corner in 0..3 {
543                let a = tri.verts[corner];
544                let b = tri.verts[(corner + 1) % 3];
545                if a == b || !boundary.contains_key(&edge_key(a, b)) {
546                    continue;
547                }
548                if let Some(previous) = out.insert(a, b) {
549                    if previous != b {
550                        return Err(
551                            "mesh_regions_to_brep: region boundary pinches at a vertex (two \
552                             outgoing boundary edges)"
553                                .into(),
554                        );
555                    }
556                }
557            }
558        }
559        let mut starts: Vec<usize> = out.keys().copied().collect();
560        starts.sort_unstable();
561        let mut visited: rustc_hash::FxHashSet<usize> = rustc_hash::FxHashSet::default();
562        let mut cycles = Vec::new();
563        for &start in &starts {
564            if visited.contains(&start) {
565                continue;
566            }
567            let mut cycle = vec![start];
568            visited.insert(start);
569            let mut current = out[&start];
570            while current != start {
571                if !visited.insert(current) {
572                    return Err("mesh_regions_to_brep: region boundary walk self-crosses".into());
573                }
574                cycle.push(current);
575                current = *out
576                    .get(&current)
577                    .ok_or("mesh_regions_to_brep: region boundary walk breaks")?;
578            }
579            cycles.push(cycle);
580        }
581        region_cycles.push(cycles);
582    }
583
584    let mut builder = RegionBrepBuilder {
585        data: &data,
586        tol: (options.fit_tolerance * data.diag).max(1e-12),
587        chains,
588        edge_chain,
589        chain_edges: Vec::new(),
590        vertices: Vec::new(),
591        vertex_ids: HashMap::default(),
592        edges: Vec::new(),
593        edge_index: HashMap::default(),
594        next_id: 1,
595    };
596    builder.chain_edges = (0..builder.chains.len()).map(|_| None).collect();
597
598    // Curved regions first: walls and patches create the exact circle / arc /
599    // ruling edges their planar neighbors then share.
600    let mut faces: Vec<FaceRecord> = Vec::new();
601    for (index, region) in seg.regions.iter().enumerate() {
602        match &region.carrier {
603            RegionCarrier::Cylinder { .. } | RegionCarrier::Cone { .. } => {
604                let face = build_revolved_region_face(&mut builder, region, &region_cycles[index])?;
605                faces.push(face);
606            }
607            _ => {}
608        }
609    }
610    for (index, region) in seg.regions.iter().enumerate() {
611        if let RegionCarrier::Plane { origin, normal } = region.carrier {
612            let face = build_planar_region_face(
613                &mut builder,
614                region.id,
615                origin,
616                normal,
617                &region_cycles[index],
618            )?;
619            faces.push(face);
620        }
621    }
622
623    let shell_id = builder.allocate_id();
624    let solid_id = builder.allocate_id();
625    let mut solid = BrepSolid {
626        id: solid_id,
627        vertices: builder.vertices,
628        edges: builder.edges,
629        shells: vec![ShellRecord {
630            id: shell_id,
631            faces,
632        }],
633        genus: 0,
634    };
635    let vertex_count = solid.vertices.len() as i64;
636    let edge_count = solid.edges.len() as i64;
637    let face_count = solid.shells[0].faces.len() as i64;
638    let hole_count: i64 = solid.shells[0]
639        .faces
640        .iter()
641        .map(|face| face.loops.len().saturating_sub(1) as i64)
642        .sum();
643    let euler = vertex_count - edge_count + face_count - hole_count;
644    let numerator = 2 - euler;
645    if numerator < 0 || numerator % 2 != 0 {
646        return Err(format!(
647            "mesh_regions_to_brep: rebuilt topology has non-manifold Euler count V-E+F-H={euler}"
648        ));
649    }
650    solid.genus = numerator / 2;
651    let issues = solid.validate();
652    if !issues.is_empty() {
653        return Err(format!(
654            "mesh_regions_to_brep: rebuilt solid failed validation: {issues:?}"
655        ));
656    }
657    let volume = solid_signed_volume(&solid)?;
658    if !(volume > 0.0) {
659        return Err(format!(
660            "mesh_regions_to_brep: rebuilt solid is inverted (signed volume {volume})"
661        ));
662    }
663    Ok(solid)
664}