Skip to main content

geop_ops_parts/
examples.rs

1//! Example programs, written in Rust against the operation types.
2//!
3//! Each refers to what earlier steps built only by name, and so reads as
4//! the recipe it is: `extrude(box,end)` is the end cap of the step `box`,
5//! whatever internal id it happens to get.
6
7use crate::{
8    AddDatumArgs, AddSketchArgs, Combine, Construction, EntityRef, ExtrudeArgs, Program,
9    RevolveArgs, WorldAxis,
10};
11use geop_core_sketch::{Constraint, CurveId, PointId, Sketch};
12
13/// A closed polygon through `corners`, one line per side: its points and
14/// lines.
15fn polygon(sketch: &mut Sketch, corners: &[[f64; 2]]) -> (Vec<PointId>, Vec<CurveId>) {
16    let points: Vec<PointId> = corners
17        .iter()
18        .map(|c| sketch.add_point(c[0], c[1]))
19        .collect();
20    let lines = (0..points.len())
21        .map(|i| sketch.add_line(points[i], points[(i + 1) % points.len()]))
22        .collect();
23    (points, lines)
24}
25
26/// Solves `sketch`, which the examples all constrain fully.
27fn solved(mut sketch: Sketch) -> Sketch {
28    let report = sketch.solve().expect("example sketches are valid");
29    assert!(
30        report.converged,
31        "example sketch does not solve: {report:?}"
32    );
33    sketch
34}
35
36/// A `width` x `depth` rectangle with its first corner at `origin`, drawn
37/// roughly and fully constrained.
38fn rectangle(sketch: &mut Sketch, origin: [f64; 2], width: f64, depth: f64) -> Vec<CurveId> {
39    let [x, y] = origin;
40    // Deliberately a little off: the constraints decide the shape.
41    let (p, l) = polygon(
42        sketch,
43        &[
44            [x + 0.05, y - 0.02],
45            [x + width, y + 0.03],
46            [x + width - 0.04, y + depth],
47            [x, y + depth + 0.01],
48        ],
49    );
50    sketch.constrain(Constraint::Fix { point: p[0], x, y });
51    sketch.constrain(Constraint::Horizontal { line: l[0] });
52    sketch.constrain(Constraint::Horizontal { line: l[2] });
53    sketch.constrain(Constraint::Vertical { line: l[1] });
54    sketch.constrain(Constraint::Vertical { line: l[3] });
55    sketch.constrain(Constraint::Length {
56        curve: l[0],
57        value: width,
58    });
59    sketch.constrain(Constraint::Length {
60        curve: l[1],
61        value: depth,
62    });
63    l
64}
65
66/// A circle of `radius` around `center`, fully constrained.
67fn circle(sketch: &mut Sketch, center: [f64; 2], radius: f64) -> CurveId {
68    let c = sketch.add_point(center[0], center[1]);
69    let circle = sketch.add_circle(c, radius * 1.1);
70    sketch.constrain(Constraint::Fix {
71        point: c,
72        x: center[0],
73        y: center[1],
74    });
75    sketch.constrain(Constraint::Radius {
76        curve: circle,
77        value: radius,
78    });
79    circle
80}
81
82/// A 2 x 2 x 1 box with a blind hole drilled into its top: a rectangle
83/// sketched on the Z plane and extruded up (`box`), and a circle sketched on
84/// the box's end cap `extrude(box,end)` and extruded back into it, cutting
85/// it out of the box (`hole`) — the drilled box is `extrude(hole)`.
86pub fn box_with_drill_hole() -> Program {
87    let mut program = Program::new();
88
89    let mut outline = Sketch::new();
90    rectangle(&mut outline, [0.0, 0.0], 2.0, 2.0);
91    program.push(
92        "outline",
93        AddSketchArgs {
94            plane: EntityRef::Plane {
95                normal: WorldAxis::Z,
96            },
97            sketch: solved(outline),
98        },
99    );
100    program.push(
101        "box",
102        ExtrudeArgs {
103            sketch: "outline".into(),
104            distance: 1.0,
105            symmetric: false,
106            combine: Combine::NewBody,
107        },
108    );
109
110    let mut hole = Sketch::new();
111    circle(&mut hole, [1.0, 1.0], 0.4);
112    program.push(
113        "hole_sketch",
114        AddSketchArgs {
115            plane: EntityRef::Face {
116                name: "extrude(box,end)".into(),
117            },
118            sketch: solved(hole),
119        },
120    );
121    program.push(
122        "hole",
123        ExtrudeArgs {
124            sketch: "hole_sketch".into(),
125            distance: -0.5,
126            symmetric: false,
127            combine: Combine::Difference {
128                target: "extrude(box)".into(),
129            },
130        },
131    );
132    program
133}
134
135/// A stepped shaft revolved around the world z-axis, cross-drilled through
136/// its thinner end: a half section sketched on the X plane, closed by the
137/// axis itself (`shaft`), and a circle on the Y plane extruded through both
138/// sides of it and cut out of it (`bore`) — the drilled shaft is
139/// `extrude(bore)`.
140pub fn cross_drilled_shaft() -> Program {
141    let mut program = Program::new();
142
143    // Sketch x runs along world y, sketch y along world z.
144    let mut section = Sketch::new();
145    let (p, l) = polygon(
146        &mut section,
147        &[
148            [0.0, 0.0],
149            [1.0, 0.0],
150            [1.0, 1.0],
151            [0.6, 1.0],
152            [0.6, 3.0],
153            [0.0, 3.0],
154        ],
155    );
156    let axis = l[5];
157    section.constrain(Constraint::Fix {
158        point: p[0],
159        x: 0.0,
160        y: 0.0,
161    });
162    section.constrain(Constraint::Vertical { line: axis });
163    for (line, horizontal) in [
164        (l[0], true),
165        (l[1], false),
166        (l[2], true),
167        (l[3], false),
168        (l[4], true),
169    ] {
170        section.constrain(if horizontal {
171            Constraint::Horizontal { line }
172        } else {
173            Constraint::Vertical { line }
174        });
175    }
176    section.constrain(Constraint::Length {
177        curve: l[0],
178        value: 1.0,
179    });
180    section.constrain(Constraint::Length {
181        curve: l[1],
182        value: 1.0,
183    });
184    section.constrain(Constraint::Length {
185        curve: l[3],
186        value: 2.0,
187    });
188    section.constrain(Constraint::Length {
189        curve: l[4],
190        value: 0.6,
191    });
192    program.push(
193        "section",
194        AddSketchArgs {
195            plane: EntityRef::Plane {
196                normal: WorldAxis::X,
197            },
198            sketch: solved(section),
199        },
200    );
201    program.push(
202        "shaft",
203        RevolveArgs {
204            sketch: "section".into(),
205            axis,
206            combine: Combine::NewBody,
207        },
208    );
209
210    // Sketch x runs along world x, sketch y along world -z: a bore across
211    // the thin end, at z = 2.2.
212    let mut bore = Sketch::new();
213    circle(&mut bore, [0.0, -2.2], 0.25);
214    program.push(
215        "bore_sketch",
216        AddSketchArgs {
217            plane: EntityRef::Plane {
218                normal: WorldAxis::Y,
219            },
220            sketch: solved(bore),
221        },
222    );
223    program.push(
224        "bore",
225        ExtrudeArgs {
226            sketch: "bore_sketch".into(),
227            distance: 3.0,
228            symmetric: true,
229            combine: Combine::Difference {
230                target: "revolve(shaft)".into(),
231            },
232        },
233    );
234    program
235}
236
237/// Two separate plates from one sketch of two regions, one with a round
238/// hole, extruded symmetrically into a single solid of two shells.
239pub fn two_plates() -> Program {
240    let mut program = Program::new();
241    let mut plates = Sketch::new();
242    rectangle(&mut plates, [0.0, 0.0], 1.5, 1.0);
243    rectangle(&mut plates, [2.0, 0.0], 1.0, 1.0);
244    circle(&mut plates, [0.75, 0.5], 0.3);
245    program.push(
246        "plates_sketch",
247        AddSketchArgs {
248            plane: EntityRef::Plane {
249                normal: WorldAxis::Z,
250            },
251            sketch: solved(plates),
252        },
253    );
254    program.push(
255        "plates",
256        ExtrudeArgs {
257            sketch: "plates_sketch".into(),
258            distance: 0.25,
259            symmetric: true,
260            combine: Combine::NewBody,
261        },
262    );
263    program
264}
265
266/// A box with a round boss standing on it, sketched on a reference plane:
267/// the plane half a unit above the box's top (`lifted`, offset from
268/// `extrude(box,end)`), a circle sketched on it, and extruded back down
269/// through the gap and into the box, joined to it (`boss`) — the part is
270/// `extrude(boss)`.
271pub fn boss_on_reference_plane() -> Program {
272    let mut program = Program::new();
273    let mut outline = Sketch::new();
274    rectangle(&mut outline, [0.0, 0.0], 2.0, 2.0);
275    program.push(
276        "outline",
277        AddSketchArgs {
278            plane: EntityRef::Plane {
279                normal: WorldAxis::Z,
280            },
281            sketch: solved(outline),
282        },
283    );
284    program.push(
285        "box",
286        ExtrudeArgs {
287            sketch: "outline".into(),
288            distance: 1.0,
289            symmetric: false,
290            combine: Combine::NewBody,
291        },
292    );
293    program.push(
294        "lifted",
295        AddDatumArgs {
296            selection: vec![EntityRef::Face {
297                name: "extrude(box,end)".into(),
298            }],
299            construction: Construction::Offset { distance: 0.5 },
300        },
301    );
302    let mut boss = Sketch::new();
303    circle(&mut boss, [1.0, 1.0], 0.5);
304    program.push(
305        "boss_sketch",
306        AddSketchArgs {
307            plane: EntityRef::Datum {
308                name: "lifted".into(),
309            },
310            sketch: solved(boss),
311        },
312    );
313    program.push(
314        "boss",
315        ExtrudeArgs {
316            sketch: "boss_sketch".into(),
317            distance: -0.75,
318            symmetric: false,
319            combine: Combine::Union {
320                target: "extrude(box)".into(),
321            },
322        },
323    );
324    program
325}
326
327/// A hand-drawn handle-shaped outline (an irregular blob, splined, with a
328/// smaller splined region inside it — as a free-hand sketch in the web
329/// editor draws, unconstrained) extruded, then cross-drilled with a
330/// circular hole near one end.
331///
332/// Reproduces two real user bug reports (2026-09-27) from the same
333/// outline, differing only in the hole's exact center: one had the
334/// `extrude`'s boolean difference fail outright with a degenerate-face
335/// error from `face_interior_point`; the other had it succeed but leave a
336/// non-watertight hole. Both point at the same root cause — the outline's
337/// hand-drawn spline passes close enough to itself, and to the hole, that
338/// the boolean's numerics lose a face — so this example uses the exact
339/// reported coordinates, unconstrained, rather than a re-derived "clean"
340/// equivalent that might not carry the same numerical case at all.
341pub fn handle_with_hole() -> Program {
342    let mut program = Program::new();
343
344    let mut outline = Sketch::new();
345    let outer_points = [
346        (0.01007682018456503, 0.8939399106232252),
347        (-0.9693901017551558, 0.8213868052943569),
348        (-1.00566665441959, 0.3175457960661055),
349        (-0.9452057333121998, -0.3555857922628385),
350        (0.09472210973491128, -0.9481028191152622),
351        (0.695300592734987, -0.3878316168534466),
352        (1.384555093359235, -0.4523232660346628),
353        (1.6233271012047248, -0.9224957048864859),
354        (1.9214381194506664, 0.15401630544608172),
355        (1.2092840203075834, 0.8578895429712221),
356    ]
357    .map(|(x, y)| outline.add_point(x, y));
358    let mut outer_loop = outer_points.to_vec();
359    outer_loop.push(outer_points[0]);
360    outline.add_spline(outer_loop);
361
362    let inner_points = [
363        (0.27854547786607586, 0.568500810276078),
364        (-0.3241022513094386, 0.561767316095346),
365        (-0.3645032163938306, 0.00962079327532167),
366        (0.03950643445008967, -0.4246895813818926),
367        (0.507484280010964, -0.11831559615858644),
368        (0.8643594715897602, 0.12072344725739975),
369        (1.1202322504575766, -0.1452495728815144),
370        (1.2919363520662426, 0.18805838906471978),
371        (1.0427970673791584, 0.470865144655464),
372    ]
373    .map(|(x, y)| outline.add_point(x, y));
374    let mut inner_loop = inner_points.to_vec();
375    inner_loop.push(inner_points[0]);
376    outline.add_spline(inner_loop);
377
378    program.push(
379        "outline",
380        AddSketchArgs {
381            plane: EntityRef::Plane {
382                normal: WorldAxis::Z,
383            },
384            sketch: solved(outline),
385        },
386    );
387    program.push(
388        "handle",
389        ExtrudeArgs {
390            sketch: "outline".into(),
391            distance: 1.0,
392            symmetric: false,
393            combine: Combine::NewBody,
394        },
395    );
396
397    let mut hole = Sketch::new();
398    let center = hole.add_point(0.32989396295411244, -0.5632891678453777);
399    hole.add_circle(center, 0.31727744879927977);
400    program.push(
401        "hole_sketch",
402        AddSketchArgs {
403            plane: EntityRef::Plane {
404                normal: WorldAxis::Y,
405            },
406            sketch: solved(hole),
407        },
408    );
409    program.push(
410        "hole",
411        ExtrudeArgs {
412            sketch: "hole_sketch".into(),
413            distance: 2.44,
414            symmetric: true,
415            combine: Combine::Difference {
416                target: "extrude(handle)".into(),
417            },
418        },
419    );
420    program
421}
422
423/// Every example, by name.
424pub fn all() -> Vec<(&'static str, Program)> {
425    vec![
426        ("box_with_drill_hole", box_with_drill_hole()),
427        ("cross_drilled_shaft", cross_drilled_shaft()),
428        ("two_plates", two_plates()),
429        ("boss_on_reference_plane", boss_on_reference_plane()),
430    ]
431}
432
433#[cfg(test)]
434mod tests {
435    use std::collections::BTreeMap;
436
437    use geop_core_math::{scalars::ScalInF64 as S, scalars::Scalar, vector::Vector3};
438    use geop_core_part::{Part, PartDescription, RefId};
439    use geop_core_topology::{
440        contains::shell::{PointClassification, shell_contains},
441        validation::{ValidationParameters, validate},
442    };
443
444    use super::*;
445
446    fn outputs_dir() -> std::path::PathBuf {
447        let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../outputs/parts");
448        std::fs::create_dir_all(&dir).unwrap();
449        dir
450    }
451
452    /// Every named entity's exact geometry, by name: vertex points, edge
453    /// curves and face surfaces as their full `Debug` enclosures — equal only
454    /// if the two parts agree to the last bit of every interval.
455    fn geometry_by_name(part: &Part<S>) -> BTreeMap<String, String> {
456        let model = part.topology();
457        part.names()
458            .iter()
459            .filter_map(|(id, name)| {
460                let geometry = match id {
461                    RefId::Vertex(v) => format!("{:?}", model.get_vertex(v).ok()?.point),
462                    RefId::Edge(e) => format!("{:?}", model.get_edge(e).ok()?.curve),
463                    RefId::Face(f) => format!("{:?}", model.get_face(f).ok()?.surface),
464                    RefId::Solid(_) | RefId::Sketch(_) | RefId::Datum(_) => return None,
465                };
466                Some((name.to_string(), geometry))
467            })
468            .collect()
469    }
470
471    /// Builds `program`, writes it and a description of the part it builds
472    /// to `outputs/parts/`, reads the program back from its JSON, and
473    /// requires the read-back program to be the same program and to build
474    /// the very same part: every name, every piece of topology between
475    /// names, and every bit of geometry.
476    fn build_and_round_trip(name: &str, program: &Program) -> Part<S> {
477        let part = program.apply(Part::<S>::new()).unwrap();
478        let validation = ValidationParameters::default();
479        if let Err(errors) = validate(&validation, part.topology()) {
480            panic!(
481                "{name}: {} validation error(s): {}",
482                errors.len(),
483                errors[0]
484            );
485        }
486
487        let json = program.to_json().unwrap();
488        let dir = outputs_dir();
489        std::fs::write(dir.join(format!("{name}.program.json")), &json).unwrap();
490        let description = PartDescription::of(&part).unwrap();
491        std::fs::write(
492            dir.join(format!("{name}.part.json")),
493            serde_json::to_string_pretty(&description).unwrap(),
494        )
495        .unwrap();
496        geop_ops_rasterize::rasterize_model(part.topology(), 16)
497            .unwrap()
498            .save_to_file(dir.join(format!("{name}.html")).to_str().unwrap())
499            .unwrap();
500
501        let read_back = Program::from_json(&json).unwrap();
502        assert_eq!(
503            &read_back, program,
504            "{name}: JSON round trip changed the program"
505        );
506        assert_eq!(
507            read_back.to_json().unwrap(),
508            json,
509            "{name}: JSON is not stable"
510        );
511
512        let rebuilt = read_back.apply(Part::<S>::new()).unwrap();
513        assert_eq!(
514            PartDescription::of(&rebuilt).unwrap(),
515            description,
516            "{name}: the read-back program built a different part"
517        );
518        assert_eq!(
519            geometry_by_name(&rebuilt),
520            geometry_by_name(&part),
521            "{name}: the read-back program built different geometry"
522        );
523        part
524    }
525
526    fn inside(part: &Part<S>, solid: &str, p: [f64; 3]) -> PointClassification {
527        let model = part.topology();
528        let solid = part.solid_id(solid).unwrap();
529        let point = Vector3::from_array(p.map(S::from_f64));
530        let mut result = PointClassification::Outside;
531        for &shell in &model.get_solid(solid).unwrap().shells {
532            match shell_contains(model, shell, point, 2000, S::from_f64(1e-6), 7).unwrap() {
533                PointClassification::Outside => {}
534                other => result = other,
535            }
536        }
537        result
538    }
539
540    #[test]
541    fn box_with_drill_hole_round_trips() {
542        let part = build_and_round_trip("box_with_drill_hole", &box_with_drill_hole());
543        let description = PartDescription::of(&part).unwrap();
544
545        // One solid, named after the step that cut the hole.
546        assert_eq!(
547            description.solids.keys().collect::<Vec<_>>(),
548            ["extrude(hole)"]
549        );
550        // The box's top keeps its name, and now has the hole in it.
551        let top = &description.faces["extrude(box,end)"];
552        assert_eq!(top.holes.len(), 1, "{top:?}");
553        // The hole's bottom is the hole tool's end cap; its wall is the
554        // four quarters swept by the circle.
555        assert!(description.faces.contains_key("extrude(hole,end)"));
556        let circle = box_with_drill_hole().steps[2].clone();
557        let crate::PartOperation::AddSketch(args) = circle.operation else {
558            unreachable!()
559        };
560        let circle_id = *args.sketch.curves.keys().next().unwrap();
561        for piece in ["", "#1", "#2", "#3"] {
562            let wall = format!("extrude(hole,hole_sketch,{circle_id}{piece})");
563            assert!(description.faces.contains_key(&wall), "no face {wall}");
564        }
565
566        assert_eq!(
567            inside(&part, "extrude(hole)", [0.3, 0.3, 0.5]),
568            PointClassification::Inside
569        );
570        assert_eq!(
571            inside(&part, "extrude(hole)", [1.0, 1.0, 0.8]),
572            PointClassification::Outside
573        );
574        assert_eq!(
575            inside(&part, "extrude(hole)", [1.0, 1.0, 0.3]),
576            PointClassification::Inside
577        );
578    }
579
580    #[test]
581    fn cross_drilled_shaft_round_trips() {
582        let part = build_and_round_trip("cross_drilled_shaft", &cross_drilled_shaft());
583        let description = PartDescription::of(&part).unwrap();
584        assert_eq!(
585            description.solids.keys().collect::<Vec<_>>(),
586            ["extrude(bore)"]
587        );
588        assert_eq!(
589            inside(&part, "extrude(bore)", [0.0, 0.8, 0.5]),
590            PointClassification::Inside
591        );
592        assert_eq!(
593            inside(&part, "extrude(bore)", [0.0, 0.0, 2.2]),
594            PointClassification::Outside
595        );
596        assert_eq!(
597            inside(&part, "extrude(bore)", [0.0, 0.0, 2.7]),
598            PointClassification::Inside
599        );
600    }
601
602    #[test]
603    fn two_plates_round_trip() {
604        let part = build_and_round_trip("two_plates", &two_plates());
605        let description = PartDescription::of(&part).unwrap();
606        assert_eq!(
607            description.solids["extrude(plates)"].len(),
608            2,
609            "one shell per plate"
610        );
611        assert_eq!(
612            inside(&part, "extrude(plates)", [0.2, 0.2, 0.0]),
613            PointClassification::Inside
614        );
615        assert_eq!(
616            inside(&part, "extrude(plates)", [0.75, 0.5, 0.0]),
617            PointClassification::Outside
618        );
619        assert_eq!(
620            inside(&part, "extrude(plates)", [2.5, 0.5, 0.1]),
621            PointClassification::Inside
622        );
623    }
624
625    #[test]
626    fn boss_on_reference_plane_round_trips() {
627        let part = build_and_round_trip("boss_on_reference_plane", &boss_on_reference_plane());
628        let description = PartDescription::of(&part).unwrap();
629        assert_eq!(
630            description.solids.keys().collect::<Vec<_>>(),
631            ["extrude(boss)"]
632        );
633        assert_eq!(description.datums, ["lifted"]);
634        // The boss stands on the box: from the box's top up to the plane.
635        assert_eq!(
636            inside(&part, "extrude(boss)", [1.0, 1.0, 1.3]),
637            PointClassification::Inside
638        );
639        assert_eq!(
640            inside(&part, "extrude(boss)", [1.0, 1.0, 1.6]),
641            PointClassification::Outside
642        );
643        assert_eq!(
644            inside(&part, "extrude(boss)", [0.2, 0.2, 1.3]),
645            PointClassification::Outside
646        );
647        assert_eq!(
648            inside(&part, "extrude(boss)", [0.2, 0.2, 0.5]),
649            PointClassification::Inside
650        );
651    }
652
653    /// The names of a program's entities don't depend on its numbers: a
654    /// taller box with a wider hole has the very same names.
655    #[test]
656    fn names_survive_a_change_of_dimensions() {
657        let names = |program: &Program| {
658            let part = program.apply(Part::<S>::new()).unwrap();
659            let description = PartDescription::of(&part).unwrap();
660            (
661                description.faces.keys().cloned().collect::<Vec<_>>(),
662                description.edges.keys().cloned().collect::<Vec<_>>(),
663                description.vertices.keys().cloned().collect::<Vec<_>>(),
664            )
665        };
666        let original = box_with_drill_hole();
667        let mut edited = original.clone();
668        for step in &mut edited.steps {
669            match &mut step.operation {
670                crate::PartOperation::Extrude(args) if step.id == "box" => args.distance = 1.5,
671                crate::PartOperation::AddSketch(args) if step.id == "hole_sketch" => {
672                    for c in args.sketch.constraints.values_mut() {
673                        if let Constraint::Radius { value, .. } = c {
674                            *value = 0.6;
675                        }
676                    }
677                    args.sketch.solve().unwrap();
678                }
679                _ => {}
680            }
681        }
682        assert_ne!(original, edited);
683        assert_eq!(names(&edited), names(&original));
684    }
685
686    /// An unknown name is reported, not guessed at.
687    #[test]
688    fn referring_to_a_missing_entity_fails() {
689        let mut program = box_with_drill_hole();
690        let crate::PartOperation::AddSketch(args) = &mut program.steps[2].operation else {
691            unreachable!()
692        };
693        args.plane = EntityRef::Face {
694            name: "extrude(box,side)".into(),
695        };
696        let Err(err) = program.apply(Part::<S>::new()) else {
697            panic!("a sketch on a face that doesn't exist was placed somewhere");
698        };
699        assert!(format!("{err:?}").contains("extrude(box,side)"), "{err:?}");
700    }
701
702    /// Step ids have to be unique: every name a step creates is built from
703    /// its id.
704    #[test]
705    fn duplicate_step_ids_are_rejected() {
706        let mut program = box_with_drill_hole();
707        program.steps[3].id = "box".into();
708        assert!(program.apply(Part::<S>::new()).is_err());
709    }
710
711    /// Reproduces the two real bug reports described on `handle_with_hole`.
712    ///
713    /// Currently fails during `program.apply` (not even reaching
714    /// `build_and_round_trip`'s own `validate`): the `Difference` boolean
715    /// in the "hole" step errors from `face_interior_point` — "no point
716    /// strictly inside face FaceId(52) was found ... a loop spanning
717    /// nothing encloses no area, so the face is degenerate". Exact match to
718    /// one of the two bug reports; the other report's variant of this same
719    /// outline (a slightly different hole center) instead *succeeds* but
720    /// leaves a non-watertight hole, which is the more likely true failure
721    /// mode — this error is probably the same root cause caught earlier by
722    /// a stricter check. Not yet root-caused. `handle_with_hole` is
723    /// deliberately left out of `all()` until this is fixed, so it does not
724    /// reach the CLI's `examples` export (breaking the landing page's
725    /// carousel build) or the web app's example list.
726    #[test]
727    #[ignore = "known failure: face_interior_point degenerate face in the hole's boolean difference, not yet root-caused — see this test's doc comment"]
728    fn handle_with_hole_round_trips() {
729        build_and_round_trip("handle_with_hole", &handle_with_hole());
730    }
731}