Skip to main content

geop_ops_assembly/
examples.rs

1//! The examples of assemblies ([`set`]): programs placing the parts other
2//! files build, written in Rust against the operation types — and the
3//! parts they place, the files of each example made of several
4//! ([`workspaces`]).
5//!
6//! Each refers to what earlier steps built only by name, and so reads as
7//! the recipe it is: `extrude(pin,start)` is the bottom of the pin.
8
9// An example is fixed data: one that does not build is a bug in it, said
10// where it is made.
11#![allow(
12    clippy::expect_used,
13    reason = "an example that does not build is a bug in it"
14)]
15
16use std::collections::BTreeMap;
17
18use geop_core_editor::registry::{Example, ExampleFiles, ExampleSet};
19use geop_core_math::{
20    primitives::{FrameAxis, Pose},
21    vector::Vector3,
22};
23use geop_core_program::{
24    operation_registry::OperationRegistry,
25    part::Part,
26    program::Program,
27    runner::Runner,
28    values::{Setting, Settings, StateValue, StateValues, pose_state},
29};
30use geop_core_sketch::CurveId;
31use geop_lib_stdlib::{Drawing, outline_plane};
32use geop_ops_extrude_revolve::{
33    Extent, Extents, Extrude, ExtrudeArgs, Output,
34    examples::{box_with_drill_hole, parametric_plate},
35};
36use geop_ops_model::datums::{datum, origin_plane};
37use geop_ops_model::parameters::{Material, Parameters, set_parameters};
38use geop_ops_model::selection::Selected;
39use geop_ops_model::sketch::{
40    Sketch, SketchPlane,
41    figures::{circle, n, rectangle, solved},
42    whole_sketch,
43};
44use geop_ops_model::{Combine, PartDescription, element, read_model};
45use geop_ops_sketch::{AddSketch, AddSketchArgs};
46
47use crate::{
48    AddPartArgs, ExplodeArgs, Joint, JointKind, Kind, Limits, Mate, Motion, MotionArgs,
49    joint_parameter,
50};
51use geop_core_math::primitives::DatumComponent;
52use geop_core_program::element::ElementReference;
53use geop_ops_model::datums::{ORIGIN, component_name, datums};
54use geop_ops_model::mate_entity::MateEntity;
55use geop_ops_model::placed::motion::MotionStudy;
56
57/// The plane of the origin normal to `normal`, as a plane to sketch on.
58fn on_origin(normal: FrameAxis) -> SketchPlane {
59    SketchPlane::Datum(origin_plane(normal))
60}
61
62/// A pose of an example: at `position`, turned by `degrees` (see
63/// [`Pose::from_euler`]) — as a file holds it ([`Pose::saved`]): where an
64/// example puts a part to start from is a free choice, and one its file
65/// can say saves and reads back as it is.
66pub fn pose(position: [f64; 3], degrees: [f64; 3]) -> Pose {
67    (Pose::from_euler(Vector3::from_array(position.map(n)), degrees.map(n)))
68        .and_then(|pose| pose.saved())
69        .expect("an example's rotation is one")
70}
71
72/// The part `program` builds on its own, written in sketches and
73/// extrusions: what an example reads of a part it places.
74fn built(program: &Program) -> Part {
75    let registry = (OperationRegistry::default().with(AddSketch))
76        .and_then(|r| r.with(Extrude))
77        .expect("the operations each have a name of their own");
78    let mut part = Part::default();
79    (program.run(&Runner::new(registry), &mut part)).expect("the part builds");
80    part
81}
82
83/// A pin: a circle of radius 0.4 around the origin, extruded 2 up — made to
84/// fit the hole of [`box_with_drill_hole`].
85pub fn pin() -> Program {
86    let mut program = Program::new();
87    let mut sketch = Sketch::new();
88    circle(&mut sketch, [0.0, 0.0], 0.4);
89    program.push(
90        "pin_sketch",
91        AddSketchArgs::on(on_origin(FrameAxis::Z), solved(sketch)),
92    );
93    program.push(
94        "pin",
95        ExtrudeArgs {
96            sketch: whole_sketch("pin_sketch"),
97            extent: Extents::blind(2.0),
98            output: Output::Solid(Combine::NewBody),
99            draft: 0.0,
100        },
101    );
102    program
103}
104
105/// The assembly of `pin_in_plate`: the plate (`plate.geop`, the box of
106/// [`box_with_drill_hole`]) placed fixed, and the pin (`pin.geop`, [`pin`])
107/// mated into its hole — its side on the hole's axis, its bottom on the
108/// hole's. Its state puts both where those mates hold them.
109pub fn pin_in_plate_assembly() -> Program {
110    let mut program = Program::new();
111    program.push(
112        "plate",
113        AddPartArgs {
114            file: "plate.geop".into(),
115            ..Default::default()
116        }
117        .fixed(),
118    );
119    let mate = |kind, [a, b]: [&str; 2]| {
120        Mate::constraint(kind, vec![placed_face("pin", a), placed_face("plate", b)])
121    };
122    program.push(
123        "pin",
124        AddPartArgs {
125            file: "pin.geop".into(),
126            mates: BTreeMap::from([
127                (
128                    "m1".into(),
129                    mate(
130                        Kind::Concentric,
131                        ["extrude(pin,pin_sketch,c1)", "extrude(hole,hole_sketch,c1)"],
132                    ),
133                ),
134                (
135                    "m2".into(),
136                    mate(
137                        Kind::Coincident,
138                        ["extrude(pin,start)", "extrude(hole,end)"],
139                    ),
140                ),
141            ]),
142            ..Default::default()
143        },
144    );
145    let pose = |position| StateValue::Pose(pose(position, [0.0; 3]));
146    program.state = StateValues::from([
147        (pose_state("plate"), pose([0.0; 3])),
148        (pose_state("pin"), pose([1.0, 1.0, 0.5])),
149    ]);
150    program
151}
152
153/// A link's sketch: a 4 x 1 bar around the origin's `x` axis, from -0.5
154/// to 3.5, with a hole of radius 0.3 at each end, 3 apart — and the holes.
155/// A slotted bar `length` long between the centers of the holes at its
156/// ends — the near one at the origin, the far one along `x` — and its
157/// holes, near one first. Built alike for every length, so its entities are
158/// named alike.
159fn link_sketch(length: f64) -> (Sketch, [CurveId; 2]) {
160    let mut sketch = Sketch::new();
161    rectangle(&mut sketch, [-0.5, -0.5], length + 1.0, 1.0);
162    let near = circle(&mut sketch, [0.0, 0.0], 0.3);
163    let far = circle(&mut sketch, [length, 0.0], 0.3);
164    (solved(sketch), [near, far])
165}
166
167/// A bar of aluminium: [`link_sketch`] `length` long, extruded 0.2 up, to
168/// be linked by its holes.
169pub fn bar(length: f64) -> Program {
170    let mut program = Program::new();
171    set_parameters(
172        &mut program,
173        Parameters {
174            material: Material::named("Aluminium 6061"),
175            ..Parameters::default()
176        },
177    );
178    program.push(
179        "link_sketch",
180        AddSketchArgs::on(on_origin(FrameAxis::Z), link_sketch(length).0),
181    );
182    program.push(
183        "link",
184        ExtrudeArgs {
185            sketch: whole_sketch("link_sketch"),
186            extent: Extents::blind(0.2),
187            output: Output::Solid(Combine::NewBody),
188            draft: 0.0,
189        },
190    );
191    program
192}
193
194/// The link of the chain: a [`bar`] 3 long.
195pub fn link() -> Program {
196    bar(3.0)
197}
198
199/// The face `name` of the part placed as `instance`, as a mate holds it.
200fn placed_face(instance: &str, name: impl Into<String>) -> MateEntity {
201    MateEntity::of(instance, Selected::Face(element(name)))
202}
203
204/// The faces of the holes of a [`bar`] placed as `instance`, near one first.
205pub fn link_holes(instance: &str) -> [MateEntity; 2] {
206    (link_sketch(3.0).1)
207        .map(|hole| placed_face(instance, format!("extrude(link,link_sketch,{hole})")))
208}
209
210/// The [`bar`] placed as `above` lies on the one placed as `below`: its
211/// bottom face on the other's top — so bars linked in a stack never cut
212/// into each other.
213fn on_top(above: &str, below: &str) -> Mate {
214    Mate::constraint(
215        Kind::Coincident,
216        vec![
217            placed_face(above, "extrude(link,start)"),
218            placed_face(below, "extrude(link,end)"),
219        ],
220    )
221}
222
223/// The hole `hole` of the bar placed as `link` on the axis of the hole `pin`
224/// of the one placed as `on`.
225fn pinned(link: &str, hole: usize, on: &str, pin: usize) -> Mate {
226    Mate::constraint(
227        Kind::Concentric,
228        vec![link_holes(on)[pin].clone(), link_holes(link)[hole].clone()],
229    )
230}
231
232/// A part placed from `file`, held by `mates`.
233fn placed(file: &str, fixed: bool, mates: Vec<Mate>) -> AddPartArgs {
234    let args = AddPartArgs {
235        file: file.into(),
236        mates: mates
237            .into_iter()
238            .enumerate()
239            .map(|(i, mate)| (format!("m{}", i + 1), mate))
240            .collect(),
241        ..Default::default()
242    };
243    if fixed { args.fixed() } else { args }
244}
245
246/// The assembly of `chain`: three links (`link.geop`, [`link`]), the first
247/// fixed, each next one's near hole on the axis of the one before's far
248/// hole, lying on it — and nothing else: each turns about its pin.
249pub fn chain_assembly() -> Program {
250    let mut program = Program::new();
251    program.push("link1", placed("link.geop", true, Vec::new()));
252    for (before, link) in [("link1", "link2"), ("link2", "link3")] {
253        program.push(
254            link,
255            placed(
256                "link.geop",
257                false,
258                vec![pinned(link, 0, before, 1), on_top(link, before)],
259            ),
260        );
261    }
262    let pose = |x, z| StateValue::Pose(pose([x, 0.0, z], [0.0; 3]));
263    program.state = StateValues::from([
264        (pose_state("link1"), pose(0.0, 0.0)),
265        (pose_state("link2"), pose(3.0, 0.2)),
266        (pose_state("link3"), pose(6.0, 0.4)),
267    ]);
268    program
269}
270
271/// The circular edge of the hole `hole` (0 near, 1 far) of the [`link`]
272/// placed as `instance`, on its top (`end`) or bottom (`start`) face — and
273/// the frame it gives: at its center, its `z` along its axis, what a click
274/// on the rim of a hole picks.
275pub fn link_rim(instance: &str, hole: usize, cap: &str) -> MateEntity {
276    let name = format!(
277        "extrude(link,link_sketch,{},{cap})",
278        link_sketch(3.0).1[hole]
279    );
280    MateEntity::of(instance, Selected::Edge(element(name)))
281}
282
283/// The [`link`] placed as `link` turns on the one placed as `on`: a
284/// revolute joint from the frame on the rim of `on`'s far hole on its top
285/// to the frame on the rim of `link`'s near hole on its bottom — so it lies
286/// on it, pinned there — turning from `min` to `max` degrees.
287fn hinged(link: &str, on: &str, min: f64, max: f64) -> Mate {
288    Mate::joint(
289        JointKind::new(Joint::Revolute).limited(
290            Motion::Turn,
291            Limits {
292                min: Some(n(min)),
293                max: Some(n(max)),
294            },
295        ),
296        vec![link_rim(on, 1, "end"), link_rim(link, 0, "start")],
297    )
298}
299
300/// The assembly of `arm`: three links (`link.geop`, [`link`]) — the first
301/// fixed, each next one hinged on the far end of the one before by a
302/// revolute joint (see [`hinged`]), the elbow turning at most 150° either
303/// way, the wrist 120°. Its state sets the joints — 30° and -45° — and puts
304/// the links where those joints have them.
305pub fn arm_assembly() -> Program {
306    let mut program = Program::new();
307    program.push("upper", placed("link.geop", true, Vec::new()));
308    program.push(
309        "fore",
310        placed(
311            "link.geop",
312            false,
313            vec![hinged("fore", "upper", -150.0, 150.0)],
314        ),
315    );
316    program.push(
317        "hand",
318        placed(
319            "link.geop",
320            false,
321            vec![hinged("hand", "fore", -120.0, 120.0)],
322        ),
323    );
324    let (elbow, wrist) = (30.0_f64, -45.0_f64);
325    let turned = |angle: f64| {
326        [
327            3.0 * angle.to_radians().cos(),
328            3.0 * angle.to_radians().sin(),
329        ]
330    };
331    let fore = [3.0, 0.0];
332    let [dx, dy] = turned(elbow);
333    let hand = [fore[0] + dx, fore[1] + dy];
334    let at = |[x, y]: [f64; 2], z, turn| StateValue::Pose(pose([x, y, z], [0.0, 0.0, turn]));
335    program.state = StateValues::from([
336        (pose_state("upper"), at([0.0, 0.0], 0.0, 0.0)),
337        (pose_state("fore"), at(fore, 0.2, elbow)),
338        (pose_state("hand"), at(hand, 0.4, elbow + wrist)),
339        (
340            joint_parameter("add_part(fore,m1)", Motion::Turn),
341            StateValue::Number(n(elbow)),
342        ),
343        (
344            joint_parameter("add_part(hand,m1)", Motion::Turn),
345            StateValue::Number(n(wrist)),
346        ),
347    ]);
348    program
349}
350
351/// The assembly of `four_bar`: a four-bar linkage of [`bar`]s — the ground
352/// (4 long, fixed), the crank (1.5) and the rocker (3) each pinned to one end
353/// of it and lying on it, and the coupler (4) pinned to their far ends,
354/// lying on the crank. The crank is the shortest bar and
355/// `1.5 + 4 ≤ 4 + 3`, so it turns all the way round while the rocker rocks.
356pub fn four_bar_assembly() -> Program {
357    let mut program = Program::new();
358    program.push("ground", placed("ground.geop", true, Vec::new()));
359    program.push(
360        "crank",
361        placed(
362            "crank.geop",
363            false,
364            vec![pinned("crank", 0, "ground", 0), on_top("crank", "ground")],
365        ),
366    );
367    program.push(
368        "rocker",
369        placed(
370            "rocker.geop",
371            false,
372            vec![pinned("rocker", 0, "ground", 1), on_top("rocker", "ground")],
373        ),
374    );
375    program.push(
376        "coupler",
377        placed(
378            "coupler.geop",
379            false,
380            vec![
381                pinned("coupler", 0, "crank", 1),
382                pinned("coupler", 1, "rocker", 1),
383                on_top("coupler", "crank"),
384            ],
385        ),
386    );
387    // The crank at 60°, and where that puts the others (worked out from the
388    // bars' lengths; the mates hold them there exactly).
389    let pose = |position, turn| StateValue::Pose(pose(position, [0.0, 0.0, turn]));
390    program.state = StateValues::from([
391        (pose_state("ground"), pose([0.0, 0.0, 0.0], 0.0)),
392        (pose_state("crank"), pose([0.0, 0.0, 0.2], 60.0)),
393        (
394            pose_state("rocker"),
395            pose([4.0, 0.0, 0.2], 82.690_722_887_668_12),
396        ),
397        (
398            pose_state("coupler"),
399            pose([0.75, 1.299_038_105_676_658, 0.4], 24.780_674_143_948_414),
400        ),
401    ]);
402    program
403}
404
405/// Two of [`parametric_plate`], placed with other values: one as it is,
406/// one 5 wide with an M6 hole, in blue.
407pub fn plates_assembly() -> Program {
408    let mut program = Program::new();
409    program.push(
410        "small",
411        AddPartArgs {
412            file: "plate.geop".into(),
413            ..Default::default()
414        }
415        .fixed(),
416    );
417    program.push(
418        "large",
419        AddPartArgs {
420            file: "plate.geop".into(),
421            parameters: Settings::from([
422                ("width".to_string(), Setting::Number(5.0)),
423                ("screw".to_string(), Setting::Text("M6".into())),
424                ("color".to_string(), Setting::Text("#3e7bd0".into())),
425            ]),
426            ..Default::default()
427        }
428        .fixed(),
429    );
430    program.state = StateValues::from([
431        (
432            pose_state("small"),
433            StateValue::Pose(pose([0.0; 3], [0.0; 3])),
434        ),
435        (
436            pose_state("large"),
437            StateValue::Pose(pose([0.0, 3.0, 0.0], [0.0; 3])),
438        ),
439    ]);
440    program
441}
442
443/// A plate 40 x 40 x 5 with a 4.5 hole through it at `(20, 20)` — in
444/// millimetres, as the standard parts are: clearance for an M4 screw.
445pub fn metric_plate() -> Program {
446    let none = Program::new();
447    let corner = |x: &str, y: &str| [x.to_string(), y.to_string()];
448    let mut outline = Drawing::new(&none).expect("no parameters to resolve");
449    let mut hole = Drawing::new(&none).expect("no parameters to resolve");
450    outline
451        .polygon(&[
452            corner("0", "0"),
453            corner("40", "0"),
454            corner("40", "40"),
455            corner("0", "40"),
456        ])
457        .expect("the corners are numbers");
458    let centre = hole.point("20", "20").expect("the centre is numbers");
459    hole.circle(centre, "4.5")
460        .expect("the diameter is a number");
461    let mut program = Program::new();
462    program.push("outline", outline.on(outline_plane()));
463    program.push(
464        "plate",
465        ExtrudeArgs {
466            sketch: whole_sketch("outline"),
467            extent: Extents::blind(5.0),
468            output: Output::Solid(Combine::NewBody),
469            draft: 0.0,
470        },
471    );
472    program.push("hole_sketch", hole.on(outline_plane()));
473    program.push(
474        "hole",
475        ExtrudeArgs {
476            sketch: whole_sketch("hole_sketch"),
477            extent: Extents {
478                side1: Extent::ThroughAll,
479                ..Extents::blind(0.0)
480            },
481            output: Output::Solid(Combine::cut("extrude(plate)")),
482            draft: 0.0,
483        },
484    );
485    program
486}
487
488/// The name of the face of [`metric_plate`]'s hole.
489pub fn metric_plate_hole() -> String {
490    let plate = built(&metric_plate());
491    let part = read_model(&plate.current()).expect("the plate has a model");
492    PartDescription::of(&part)
493        .expect("the plate describes itself")
494        .faces
495        .into_keys()
496        .find(|name| name.starts_with("extrude(hole,hole_sketch,"))
497        .expect("the hole has a face")
498}
499
500/// [`metric_plate`] (`plate.geop`) bolted together from standard parts: an
501/// ISO 4762 M4 x 12 cap screw down its hole, and an ISO 4032 M4 nut under
502/// it — each mated by its datums `axis` and `seat` or `base`.
503pub fn bolted_plate() -> Program {
504    let mut program = Program::new();
505    program.push(
506        "plate",
507        AddPartArgs {
508            file: "plate.geop".into(),
509            ..Default::default()
510        }
511        .fixed(),
512    );
513    let mate = |kind, a: MateEntity, b: MateEntity| Mate::constraint(kind, vec![a, b]);
514    let datum_of = |part: &str, name: &str| MateEntity::of(part, Selected::Datum(datum(name)));
515    let size = |row: &str| Settings::from([("size".to_string(), Setting::Text(row.into()))]);
516    program.push(
517        "screw",
518        AddPartArgs {
519            file: "std:iso4762_socket_head_cap_screw.geop".into(),
520            parameters: size("M4x12"),
521            mates: BTreeMap::from([
522                (
523                    "m1".into(),
524                    mate(
525                        Kind::Concentric,
526                        datum_of("screw", "axis"),
527                        placed_face("plate", metric_plate_hole()),
528                    ),
529                ),
530                (
531                    "m2".into(),
532                    mate(
533                        Kind::Coincident,
534                        datum_of("screw", "seat"),
535                        placed_face("plate", "extrude(plate,end)"),
536                    ),
537                ),
538            ]),
539            mirror: None,
540        },
541    );
542    program.push(
543        "nut",
544        AddPartArgs {
545            file: "std:iso4032_hex_nut.geop".into(),
546            parameters: size("M4"),
547            mates: BTreeMap::from([
548                (
549                    "m1".into(),
550                    mate(
551                        Kind::Concentric,
552                        datum_of("nut", "axis"),
553                        datum_of("screw", "axis"),
554                    ),
555                ),
556                (
557                    "m2".into(),
558                    mate(
559                        Kind::Coincident,
560                        datum_of("nut", "base"),
561                        placed_face("plate", "extrude(plate,start)"),
562                    ),
563                ),
564            ]),
565            mirror: None,
566        },
567    );
568    program.state = StateValues::from([
569        (
570            pose_state("plate"),
571            StateValue::Pose(pose([0.0; 3], [0.0; 3])),
572        ),
573        (
574            pose_state("screw"),
575            StateValue::Pose(pose([20.0, 20.0, 5.0], [0.0; 3])),
576        ),
577        (
578            pose_state("nut"),
579            StateValue::Pose(pose([20.0, 20.0, 0.0], [180.0, 0.0, 0.0])),
580        ),
581    ]);
582    program
583}
584
585/// A cam: a disc of radius 2 whose center is 0.5 off the origin along `x`
586/// — so turned about the origin's `z` axis it is eccentric — 1 thick,
587/// half of it either side of the origin's `xy` plane.
588pub fn cam() -> Program {
589    let mut program = Program::new();
590    let mut sketch = Sketch::new();
591    circle(&mut sketch, [0.5, 0.0], 2.0);
592    program.push(
593        "cam_sketch",
594        AddSketchArgs::on(on_origin(FrameAxis::Z), solved(sketch)),
595    );
596    program.push(
597        "cam",
598        ExtrudeArgs {
599            sketch: whole_sketch("cam_sketch"),
600            extent: Extents {
601                symmetric: true,
602                ..Extents::blind(1.0)
603            },
604            output: Output::Solid(Combine::NewBody),
605            draft: 0.0,
606        },
607    );
608    program
609}
610
611/// The datum `name` of the origin of the part placed as `instance` — of
612/// the part itself, for none.
613fn origin_datum(instance: Option<&str>, name: String) -> MateEntity {
614    let datum = Selected::Datum(ElementReference::new(datums(), name));
615    match instance {
616        Some(instance) => MateEntity::of(instance, datum),
617        None => MateEntity::on(datum),
618    }
619}
620
621/// The assembly of `cam_follower`: the cam (`cam.geop`, [`cam`]) turning
622/// on a revolute joint about the origin's `z` axis, and the follower — a
623/// [`pin`] (`pin.geop`) — on a slider along the origin's `x` axis, the rim
624/// of its foot touching the cam's side (a tangent mate). A motion, `spin`,
625/// turns the cam once round in 4 s, the follower riding on it; and a move
626/// of an exploded view, `apart`, lifts the follower off the cam.
627pub fn cam_follower_assembly() -> Program {
628    let mut program = Program::new();
629    let frame = |instance| origin_datum(instance, ORIGIN.to_string());
630    let x_plane = |instance| {
631        let name = component_name(ORIGIN, DatumComponent::Plane(FrameAxis::X));
632        origin_datum(instance, name)
633    };
634    let spin = Mate::joint(
635        JointKind::new(Joint::Revolute),
636        vec![frame(None), frame(Some("cam"))],
637    );
638    program.push("cam", placed("cam.geop", false, vec![spin]));
639    let slide = Mate::joint(
640        JointKind::new(Joint::Slider),
641        vec![x_plane(None), x_plane(Some("follower"))],
642    );
643    let touch = Mate::constraint(
644        Kind::Tangent,
645        vec![
646            placed_face("cam", "extrude(cam,cam_sketch,c1)"),
647            MateEntity::of(
648                "follower",
649                Selected::Edge(element("extrude(pin,pin_sketch,c1,start)")),
650            ),
651        ],
652    );
653    program.push("follower", placed("pin.geop", false, vec![slide, touch]));
654    program.push(
655        "spin",
656        MotionArgs {
657            study: MotionStudy {
658                drivers: vec![joint_parameter("add_part(cam,m1)", Motion::Turn)],
659                keys: vec![vec![0.0, 0.0], vec![4.0, 360.0]],
660                frames: 25,
661            },
662        },
663    );
664    program.push(
665        "apart",
666        ExplodeArgs {
667            parts: vec!["follower".into()],
668            distance: 3.0,
669            ..Default::default()
670        },
671    );
672    // The cam at a turn of none, its far side 2.5 out along x, and the
673    // follower's axis its radius, 0.4, further.
674    let at = |x| StateValue::Pose(pose([x, 0.0, 0.0], [0.0; 3]));
675    program.state = StateValues::from([
676        (pose_state("cam"), at(0.0)),
677        (pose_state("follower"), at(2.9)),
678        (
679            joint_parameter("add_part(cam,m1)", Motion::Turn),
680            StateValue::Number(n(0.0)),
681        ),
682        (
683            joint_parameter("add_part(follower,m1)", Motion::Slide),
684            StateValue::Number(n(2.9)),
685        ),
686    ]);
687    program
688}
689
690/// An example drawn in the editor, kept as the program it saved (in
691/// `src/drawn/`): its sketches as they were drawn, which no code here says
692/// better. Its tests build it like every other example, so a change to the
693/// format that breaks it is found there: save it again from the editor.
694fn drawn(text: &str) -> Program {
695    Program::from_json(text).expect("a drawn example is a program")
696}
697
698/// The examples of assemblies, as the editor offers them: the parts they
699/// place that are examples of their own ([`pin`], [`link`]), and the
700/// examples made of several files ([`workspaces`]).
701pub fn set() -> ExampleSet {
702    ExampleSet {
703        programs: vec![("pin", pin), ("link", link)],
704        workspaces: workspaces(),
705    }
706}
707
708/// Every example made of several files, by name, each with what makes its
709/// files: listing them builds nothing.
710pub fn workspaces() -> Vec<Example<ExampleFiles>> {
711    vec![
712        ("press", || {
713            vec![
714                ("asm.geop", drawn(include_str!("drawn/press/asm.geop"))),
715                ("base.geop", drawn(include_str!("drawn/press/base.geop"))),
716                (
717                    "handle.geop",
718                    drawn(include_str!("drawn/press/handle.geop")),
719                ),
720                (
721                    "slider.geop",
722                    drawn(include_str!("drawn/press/slider.geop")),
723                ),
724                ("link.geop", drawn(include_str!("drawn/press/link.geop"))),
725            ]
726        }),
727        ("duck", || {
728            vec![
729                ("duck.geop", drawn(include_str!("drawn/duck/duck.geop"))),
730                (
731                    "left_leg.geop",
732                    drawn(include_str!("drawn/duck/left_leg.geop")),
733                ),
734                (
735                    "right_leg.geop",
736                    drawn(include_str!("drawn/duck/right_leg.geop")),
737                ),
738                (
739                    "left_hip.geop",
740                    drawn(include_str!("drawn/duck/left_hip.geop")),
741                ),
742                (
743                    "left_knee.geop",
744                    drawn(include_str!("drawn/duck/left_knee.geop")),
745                ),
746                (
747                    "left_ankle.geop",
748                    drawn(include_str!("drawn/duck/left_ankle.geop")),
749                ),
750                ("foot.geop", drawn(include_str!("drawn/duck/foot.geop"))),
751                ("neck.geop", drawn(include_str!("drawn/duck/neck.geop"))),
752                ("head.geop", drawn(include_str!("drawn/duck/head.geop"))),
753                ("mouth.geop", drawn(include_str!("drawn/duck/mouth.geop"))),
754            ]
755        }),
756        ("pin_in_plate", || {
757            vec![
758                ("assembly.geop", pin_in_plate_assembly()),
759                ("plate.geop", box_with_drill_hole()),
760                ("pin.geop", pin()),
761            ]
762        }),
763        ("chain", || {
764            vec![("chain.geop", chain_assembly()), ("link.geop", link())]
765        }),
766        ("parametric_plates", || {
767            vec![
768                ("plates.geop", plates_assembly()),
769                ("plate.geop", parametric_plate()),
770            ]
771        }),
772        ("four_bar", || {
773            vec![
774                ("four_bar.geop", four_bar_assembly()),
775                ("ground.geop", bar(4.0)),
776                ("crank.geop", bar(1.5)),
777                ("rocker.geop", bar(3.0)),
778                ("coupler.geop", bar(4.0)),
779            ]
780        }),
781        ("arm", || {
782            vec![("arm.geop", arm_assembly()), ("link.geop", link())]
783        }),
784        ("bolted_plate", || {
785            vec![
786                ("bolted_plate.geop", bolted_plate()),
787                ("plate.geop", metric_plate()),
788            ]
789        }),
790        ("cam_follower", || {
791            vec![
792                ("cam_follower.geop", cam_follower_assembly()),
793                ("cam.geop", cam()),
794                ("pin.geop", pin()),
795            ]
796        }),
797    ]
798}