Skip to main content

geop_ops_extrude_revolve/
examples.rs

1//! The examples of extruding, revolving and sweeping: programs written in
2//! Rust against the operation types, which an editor offers to load
3//! ([`set`]).
4//!
5//! Each refers to what earlier steps built only by name, and so reads as
6//! the recipe it is: `extrude(box,end)` is the end cap of the step `box`,
7//! whatever internal id it happens to get.
8
9use std::collections::BTreeMap;
10
11use geop_core_editor::registry::ExampleSet;
12use geop_core_math::{geop_error::GeopResult, primitives::FrameAxis, vector::Vector3};
13use geop_core_program::{
14    element::ElementReference, numeric::NumericInput, operation_registry::OperationRegistry,
15    part::Part, program::Program, runner::Runner,
16};
17use geop_core_sketch::CurveId;
18use geop_lib_sweep::Orientation;
19use geop_ops_model::datums::origin_plane;
20use geop_ops_model::parameters::{
21    DefineParameters, LookupTable, Material, Parameter, ParameterKind, Parameters, PartProperties,
22    Row, set_parameters,
23};
24use geop_ops_model::sketch::{
25    Constraint, Sketch, SketchPlane,
26    figures::{circle, n, polygon, rectangle, rectangle_outline, solved},
27    sketch, sketch_element, whole_sketch,
28};
29use geop_ops_model::sketch3d::{Constraint3d, Sketch3d, sketch3d_path};
30use geop_ops_model::{Combine, element};
31use geop_ops_sketch::{
32    AddSketch, AddSketchArgs,
33    references::{Projected, Reference, Source},
34};
35use geop_ops_sketch3d::AddSketch3dArgs;
36
37use crate::{
38    Extent, Extents, Extrude, ExtrudeArgs, Output, PathRef, RevolveArgs, RevolveAxis, SweepArgs,
39};
40
41/// The examples of this crate, as an editor offers them.
42pub fn set() -> ExampleSet {
43    ExampleSet {
44        programs: vec![
45            ("box_with_drill_hole", box_with_drill_hole),
46            ("bracket", bracket),
47            ("cross_drilled_shaft", cross_drilled_shaft),
48            ("handle_with_hole", handle_with_hole),
49            ("luggage_tag", luggage_tag),
50            ("revolved_cone_on_box", revolved_cone_on_box),
51            ("parametric_plate", parametric_plate),
52            ("pipe", pipe),
53            ("horn", horn),
54            ("motor_flange", motor_flange),
55            ("edge_part", edge_part),
56            ("name_plate", name_plate),
57            ("coil_spring", coil_spring),
58            ("square_thread", square_thread),
59        ],
60        workspaces: Vec::new(),
61    }
62}
63
64/// The part `program` builds, written in the parameters, sketches and
65/// extrusions: what an example reads to draw its next sketch from.
66fn built(program: &Program) -> GeopResult<Part> {
67    let operations = (OperationRegistry::default())
68        .with(DefineParameters)?
69        .with(LookupTable)?
70        .with(PartProperties)?
71        .with(AddSketch)?
72        .with(Extrude)?;
73    let mut part = Part::default();
74    program.run(&Runner::new(operations), &mut part)?;
75    Ok(part)
76}
77
78/// The flat face `name` of the part, as a plane to sketch on.
79fn on_face(name: impl Into<String>) -> SketchPlane {
80    SketchPlane::Face(element(name))
81}
82
83/// The plane of the origin normal to `normal`, as a plane to sketch on.
84fn on_origin(normal: FrameAxis) -> SketchPlane {
85    SketchPlane::Datum(origin_plane(normal))
86}
87
88/// A coil spring: a 2 x 2 rectangle (`section`) on the origin's `y` plane,
89/// swept (`coil`) along a helix of radius 10 rising 20 in three turns round
90/// the `z` axis, drawn in a 3-D sketch (`route`).
91pub fn coil_spring() -> Program {
92    let mut program = Program::new();
93    program.push(
94        "section",
95        AddSketchArgs::on(
96            on_origin(FrameAxis::Y),
97            rectangle_outline(9.0, -1.0, 11.0, 1.0),
98        ),
99    );
100    let mut route = Sketch3d::new();
101    let at = |p: [f64; 3]| Vector3::from_array(p.map(n));
102    let start = route.add_point(at([10.0, 0.0, 0.0]));
103    let base = route.add_point(at([0.0, 0.0, 0.0]));
104    let top = route.add_point(at([0.0, 0.0, 20.0]));
105    route
106        .add_helix(start, base, top, n(3.0), false)
107        .expect("the helix has an axis and a start off it");
108    program.push(
109        "route",
110        AddSketch3dArgs {
111            sketch: route,
112            references: Vec::new(),
113        },
114    );
115    program.push(
116        "coil",
117        SweepArgs {
118            profile: whole_sketch("section"),
119            path: Some(PathRef::Sketch3d(sketch3d_path("route"))),
120            orientation: Orientation::FollowPath,
121            twist: 0.0,
122            end_scale: 1.0,
123            rails: Vec::new(),
124            output: Output::Solid(Combine::NewBody),
125        },
126    );
127    program
128}
129
130/// A name plate: a 40 x 14 x 3 plate sketched on the Z plane and
131/// extruded up (`plate`), and the word "GEOP", its capitals 7 tall, its
132/// baseline from a point fixed at `(7, 3.5)`, sketched on the plate's top
133/// `extrude(plate,end)` and extruded 1 up from it, joined to the plate
134/// (`letters`): each letter a lump standing on the plate, the holes of the
135/// `O` and the `P` open down to it.
136pub fn name_plate() -> Program {
137    let mut program = Program::new();
138
139    let mut outline = Sketch::new();
140    rectangle(&mut outline, [0.0, 0.0], 40.0, 14.0);
141    program.push(
142        "outline",
143        AddSketchArgs::on(on_origin(FrameAxis::Z), solved(outline)),
144    );
145    program.push(
146        "plate",
147        ExtrudeArgs {
148            sketch: whole_sketch("outline"),
149            extent: Extents::blind(3.0),
150            output: Output::Solid(Combine::NewBody),
151            draft: 0.0,
152        },
153    );
154
155    let mut lettering = Sketch::new();
156    let anchor = lettering.new_point_at([7.0, 3.5]);
157    lettering.constrain(Constraint::Fix {
158        point: anchor,
159        x: n(7.0),
160        y: n(3.5),
161    });
162    lettering.add_text(anchor, "GEOP", n(7.0), n(0.0));
163    program.push(
164        "lettering",
165        AddSketchArgs::on(on_face("extrude(plate,end)"), solved(lettering)),
166    );
167    program.push(
168        "letters",
169        ExtrudeArgs {
170            sketch: whole_sketch("lettering"),
171            extent: Extents::blind(1.0),
172            output: Output::Solid(Combine::join("extrude(plate)")),
173            draft: 0.0,
174        },
175    );
176    program
177}
178
179/// A 2 x 2 x 1 box with a blind hole drilled into its top: a rectangle
180/// sketched on the Z plane and extruded up (`box`), and a circle sketched on
181/// the box's end cap `extrude(box,end)` and extruded back into it, cutting
182/// it out of the box (`hole`) — the drilled box is `extrude(hole)`.
183pub fn box_with_drill_hole() -> Program {
184    let mut program = Program::new();
185
186    let mut outline = Sketch::new();
187    rectangle(&mut outline, [0.0, 0.0], 2.0, 2.0);
188    program.push(
189        "outline",
190        AddSketchArgs::on(on_origin(FrameAxis::Z), solved(outline)),
191    );
192    program.push(
193        "box",
194        ExtrudeArgs {
195            sketch: whole_sketch("outline"),
196            extent: Extents::blind(1.0),
197            output: Output::Solid(Combine::NewBody),
198            draft: 0.0,
199        },
200    );
201
202    let mut hole = Sketch::new();
203    circle(&mut hole, [1.0, 1.0], 0.4);
204    program.push(
205        "hole_sketch",
206        AddSketchArgs::on(on_face("extrude(box,end)"), solved(hole)),
207    );
208    program.push(
209        "hole",
210        ExtrudeArgs {
211            sketch: whole_sketch("hole_sketch"),
212            extent: Extents::blind(-0.5),
213            output: Output::Solid(Combine::cut("extrude(box)")),
214            draft: 0.0,
215        },
216    );
217    program
218}
219
220/// A 40 x 40 x 10 mm bracket with a 6 mm hole through its middle: a
221/// rectangle sketched on the Z plane and extruded up (`block`), and a
222/// circle sketched on the block's end cap and extruded back through its
223/// full thickness, cutting it out of the block (`hole`). Unlike
224/// `box_with_drill_hole`, the cut ends exactly on the bottom face, so the
225/// hole goes all the way through. The program the landing page's agent demo
226/// transcript writes, step for step.
227pub fn bracket() -> Program {
228    let mut program = Program::new();
229
230    let mut outline = Sketch::new();
231    rectangle(&mut outline, [0.0, 0.0], 40.0, 40.0);
232    program.push(
233        "outline",
234        AddSketchArgs::on(on_origin(FrameAxis::Z), solved(outline)),
235    );
236    program.push(
237        "block",
238        ExtrudeArgs {
239            sketch: whole_sketch("outline"),
240            extent: Extents::blind(10.0),
241            output: Output::Solid(Combine::NewBody),
242            draft: 0.0,
243        },
244    );
245
246    let mut hole = Sketch::new();
247    circle(&mut hole, [20.0, 20.0], 3.0);
248    program.push(
249        "hole_sketch",
250        AddSketchArgs::on(on_face("extrude(block,end)"), solved(hole)),
251    );
252    program.push(
253        "hole",
254        ExtrudeArgs {
255            sketch: whole_sketch("hole_sketch"),
256            extent: Extents::blind(-10.0),
257            output: Output::Solid(Combine::cut("extrude(block)")),
258            draft: 0.0,
259        },
260    );
261    program
262}
263
264/// A stepped shaft revolved around the world z-axis, cross-drilled through
265/// its thinner end: a half section sketched on the X plane, closed by the
266/// axis itself (`shaft`), and a circle on the Y plane extruded through both
267/// sides of it and cut out of it (`bore`) — the drilled shaft is
268/// `extrude(bore)`.
269pub fn cross_drilled_shaft() -> Program {
270    let mut program = Program::new();
271
272    // Sketch x runs along world y, sketch y along world z.
273    let mut section = Sketch::new();
274    let (p, l) = polygon(
275        &mut section,
276        &[
277            [0.0, 0.0],
278            [1.0, 0.0],
279            [1.0, 1.0],
280            [0.6, 1.0],
281            [0.6, 3.0],
282            [0.0, 3.0],
283        ],
284    );
285    let axis = l[5];
286    section.constrain(Constraint::Fix {
287        point: p[0],
288        x: n(0.0),
289        y: n(0.0),
290    });
291    section.constrain(Constraint::Vertical { line: axis });
292    for (line, horizontal) in [
293        (l[0], true),
294        (l[1], false),
295        (l[2], true),
296        (l[3], false),
297        (l[4], true),
298    ] {
299        section.constrain(if horizontal {
300            Constraint::Horizontal { line }
301        } else {
302            Constraint::Vertical { line }
303        });
304    }
305    section.constrain(Constraint::Length {
306        curve: l[0],
307        value: n(1.0),
308    });
309    section.constrain(Constraint::Length {
310        curve: l[1],
311        value: n(1.0),
312    });
313    section.constrain(Constraint::Length {
314        curve: l[3],
315        value: n(2.0),
316    });
317    section.constrain(Constraint::Length {
318        curve: l[4],
319        value: n(0.6),
320    });
321    program.push(
322        "section",
323        AddSketchArgs::on(on_origin(FrameAxis::X), solved(section)),
324    );
325    program.push(
326        "shaft",
327        RevolveArgs {
328            sketch: whole_sketch("section"),
329            axis: Some(RevolveAxis::Sketch(sketch_element("section", axis))),
330            extent: Extents::blind(360.0),
331            output: Output::Solid(Combine::NewBody),
332        },
333    );
334
335    // Sketch x runs along world x, sketch y along world -z: a bore across
336    // the thin end, at z = 2.2.
337    let mut bore = Sketch::new();
338    circle(&mut bore, [0.0, -2.2], 0.25);
339    program.push(
340        "bore_sketch",
341        AddSketchArgs::on(on_origin(FrameAxis::Y), solved(bore)),
342    );
343    program.push(
344        "bore",
345        ExtrudeArgs {
346            sketch: whole_sketch("bore_sketch"),
347            extent: Extents {
348                side1: Extent::blind(3.0),
349                symmetric: true,
350                side2: None,
351                reversed: false,
352            },
353            output: Output::Solid(Combine::cut("revolve(shaft)")),
354            draft: 0.0,
355        },
356    );
357    program
358}
359
360/// A hand-drawn handle-shaped outline (an irregular blob, splined, with a
361/// smaller splined region inside it — as a free-hand sketch in the web
362/// editor draws, unconstrained) extruded, then cross-drilled with a
363/// circular hole near one end.
364///
365/// Reproduces a real user bug report (2026-09-27) with the exact reported
366/// coordinates, unconstrained, rather than a re-derived "clean" equivalent
367/// that might not carry the same numerical case at all: the hole's boolean
368/// difference failed with a degenerate-face error from
369/// `face_interior_point`. (A second report, of the same outline with a
370/// slightly different hole center, left a non-watertight hole instead; its
371/// coordinates were not recorded.)
372pub fn handle_with_hole() -> Program {
373    let mut program = Program::new();
374
375    let mut outline = Sketch::new();
376    let outer_points = [
377        (0.01007682018456503, 0.8939399106232252),
378        (-0.9693901017551558, 0.8213868052943569),
379        (-1.00566665441959, 0.3175457960661055),
380        (-0.9452057333121998, -0.3555857922628385),
381        (0.09472210973491128, -0.9481028191152622),
382        (0.695300592734987, -0.3878316168534466),
383        (1.384555093359235, -0.4523232660346628),
384        (1.6233271012047248, -0.9224957048864859),
385        (1.9214381194506664, 0.15401630544608172),
386        (1.2092840203075834, 0.8578895429712221),
387    ]
388    .map(|(x, y)| outline.add_point(n(x), n(y)));
389    let mut outer_loop = outer_points.to_vec();
390    outer_loop.push(outer_points[0]);
391    outline.add_spline(outer_loop);
392
393    let inner_points = [
394        (0.27854547786607586, 0.568500810276078),
395        (-0.3241022513094386, 0.561767316095346),
396        (-0.3645032163938306, 0.00962079327532167),
397        (0.03950643445008967, -0.4246895813818926),
398        (0.507484280010964, -0.11831559615858644),
399        (0.8643594715897602, 0.12072344725739975),
400        (1.1202322504575766, -0.1452495728815144),
401        (1.2919363520662426, 0.18805838906471978),
402        (1.0427970673791584, 0.470865144655464),
403    ]
404    .map(|(x, y)| outline.add_point(n(x), n(y)));
405    let mut inner_loop = inner_points.to_vec();
406    inner_loop.push(inner_points[0]);
407    outline.add_spline(inner_loop);
408
409    program.push(
410        "outline",
411        AddSketchArgs::on(on_origin(FrameAxis::Z), solved(outline)),
412    );
413    program.push(
414        "handle",
415        ExtrudeArgs {
416            sketch: whole_sketch("outline"),
417            extent: Extents::blind(1.0),
418            output: Output::Solid(Combine::NewBody),
419            draft: 0.0,
420        },
421    );
422
423    let mut hole = Sketch::new();
424    let center = hole.add_point(n(0.32989396295411244), n(-0.5632891678453777));
425    hole.add_circle(center, n(0.31727744879927977));
426    program.push(
427        "hole_sketch",
428        AddSketchArgs::on(on_origin(FrameAxis::Y), solved(hole)),
429    );
430    program.push(
431        "hole",
432        ExtrudeArgs {
433            sketch: whole_sketch("hole_sketch"),
434            extent: Extents {
435                side1: Extent::blind(2.44),
436                symmetric: true,
437                side2: None,
438                reversed: false,
439            },
440            output: Output::Solid(Combine::cut("extrude(handle)")),
441            draft: 0.0,
442        },
443    );
444    program
445}
446
447/// A rounded-top luggage tag: two vertical sides and a horizontal bottom,
448/// closed by a semicircular top tangent to both sides, with a hang-hole
449/// through the top (concentric with it) and a rectangular window in the
450/// body, inset 0.15 from each of the two sides and the bottom.
451pub fn luggage_tag() -> Program {
452    let mut program = Program::new();
453
454    let mut outline = Sketch::new();
455    let p0 = outline.add_point(n(-0.7051397478635735), n(0.7809633733011653));
456    let p1 = outline.add_point(n(-0.7051397478646341), n(-0.6729821470985564));
457    let p4 = outline.add_point(n(0.695311597099524), n(-0.6729821470973941));
458    let p7 = outline.add_point(n(0.695311597101934), n(0.7809633732995341));
459    let left = outline.add_line(p0, p1);
460    let bottom = outline.add_line(p1, p4);
461    let right = outline.add_line(p4, p7);
462    let top = outline.add_arc(p7, p0, n(std::f64::consts::PI));
463    outline.constrain(Constraint::Vertical { line: left });
464    outline.constrain(Constraint::Horizontal { line: bottom });
465    outline.constrain(Constraint::Vertical { line: right });
466    outline.constrain(Constraint::Tangent { a: top, b: right });
467    outline.constrain(Constraint::Tangent { a: top, b: left });
468
469    let hole_center = outline.add_point(n(-0.004914075380876311), n(0.7809633733016882));
470    let hole = outline.add_circle(hole_center, n(0.30711303824557856));
471    outline.constrain(Constraint::Concentric { a: hole, b: top });
472
473    let w_tl = outline.add_point(n(-0.5551397478558091), n(0.08482972707824256));
474    let w_tr = outline.add_point(n(0.5453115971039223), n(0.08482972707824256));
475    let w_br = outline.add_point(n(0.5453115971014348), n(-0.5229821471055477));
476    let w_bl = outline.add_point(n(-0.5551397478598076), n(-0.5229821471049918));
477    let w_top = outline.add_line(w_tl, w_tr);
478    let w_right = outline.add_line(w_tr, w_br);
479    let w_bottom = outline.add_line(w_br, w_bl);
480    let w_left = outline.add_line(w_bl, w_tl);
481    outline.constrain(Constraint::Horizontal { line: w_top });
482    outline.constrain(Constraint::Vertical { line: w_right });
483    outline.constrain(Constraint::Horizontal { line: w_bottom });
484    outline.constrain(Constraint::Vertical { line: w_left });
485    outline.constrain(Constraint::PointLineDistance {
486        point: w_br,
487        line: right,
488        value: n(0.15),
489    });
490    outline.constrain(Constraint::PointLineDistance {
491        point: w_bl,
492        line: left,
493        value: n(0.15),
494    });
495    outline.constrain(Constraint::PointLineDistance {
496        point: w_br,
497        line: bottom,
498        value: n(0.15),
499    });
500
501    program.push(
502        "outline",
503        AddSketchArgs::on(on_origin(FrameAxis::Z), solved(outline)),
504    );
505    program.push(
506        "tag",
507        ExtrudeArgs {
508            sketch: whole_sketch("outline"),
509            extent: Extents {
510                side1: Extent::blind(1.0 / 3.0),
511                symmetric: true,
512                side2: None,
513                reversed: false,
514            },
515            output: Output::Solid(Combine::NewBody),
516            draft: 0.0,
517        },
518    );
519    program
520}
521
522/// A box with a round hole through it, and a triangle sketched on its side
523/// face, revolved around one of its own edges and joined to the box: a double
524/// cone whose axis lies in the side face, so that both apexes sit on it and
525/// half the cone stands out of the box.
526///
527/// Reproduces a real user bug report (2026-09-30) with the exact reported
528/// coordinates and sketch ids: the `revolve1` union failed in
529/// `remesh_edges_x_faces` with "the spokes ... are not coplanar: an apex has
530/// no single normal" from `NurbSurface::pole_normal`: `trace_one_side` asked
531/// for the cone's normal at its apex, to find which way a curve leaves it.
532pub fn revolved_cone_on_box() -> Program {
533    let mut program = Program::new();
534
535    let mut outline = Sketch::new();
536    let p0 = outline.add_point(n(-2.046526714311965), n(1.182776884816516));
537    let p1 = outline.add_point(n(2.2456482324612383), n(-1.2774916901086213));
538    let p2 = outline.add_point(n(2.2456482324612383), n(1.182776884816516));
539    let p3 = outline.add_point(n(-2.046526714311965), n(-1.2774916901086213));
540    let top = outline.add_line(p0, p2);
541    outline.constrain(Constraint::Horizontal { line: top });
542    let right = outline.add_line(p2, p1);
543    outline.constrain(Constraint::Vertical { line: right });
544    let bottom = outline.add_line(p1, p3);
545    outline.constrain(Constraint::Horizontal { line: bottom });
546    let left = outline.add_line(p3, p0);
547    outline.constrain(Constraint::Vertical { line: left });
548    let center = outline.add_point(n(-0.6692695471128323), n(0.04905776553659026));
549    outline.add_circle(center, n(0.771466957205246));
550    program.push(
551        "sketch1",
552        AddSketchArgs::on(on_origin(FrameAxis::Z), outline),
553    );
554    program.push(
555        "extrude1",
556        ExtrudeArgs {
557            sketch: whole_sketch("sketch1"),
558            extent: Extents::blind(1.8900000000000001),
559            output: Output::Solid(Combine::NewBody),
560            draft: 0.0,
561        },
562    );
563
564    let mut triangle = Sketch::new();
565    let q0 = triangle.add_point(n(-0.2654953575323612), n(1.4039697276195047));
566    let q1 = triangle.add_point(n(-0.8683910652620968), n(0.8073854454124275));
567    let axis = triangle.add_line(q0, q1);
568    let q3 = triangle.add_point(n(1.9210929721682684), n(0.8073854454124275));
569    let base = triangle.add_line(q1, q3);
570    triangle.constrain(Constraint::Horizontal { line: base });
571    triangle.add_line(q3, q0);
572    program.push(
573        "sketch2",
574        AddSketchArgs::on(
575            on_face(format!("extrude(extrude1,sketch1,{right})")),
576            triangle,
577        ),
578    );
579    program.push(
580        "revolve1",
581        RevolveArgs {
582            sketch: whole_sketch("sketch2"),
583            axis: Some(RevolveAxis::Sketch(sketch_element("sketch2", axis))),
584            extent: Extents::blind(360.0),
585            output: Output::Solid(Combine::join("extrude(extrude1)")),
586        },
587    );
588    program
589}
590
591/// A plate designed by its parameters: `width` and `depth` — half the
592/// width, unless placed otherwise — extruded `thickness` thick, and a blind
593/// hole in its middle sized for the screw chosen from a table of sizes,
594/// and its colour. The hole's sketch,
595/// on the plate's top, projects the top, and puts the hole in the middle
596/// of it. The plate is meant to be placed with other values, and
597/// everything follows.
598pub fn parametric_plate() -> Program {
599    let mut program = Program::new();
600    set_parameters(
601        &mut program,
602        Parameters {
603            title: None,
604            designation: None,
605            color: Some("#d0893e".into()),
606            material: Material::named("Aluminium 6061"),
607            values: vec![
608                Parameter::number("width", "4", 1.0, 10.0),
609                Parameter::number("depth", "width / 2", 0.5, 10.0),
610                Parameter {
611                    name: "screw".into(),
612                    kind: ParameterKind::Table {
613                        columns: vec!["clearance".into(), "head".into()],
614                        rows: [
615                            ("M3", 0.34, 0.6),
616                            ("M4", 0.45, 0.8),
617                            ("M5", 0.55, 1.0),
618                            ("M6", 0.66, 1.2),
619                        ]
620                        .map(|(name, clearance, head)| Row {
621                            name: name.into(),
622                            values: vec![clearance, head],
623                        })
624                        .to_vec(),
625                        selected: "M4".into(),
626                    },
627                },
628                Parameter::number("thickness", "0.5", 0.1, 2.0),
629            ],
630        },
631    );
632
633    let mut outline = Sketch::new();
634    let lines = rectangle(&mut outline, [0.0, 0.0], 4.0, 2.0);
635    let length = |line: CurveId| {
636        outline
637            .constraints
638            .iter()
639            .find(|(_, c)| matches!(c, Constraint::Length { curve, .. } if *curve == line))
640            .map(|(&id, _)| id)
641            .expect("the rectangle's lengths")
642    };
643    let formulas = BTreeMap::from([
644        (length(lines[0]), NumericInput::from("width")),
645        (length(lines[1]), NumericInput::from("depth")),
646    ]);
647    program.push(
648        "outline",
649        AddSketchArgs {
650            formulas,
651            ..AddSketchArgs::on(on_origin(FrameAxis::Z), solved(outline))
652        },
653    );
654    program.push(
655        "plate",
656        ExtrudeArgs {
657            sketch: whole_sketch("outline"),
658            extent: Extents::blind("thickness"),
659            output: Output::Solid(Combine::NewBody),
660            draft: 0.0,
661        },
662    );
663
664    // The hole's center is the middle of the top's diagonal, between two
665    // of its corners as projected: wherever the parameters put them.
666    let mut hole = Sketch::new();
667    let top = "extrude(plate,end)";
668    let mut projection = Reference::new(Source::Projection {
669        entity: Projected::Face(element(top)),
670    });
671    let part = built(&program).expect("the plate builds");
672    let reader = part.current();
673    let plane = (on_face(top).resolve(&reader)).expect("the top is planar");
674    projection
675        .update(&mut hole, &reader, &plane)
676        .expect("the top projects");
677    let corner = |p: &str| projection.points[&format!("extrude(plate,outline,{p},end)")];
678    let diagonal = hole.add_line(corner("p0"), corner("p2"));
679    hole.set_construction(diagonal, true);
680    let center = hole.add_point(n(2.0), n(1.0));
681    let circle = hole.add_circle(center, n(0.2));
682    hole.constrain(Constraint::Midpoint {
683        point: center,
684        curve: diagonal,
685    });
686    let diameter = hole.constrain(Constraint::Diameter {
687        curve: circle,
688        value: n(0.45),
689    });
690    program.push(
691        "hole_sketch",
692        AddSketchArgs {
693            references: vec![projection],
694            formulas: BTreeMap::from([(diameter, NumericInput::from("screw.clearance"))]),
695            ..AddSketchArgs::on(on_face(top), solved(hole))
696        },
697    );
698    program.push(
699        "hole",
700        ExtrudeArgs {
701            sketch: whole_sketch("hole_sketch"),
702            extent: Extents::blind(-0.3),
703            output: Output::Solid(Combine::cut("extrude(plate)")),
704            draft: 0.0,
705        },
706    );
707    program
708}
709
710/// The route of [`pipe`], its bends of radius `bend`: from the origin
711/// along `x`, a bend turning to `y` at `(4, 0, 0)`, a bend turning up to
712/// `z` at `(4, 4, 0)`, and up to `(4, 4, 4)` — every bend tangent to the
713/// lines it joins.
714pub fn pipe_route(bend: f64) -> Sketch3d {
715    let mut s = Sketch3d::new();
716    let (r, h) = (bend, std::f64::consts::FRAC_1_SQRT_2);
717    let v = |x: f64, y: f64, z: f64| Vector3::from_array([x, y, z].map(n));
718    let p = [
719        s.add_point(v(0.0, 0.0, 0.0)),
720        s.add_point(v(4.0 - r, 0.0, 0.0)),
721        s.add_point(v(4.0 - r + r * h, r - r * h, 0.0)),
722        s.add_point(v(4.0, r, 0.0)),
723        s.add_point(v(4.0, 4.0 - r, 0.0)),
724        s.add_point(v(4.0, 4.0 - r + r * h, r - r * h)),
725        s.add_point(v(4.0, 4.0, r)),
726        s.add_point(v(4.0, 4.0, 4.0)),
727    ];
728    let curves = [
729        s.add_line(p[0], p[1]),
730        s.add_arc(p[1], p[2], p[3]),
731        s.add_line(p[3], p[4]),
732        s.add_arc(p[4], p[5], p[6]),
733        s.add_line(p[6], p[7]),
734    ];
735    for axis in geop_core_sketch::space::Coordinate::ALL {
736        s.constrain(Constraint3d::Coordinate {
737            point: p[0],
738            axis,
739            value: n(0.0),
740        });
741    }
742    s.constrain(Constraint3d::ParallelTo {
743        line: curves[0],
744        direction: v(1.0, 0.0, 0.0),
745    });
746    s.constrain(Constraint3d::Length {
747        line: curves[0],
748        value: n(4.0 - r),
749    });
750    for w in curves.windows(2) {
751        s.constrain(Constraint3d::Tangent { a: w[0], b: w[1] });
752    }
753    for arc in [curves[1], curves[3]] {
754        s.constrain(Constraint3d::Radius { arc, value: n(r) });
755    }
756    let report = s.solve().expect("the route is valid");
757    assert!(report.converged, "the route does not solve: {report:?}");
758    s
759}
760
761/// A pipe bent through space — a cable conduit, say: a circle of radius
762/// 0.3 on the `x` plane (`section`), swept along a 3-D sketch of lines and
763/// bends (`route`, see [`pipe_route`]) into the solid `sweep(pipe)`.
764pub fn pipe() -> Program {
765    let mut program = Program::new();
766    let mut section = Sketch::new();
767    circle(&mut section, [0.0, 0.0], 0.3);
768    program.push(
769        "section",
770        AddSketchArgs::on(on_origin(FrameAxis::X), solved(section)),
771    );
772    program.push(
773        "route",
774        AddSketch3dArgs {
775            sketch: pipe_route(1.0),
776            references: Vec::new(),
777        },
778    );
779    program.push(
780        "pipe",
781        SweepArgs {
782            profile: whole_sketch("section"),
783            path: Some(PathRef::Sketch3d(sketch3d_path("route"))),
784            orientation: Orientation::FollowPath,
785            twist: 0.0,
786            end_scale: 1.0,
787            rails: Vec::new(),
788            output: Output::Solid(Combine::NewBody),
789        },
790    );
791    program
792}
793
794/// A horn: a circle swept along a straight path, its size set by a rail —
795/// a spline flaring out beside the path, which the circle's point on it
796/// follows.
797pub fn horn() -> Program {
798    let base = |axis| Some(on_origin(axis));
799    let mut program = Program::new();
800    let mut mouth = Sketch::new();
801    let c = mouth.add_point(n(0.0), n(0.0));
802    mouth.add_circle(c, n(0.5));
803    program.push(
804        "mouth",
805        AddSketchArgs {
806            plane: base(FrameAxis::X),
807            sketch: mouth,
808            ..Default::default()
809        },
810    );
811    let mut axis = Sketch::new();
812    let (a, b) = (
813        axis.add_point(n(0.0), n(0.0)),
814        axis.add_point(n(4.0), n(0.0)),
815    );
816    axis.add_line(a, b);
817    program.push(
818        "axis",
819        AddSketchArgs {
820            plane: base(FrameAxis::Z),
821            sketch: axis,
822            ..Default::default()
823        },
824    );
825    let mut flare = Sketch::new();
826    let points =
827        [[0.0, 0.5], [1.5, 0.4], [3.0, 0.9], [4.0, 1.6]].map(|[x, y]| flare.add_point(n(x), n(y)));
828    flare.add_spline(points.to_vec());
829    program.push(
830        "flare",
831        AddSketchArgs {
832            plane: base(FrameAxis::Z),
833            sketch: flare,
834            ..Default::default()
835        },
836    );
837    program.push(
838        "horn",
839        SweepArgs {
840            profile: whole_sketch("mouth"),
841            path: Some(PathRef::Sketch(ElementReference::whole(sketch("axis")))),
842            orientation: Orientation::FollowPath,
843            twist: 0.0,
844            end_scale: 1.0,
845            rails: vec![PathRef::Sketch(ElementReference::whole(sketch("flare")))],
846            output: Output::Solid(Combine::NewBody),
847        },
848    );
849    program
850}
851
852/// A motor flange 30 across and 2.5 thick, drawn the way the sketch tools
853/// draw it: its bore an offset of its rim, 10 in, so the wall follows the
854/// rim; and its bolt circle one hole patterned six times round its middle,
855/// so the copies follow the first hole.
856pub fn motor_flange() -> Program {
857    let mut program = Program::new();
858    let mut outline = Sketch::new();
859    let middle = outline.add_point(n(0.0), n(0.0));
860    outline.constrain(Constraint::Fix {
861        point: middle,
862        x: n(0.0),
863        y: n(0.0),
864    });
865    let rim = outline.add_circle(middle, n(16.0));
866    outline.constrain(Constraint::Radius {
867        curve: rim,
868        value: n(15.0),
869    });
870    outline
871        .offset(&[rim], n(10.0), geop_core_sketch::offset::Corners::Round)
872        .expect("a circle offsets inwards by less than its radius");
873    let bolt = circle(&mut outline, [11.0, 0.0], 1.2);
874    outline
875        .pattern(
876            &[bolt],
877            &geop_core_sketch::copies::Step::Round {
878                center: middle,
879                angle: n(std::f64::consts::PI / 3.0),
880            },
881            6,
882        )
883        .expect("a hole off the middle turns round it");
884    program.push(
885        "outline",
886        AddSketchArgs::on(on_origin(FrameAxis::Z), solved(outline)),
887    );
888    program.push(
889        "flange",
890        ExtrudeArgs {
891            sketch: whole_sketch("outline"),
892            extent: Extents::blind(2.5),
893            output: Output::Solid(Combine::NewBody),
894            draft: 0.0,
895        },
896    );
897    program
898}
899
900/// A square thread, as drawn in the editor: a rectangle standing on a
901/// revolved cylinder, swept five turns along a helix in a 3-D sketch and
902/// joined to it, the wall it stands on lying in the cylinder's. Kept as the
903/// program it saved, as [`edge_part`] is.
904pub fn square_thread() -> Program {
905    Program::from_json(include_str!("drawn/square_thread.geop"))
906        .expect("a drawn example is a program")
907}
908
909/// Three outlines sketched on the origin's three planes, each extruded and
910/// joined to the one before, as drawn in the editor.
911///
912/// An example drawn in the editor and kept as the program it saved (in
913/// `src/drawn/`): its sketches as they were drawn, which no code says
914/// better. Its tests build it like every other example, so a change to the
915/// format that breaks it is found there: save it again from the editor.
916pub fn edge_part() -> Program {
917    Program::from_json(include_str!("drawn/edge_part.geop")).expect("a drawn example is a program")
918}