Skip to main content

brep_kernel/brep/
analytic_topology.rs

1use crate::curve::{curve_to_plane_parameters, parameter_line};
2use crate::topology::{
3    BrepSolid, CoedgeRecord, EdgeRecord, FaceRecord, LoopRecord, ShellRecord, VertexRecord,
4};
5use crate::{
6    make_cone_surface, make_line, make_plane, make_sphere_surface_framed, make_torus_surface,
7    Vec3,
8};
9
10// A certificate owns the just-built body; it is consumed at this module's
11// acceptance boundary. It is never attached to mutable BrepSolid data or
12// serialized, and no caller can certify an imported or subsequently edited body.
13// Primitive certificate type: begin
14struct CertifiedPrimitive { solid: BrepSolid }
15// Primitive certificate type: end
16impl CertifiedPrimitive {
17    fn accept(self) -> BrepSolid {
18        primitive_check_count(true);
19        self.solid
20    }
21}
22
23pub(crate) fn primitive_certificate_enabled() -> bool {
24    // Promoted after the 98/144-case byte census. Explicit =0 restores the
25    // full constructor validator; eligibility never changes geometry or errors.
26    switch!(live "BREP_CERTIFIED_PRIMITIVES", true)
27}
28
29// This is an eligibility bound, never a new refusal: every input outside it
30// takes the original validator. Keep floating point conditioning far from the
31// model/edge coincidence bands: dimensions 1e-3..1e3, origin at most 1000
32// times the smallest dimension, and axes of order unity. Surface/curve builders
33// still check finite homogeneous control points, positive weights and knots.
34// The trusted builders supply incidence and exact shared boundary construction:
35// box affine faces; cylinder two iso-curves and their plane images; sphere
36// one periodic chart with two pole edges; ring torus two periodic seams.
37// Neither a certificate nor this eligibility predicate accepts arbitrary bodies.
38pub(crate) fn primitive_domain(origin: Vec3, dimensions: &[f64], axis: Option<Vec3>) -> bool {
39    if !dimensions.iter().all(|v| v.is_finite() && (1e-3..=1e3).contains(v)) { return false; }
40    let minimum = dimensions.iter().copied().fold(f64::INFINITY, f64::min);
41    if ![origin.x, origin.y, origin.z].iter().all(|v| v.is_finite() && v.abs() <= minimum * 1000.) { return false; }
42    axis.is_none_or(|a| [a.x, a.y, a.z].iter().all(|v| v.is_finite())
43        && (0.25..=4.).contains(&a.length_squared()))
44}
45
46pub(crate) fn primitive_check_count(certified: bool) {
47    let _ = certified;
48}
49
50
51fn validated(solid: BrepSolid, label: &'static str) -> Result<BrepSolid, String> {
52    let mut profile = crate::loft_topology::ConstructionProfile::new(label);
53    primitive_check_count(false);
54    let issues = solid.validate();
55    profile.lap("validation");
56    if issues.is_empty() {
57        Ok(solid)
58    } else {
59        Err(format!(
60            "Rust {label} builder produced invalid topology: {issues:?}"
61        ))
62    }
63}
64
65pub fn make_sphere_brep(center: Vec3, radius: f64, polar_axis: Vec3) -> Result<BrepSolid, String> {
66    make_sphere_brep_framed(center, radius, polar_axis, None)
67}
68
69/// A sphere solid whose SEAM direction is chosen as well as its polar axis — see
70/// [`make_sphere_surface_framed`]. The topology is identical either way (one
71/// face, one pole-to-pole seam edge, two degenerate pole edges); only WHERE on
72/// the ball the seam and the poles sit changes. A caller that will trim the ball
73/// uses it to hide both inside the removed region, so the surviving face has no
74/// edge bordering itself.
75pub fn make_sphere_brep_framed(
76    center: Vec3,
77    radius: f64,
78    polar_axis: Vec3,
79    seam_direction: Option<Vec3>,
80) -> Result<BrepSolid, String> {
81    let mut profile = crate::loft_topology::ConstructionProfile::new("sphere");
82    let surface = make_sphere_surface_framed(center, radius, polar_axis, seam_direction)?;
83    let south = surface.evaluate(0.0, 0.0)?;
84    let north = surface.evaluate(0.0, 1.0)?;
85    let seam = surface.iso_curve_u(0.0)?;
86    let solid = BrepSolid {
87            mass_properties_cache: Default::default(),
88            cells: Default::default(),
89            id: 120,
90            vertices: vec![
91                VertexRecord {
92                    id: 1,
93                    point: south,
94                },
95                VertexRecord {
96                    id: 2,
97                    point: north,
98                },
99            ],
100            edges: vec![
101                EdgeRecord {
102                    id: 10,
103                    curve: seam,
104                    t0: 0.0,
105                    t1: 1.0,
106                    start_vertex_id: 1,
107                    end_vertex_id: 2,
108                    degenerate: false,
109                    name: None,
110                },
111                EdgeRecord {
112                    id: 11,
113                    curve: make_line(south, south)?,
114                    t0: 0.0,
115                    t1: 1.0,
116                    start_vertex_id: 1,
117                    end_vertex_id: 1,
118                    degenerate: true,
119                    name: None,
120                },
121                EdgeRecord {
122                    id: 12,
123                    curve: make_line(north, north)?,
124                    t0: 0.0,
125                    t1: 1.0,
126                    start_vertex_id: 2,
127                    end_vertex_id: 2,
128                    degenerate: true,
129                    name: None,
130                },
131            ],
132            shells: vec![ShellRecord {
133                id: 119,
134                faces: vec![FaceRecord {
135                    id: 118,
136                    surface,
137                    same_sense: true,
138                    loops: vec![LoopRecord {
139                        id: 117,
140                        coedges: vec![
141                            CoedgeRecord {
142                                id: 101,
143                                edge_id: 11,
144                                forward: true,
145                                pcurve: parameter_line(0.0, 0.0, 1.0, 0.0)?,
146                            },
147                            CoedgeRecord {
148                                id: 102,
149                                edge_id: 10,
150                                forward: true,
151                                pcurve: parameter_line(1.0, 0.0, 1.0, 1.0)?,
152                            },
153                            CoedgeRecord {
154                                id: 103,
155                                edge_id: 12,
156                                forward: true,
157                                pcurve: parameter_line(1.0, 1.0, 0.0, 1.0)?,
158                            },
159                            CoedgeRecord {
160                                id: 104,
161                                edge_id: 10,
162                                forward: false,
163                                pcurve: parameter_line(0.0, 1.0, 0.0, 0.0)?,
164                            },
165                        ],
166                    }],
167                    name: None,
168                }],
169            }],
170            genus: 0,
171            kind: crate::BodyKind::Solid,
172        };
173    profile.lap("construct");
174    // The pole/seam topology above is fixed. A caller-selected seam keeps the
175    // full validator until that separate construction is certified as well.
176    if primitive_certificate_enabled() && seam_direction.is_none()
177        && primitive_domain(center, &[radius], Some(polar_axis)) {
178        return Ok(CertifiedPrimitive { solid }.accept());
179    }
180    validated(solid, "sphere")
181}
182
183pub fn make_torus_brep(
184    center: Vec3,
185    axis_direction: Vec3,
186    major_radius: f64,
187    minor_radius: f64,
188) -> Result<BrepSolid, String> {
189    let mut profile = crate::loft_topology::ConstructionProfile::new("torus");
190    let surface = make_torus_surface(center, axis_direction, major_radius, minor_radius)?;
191    let corner = surface.evaluate(0.0, 0.0)?;
192    let major_curve = surface.iso_curve_v(0.0)?;
193    let tube_curve = surface.iso_curve_u(0.0)?;
194    let solid = BrepSolid {
195            mass_properties_cache: Default::default(),
196            cells: Default::default(),
197            id: 130,
198            vertices: vec![VertexRecord {
199                id: 1,
200                point: corner,
201            }],
202            edges: vec![
203                EdgeRecord {
204                    id: 10,
205                    curve: major_curve,
206                    t0: 0.0,
207                    t1: 1.0,
208                    start_vertex_id: 1,
209                    end_vertex_id: 1,
210                    degenerate: false,
211                    name: None,
212                },
213                EdgeRecord {
214                    id: 11,
215                    curve: tube_curve,
216                    t0: 0.0,
217                    t1: 1.0,
218                    start_vertex_id: 1,
219                    end_vertex_id: 1,
220                    degenerate: false,
221                    name: None,
222                },
223            ],
224            shells: vec![ShellRecord {
225                id: 129,
226                faces: vec![FaceRecord {
227                    id: 128,
228                    surface,
229                    same_sense: true,
230                    loops: vec![LoopRecord {
231                        id: 127,
232                        coedges: vec![
233                            CoedgeRecord {
234                                id: 101,
235                                edge_id: 10,
236                                forward: true,
237                                pcurve: parameter_line(0.0, 0.0, 1.0, 0.0)?,
238                            },
239                            CoedgeRecord {
240                                id: 102,
241                                edge_id: 11,
242                                forward: true,
243                                pcurve: parameter_line(1.0, 0.0, 1.0, 1.0)?,
244                            },
245                            CoedgeRecord {
246                                id: 103,
247                                edge_id: 10,
248                                forward: false,
249                                pcurve: parameter_line(1.0, 1.0, 0.0, 1.0)?,
250                            },
251                            CoedgeRecord {
252                                id: 104,
253                                edge_id: 11,
254                                forward: false,
255                                pcurve: parameter_line(0.0, 1.0, 0.0, 0.0)?,
256                            },
257                        ],
258                    }],
259                    name: None,
260                }],
261            }],
262            genus: 1,
263            kind: crate::BodyKind::Solid,
264        };
265    profile.lap("construct");
266    // Positive clearance makes the periodic tube a ring torus, not a horn or
267    // spindle. Near-horn numerics keep the original validation path.
268    if primitive_certificate_enabled()
269        && major_radius - minor_radius >= minor_radius * 1e-3
270        && primitive_domain(center, &[major_radius, minor_radius], Some(axis_direction)) {
271        return Ok(CertifiedPrimitive { solid }.accept());
272    }
273    validated(solid, "torus")
274}
275
276pub fn make_cone_brep(
277    base: Vec3,
278    axis_direction: Vec3,
279    radius_bottom: f64,
280    radius_top: f64,
281    height: f64,
282) -> Result<BrepSolid, String> {
283    if radius_bottom <= 0.0 || radius_top < 0.0 || height <= 0.0 {
284        return Err("makeConeSolid: invalid radius or height".into());
285    }
286    let axis = axis_direction.normalized()?;
287    let side = make_cone_surface(base, axis, radius_bottom, radius_top, height)?;
288    let bottom_point = side.evaluate(0.0, 0.0)?;
289    let bottom_circle = side.iso_curve_v(0.0)?;
290    let seam = side.iso_curve_u(0.0)?;
291    let x_axis = axis.perpendicular()?;
292    let y_axis = axis.cross(x_axis).normalized()?;
293    let bottom_origin = base
294        .add(x_axis.scale(-radius_bottom))
295        .add(y_axis.scale(-radius_bottom));
296    let bottom_plane = make_plane(
297        bottom_origin,
298        x_axis,
299        y_axis,
300        2.0 * radius_bottom,
301        2.0 * radius_bottom,
302    )?;
303    let bottom_pcurve =
304        curve_to_plane_parameters(&bottom_circle, bottom_origin, x_axis, y_axis)?.reversed()?;
305    let bottom_face = FaceRecord {
306        id: 108,
307        surface: bottom_plane,
308        same_sense: false,
309        loops: vec![LoopRecord {
310            id: 107,
311            coedges: vec![CoedgeRecord {
312                id: 106,
313                edge_id: 10,
314                forward: false,
315                pcurve: bottom_pcurve,
316            }],
317        }],
318        name: None,
319    };
320
321    if radius_top <= 1e-7 {
322        let apex = side.evaluate(0.0, 1.0)?;
323        return validated(
324            BrepSolid {
325                mass_properties_cache: Default::default(),
326                cells: Default::default(),
327                id: 140,
328                vertices: vec![
329                    VertexRecord {
330                        id: 1,
331                        point: bottom_point,
332                    },
333                    VertexRecord { id: 2, point: apex },
334                ],
335                edges: vec![
336                    EdgeRecord {
337                        id: 10,
338                        curve: bottom_circle,
339                        t0: 0.0,
340                        t1: 1.0,
341                        start_vertex_id: 1,
342                        end_vertex_id: 1,
343                        degenerate: false,
344                        name: None,
345                    },
346                    EdgeRecord {
347                        id: 11,
348                        curve: seam,
349                        t0: 0.0,
350                        t1: 1.0,
351                        start_vertex_id: 1,
352                        end_vertex_id: 2,
353                        degenerate: false,
354                        name: None,
355                    },
356                    EdgeRecord {
357                        id: 12,
358                        curve: make_line(apex, apex)?,
359                        t0: 0.0,
360                        t1: 1.0,
361                        start_vertex_id: 2,
362                        end_vertex_id: 2,
363                        degenerate: true,
364                        name: None,
365                    },
366                ],
367                shells: vec![ShellRecord {
368                    id: 139,
369                    faces: vec![
370                        FaceRecord {
371                            id: 105,
372                            surface: side,
373                            same_sense: true,
374                            loops: vec![LoopRecord {
375                                id: 100,
376                                coedges: vec![
377                                    CoedgeRecord {
378                                        id: 101,
379                                        edge_id: 10,
380                                        forward: true,
381                                        pcurve: parameter_line(0.0, 0.0, 1.0, 0.0)?,
382                                    },
383                                    CoedgeRecord {
384                                        id: 102,
385                                        edge_id: 11,
386                                        forward: true,
387                                        pcurve: parameter_line(1.0, 0.0, 1.0, 1.0)?,
388                                    },
389                                    CoedgeRecord {
390                                        id: 103,
391                                        edge_id: 12,
392                                        forward: true,
393                                        pcurve: parameter_line(1.0, 1.0, 0.0, 1.0)?,
394                                    },
395                                    CoedgeRecord {
396                                        id: 104,
397                                        edge_id: 11,
398                                        forward: false,
399                                        pcurve: parameter_line(0.0, 1.0, 0.0, 0.0)?,
400                                    },
401                                ],
402                            }],
403                            name: None,
404                        },
405                        bottom_face,
406                    ],
407                }],
408                genus: 0,
409                kind: crate::BodyKind::Solid,
410            },
411            "cone",
412        );
413    }
414
415    let top_point = side.evaluate(0.0, 1.0)?;
416    let top_circle = side.iso_curve_v(1.0)?;
417    let top_origin = base
418        .add(axis.scale(height))
419        .add(x_axis.scale(-radius_top))
420        .add(y_axis.scale(-radius_top));
421    let top_plane = make_plane(
422        top_origin,
423        x_axis,
424        y_axis,
425        2.0 * radius_top,
426        2.0 * radius_top,
427    )?;
428    let top_pcurve = curve_to_plane_parameters(&top_circle, top_origin, x_axis, y_axis)?;
429    validated(
430        BrepSolid {
431            mass_properties_cache: Default::default(),
432            cells: Default::default(),
433            id: 150,
434            vertices: vec![
435                VertexRecord {
436                    id: 1,
437                    point: bottom_point,
438                },
439                VertexRecord {
440                    id: 2,
441                    point: top_point,
442                },
443            ],
444            edges: vec![
445                EdgeRecord {
446                    id: 10,
447                    curve: bottom_circle,
448                    t0: 0.0,
449                    t1: 1.0,
450                    start_vertex_id: 1,
451                    end_vertex_id: 1,
452                    degenerate: false,
453                    name: None,
454                },
455                EdgeRecord {
456                    id: 11,
457                    curve: top_circle,
458                    t0: 0.0,
459                    t1: 1.0,
460                    start_vertex_id: 2,
461                    end_vertex_id: 2,
462                    degenerate: false,
463                    name: None,
464                },
465                EdgeRecord {
466                    id: 12,
467                    curve: seam,
468                    t0: 0.0,
469                    t1: 1.0,
470                    start_vertex_id: 1,
471                    end_vertex_id: 2,
472                    degenerate: false,
473                    name: None,
474                },
475            ],
476            shells: vec![ShellRecord {
477                id: 149,
478                faces: vec![
479                    FaceRecord {
480                        id: 105,
481                        surface: side,
482                        same_sense: true,
483                        loops: vec![LoopRecord {
484                            id: 100,
485                            coedges: vec![
486                                CoedgeRecord {
487                                    id: 101,
488                                    edge_id: 10,
489                                    forward: true,
490                                    pcurve: parameter_line(0.0, 0.0, 1.0, 0.0)?,
491                                },
492                                CoedgeRecord {
493                                    id: 102,
494                                    edge_id: 12,
495                                    forward: true,
496                                    pcurve: parameter_line(1.0, 0.0, 1.0, 1.0)?,
497                                },
498                                CoedgeRecord {
499                                    id: 103,
500                                    edge_id: 11,
501                                    forward: false,
502                                    pcurve: parameter_line(1.0, 1.0, 0.0, 1.0)?,
503                                },
504                                CoedgeRecord {
505                                    id: 104,
506                                    edge_id: 12,
507                                    forward: false,
508                                    pcurve: parameter_line(0.0, 1.0, 0.0, 0.0)?,
509                                },
510                            ],
511                        }],
512                        name: None,
513                    },
514                    bottom_face,
515                    FaceRecord {
516                        id: 111,
517                        surface: top_plane,
518                        same_sense: true,
519                        loops: vec![LoopRecord {
520                            id: 110,
521                            coedges: vec![CoedgeRecord {
522                                id: 109,
523                                edge_id: 11,
524                                forward: true,
525                                pcurve: top_pcurve,
526                            }],
527                        }],
528                        name: None,
529                    },
530                ],
531            }],
532            genus: 0,
533            kind: crate::BodyKind::Solid,
534        },
535        "cone frustum",
536    )
537}
538
539