Skip to main content

geop_ops_assembly/
editor.rs

1//! Editing a placed part: choosing its file, where it goes and whether it
2//! stays put, its mates — and dragging it, as far as its mates let it.
3//!
4//! Where the part is, is the program's state `<id>.pose`, which the editor
5//! solves the program's mates for after every edit. The step only says
6//! where it wants the part pulled ([`Form::pulls`]): while it is dragged,
7//! what point of it is pulled where; once its placement is typed in, three
8//! points of it pulled to where that puts them. It is dragged by a gizmo at
9//! its origin — moved along or turned about its own axes or the world's —
10//! or grabbed anywhere and dragged in the plane facing the eye. A part no
11//! mate may move goes where it is pulled.
12//!
13//! A joint is made between frames, one picked on each part: a click on a
14//! face, an edge or a point puts one there (see
15//! [`geop_ops_model::mate_entity::MateEntity::frame`]), and the hover shows the
16//! frame it would pick, as three arrows. A coupling is made of two joints
17//! already made, offered by what they join.
18//!
19//! A joint's coordinates are the program's state too: set in the dialog,
20//! they are held there while the step is edited ([`Pull::Value`]), and
21//! the parts move to them.
22
23use geop_core_editor::{
24    edit_form::{Edit, EditForm, FormState},
25    editable::Context,
26    editing::{
27        Action, CanvasEvent, Choice, Gizmo, ListItem, Number, Pull, Shape, Style, Tone, Unit,
28        Value, Visual,
29    },
30};
31use geop_core_math::primitives::FrameAxis;
32use geop_core_program::{
33    numeric::{ParameterValue, read_parameter_values},
34    values::{Setting, Settings, StateValue, pose_state},
35    workspace::is_program,
36};
37use std::collections::BTreeMap;
38
39use geop_core_math::{
40    primitives::{Pose, Quaternion},
41    scalars::Scalar,
42};
43use geop_ops_model::Namer;
44use geop_ops_model::parameters::{
45    COLOR, ParameterKind, file_choices, instance, parameters_of, read_properties, validate_color,
46};
47
48use crate::{
49    AddPartArgs, Anchor, CouplingKind, Joint, JointKind, Kind, Limits, Mate, MateKind, Motion,
50    Transmission,
51    add_part_derived::{plane_choices, plane_name},
52    joint_parameter,
53    mechanism::mate_holds,
54    solving::{SettingsState, check, settings},
55};
56
57/// The form of a step placing a part, and what setting its fields does.
58type PartForm<'a> = EditForm<'a, AddPartArgs, PartSession>;
59
60/// The key of the placed part, as drawn to be dragged.
61pub const PART: &str = "part";
62
63/// The kinds of mate, as added: by name, with a value to start from, what
64/// it does, and the group it is offered in.
65fn kinds() -> Vec<(&'static str, MateKind, &'static str, &'static str)> {
66    let constraint = |name, kind, doc| (name, MateKind::Constraint(kind), doc, "Add");
67    let coupling = |name, transmission, value, reverse, doc| {
68        let kind = CouplingKind::new(transmission, value, reverse);
69        (name, MateKind::Coupling(kind), doc, "Couple")
70    };
71    let joints = (Joint::ALL.into_iter()).map(|joint| {
72        (
73            joint.name(),
74            MateKind::Joint(JointKind::new(joint)),
75            joint.doc(),
76            "Joint",
77        )
78    });
79    let mut kinds = vec![
80        (
81            "fixed",
82            MateKind::Anchor(Anchor::Fixed),
83            "The part stays where it is: no mate moves it.",
84            "Add",
85        ),
86        constraint(
87            "coincident",
88            Kind::Coincident,
89            "Two points, a point and a line or a plane, two lines or two planes touch.",
90        ),
91        constraint(
92            "concentric",
93            Kind::Concentric,
94            "Two round edges or faces, or lines, share their axis.",
95        ),
96        constraint(
97            "parallel",
98            Kind::Parallel,
99            "Two lines or planes, or a line and a plane, run parallel.",
100        ),
101        constraint(
102            "perpendicular",
103            Kind::Perpendicular,
104            "Two lines or planes stand square, or a line along a plane's normal.",
105        ),
106        constraint(
107            "distance",
108            Kind::Distance { value: Scalar::ONE },
109            "Two points, a point and a line or plane, or parallel lines or planes, keep a distance.",
110        ),
111        constraint(
112            "angle",
113            Kind::Angle {
114                value: Scalar::from_i64(90),
115            },
116            "Two lines or planes meet at an angle, in degrees.",
117        ),
118        constraint(
119            "tangent",
120            Kind::Tangent,
121            "A circular edge, a ball's center or a point keeps touching a face, flat or curved: a cam and its follower's roller, a pin in a slot of any shape. Pick the face first.",
122        ),
123        constraint(
124            "path",
125            Kind::Path,
126            "A point — a vertex, a circle's center — runs along a curve: an edge or a sketch's curve. Pick the curve first.",
127        ),
128    ];
129    kinds.extend(joints);
130    kinds.extend([
131        coupling(
132            "gear",
133            Transmission::Gear,
134            Scalar::ONE,
135            true,
136            "Two joints that turn — two gears' hinges — turn together: the first `ratio` times per turn of the second. Add the two joints first.",
137        ),
138        coupling(
139            "belt",
140            Transmission::Belt,
141            Scalar::ONE,
142            false,
143            "Two joints that turn — two pulleys' hinges, a belt or a chain round them — turn the same way: the first `ratio` times per turn of the second. Add the two joints first.",
144        ),
145        coupling(
146            "rack_pinion",
147            Transmission::RackPinion,
148            Scalar::ONE,
149            false,
150            "A joint that slides — a rack — moves as far as a joint that turns — its pinion — rolls its pitch circle. Add the two joints first.",
151        ),
152        coupling(
153            "screw",
154            Transmission::Screw,
155            Scalar::from_ratio(1, 10).expect("ten is not zero"),
156            false,
157            "A joint that slides moves its lead per turn of a joint that turns — of one cylindrical joint, say. Add the joints first.",
158        ),
159        coupling(
160            "linear",
161            Transmission::Linear,
162            Scalar::ONE,
163            false,
164            "Two joints that slide move together: the second `ratio` times as far as the first. Add the two joints first.",
165        ),
166    ]);
167    kinds
168}
169
170/// What the editor keeps while a placed part is edited.
171#[derive(Clone, Debug, Default)]
172pub struct PartSession {
173    /// The mate whose entities are picked.
174    pub selected: Option<String>,
175    /// What points of the part are pulled where: while it is dragged — the
176    /// point grabbed kept from the press on, so every event of the drag is
177    /// measured from it; for a turn by the gizmo, three points of its frame
178    /// — or after its placement was typed in.
179    pulls: Vec<Pull>,
180    /// Whether the pulls are done with: they pull once more — for the
181    /// editor to solve with where they were let go — and are gone at the
182    /// next event.
183    released: bool,
184    /// The joint coordinates set in the dialog, by state: kept where they
185    /// were set while the step is edited, until the part is dragged.
186    held: BTreeMap<String, f64>,
187}
188
189impl PartSession {
190    /// Lets go of the pulls released at the event before: they pulled once
191    /// more, and are gone at this one (see
192    /// [`geop_core_editor::part_operation::PartOperation::begin_event`]).
193    pub(crate) fn let_go(&mut self) {
194        if std::mem::take(&mut self.released) {
195            self.pulls.clear();
196        }
197    }
198}
199
200/// What a step placing a part keeps while it is edited.
201pub type PartEditing = FormState<PartSession>;
202
203/// Where the part the step places is: the program's state, or — before
204/// anything was solved for it — the origin.
205fn pose_of(context: &Context) -> Pose {
206    match context.before.retrieve_state(&pose_state(context.step)) {
207        StateValue::Pose(pose) => pose,
208        StateValue::Number(_) => Pose::identity(),
209    }
210}
211
212/// The part placed as `step` pulled to `target` as a whole.
213fn pulled_to(step: &str, target: Pose) -> Vec<Pull> {
214    vec![Pull::Pose {
215        parameter: pose_state(step),
216        target: target.map(|c| c.sharpen()),
217    }]
218}
219
220/// The value a kind of mate holds, if any — as the dialog shows it.
221fn value_of(kind: &MateKind) -> Option<f64> {
222    match *kind {
223        MateKind::Constraint(Kind::Distance { value } | Kind::Angle { value })
224        | MateKind::Coupling(CouplingKind { value, .. }) => Some(value.to_f64()),
225        _ => None,
226    }
227}
228
229/// Sets the joint coordinate `parameter` to `value` and keeps it there
230/// while the step is edited: the parts move to it (see [`Pull::Value`]).
231fn set_joint(session: &mut PartSession, parameter: &str, value: f64) {
232    session.held.insert(parameter.to_string(), value);
233}
234
235/// The value a kind of mate holds is `v` now.
236fn set_value(kind: &mut MateKind, v: f64) {
237    let v = Scalar::from_f64(v);
238    match kind {
239        MateKind::Constraint(Kind::Distance { value } | Kind::Angle { value })
240        | MateKind::Coupling(CouplingKind { value, .. }) => *value = v,
241        _ => {}
242    }
243}
244
245/// The fields of the mate `id` selected: the entities to pick for a
246/// constraint or a joint; a joint's coordinates — set, they hold — and its
247/// limits; the joints a coupling ties, and how.
248fn selected<'a>(
249    f: &mut PartForm<'a>,
250    reader: &geop_core_program::reader::Reader<'a>,
251    id: &str,
252    mate: &Mate,
253    joint: Option<&SettingsState>,
254    joints: &[SettingsState],
255) {
256    if let MateKind::Coupling(kind) = mate.kind {
257        // The joints' fields, the value's — its name and unit — as each
258        // transmission reads.
259        let (labels, value_name, unit) = match kind.transmission {
260            Transmission::Gear | Transmission::Belt => {
261                (["driving joint", "driven joint"], "ratio", Unit::Fraction)
262            }
263            Transmission::Linear => (["driving joint", "driven joint"], "ratio", Unit::Fraction),
264            Transmission::RackPinion => (
265                ["pinion's joint", "rack's joint"],
266                "pitch radius",
267                Unit::Length,
268            ),
269            Transmission::Screw => (["turning joint", "sliding joint"], "lead", Unit::Length),
270        };
271        // What each of the two joints has to do: turn or slide.
272        let motions = kind.transmission.motions();
273        for (k, label) in labels.into_iter().enumerate() {
274            let moves =
275                |j: &&SettingsState| j.values.iter().any(|v| v.measure == motions[k].measure());
276            let options: Vec<Choice> = std::iter::once(Choice::new("", "Choose a joint…"))
277                .chain(joints.iter().filter(moves).map(|j| {
278                    let what = if j.between.is_empty() {
279                        j.name.clone()
280                    } else {
281                        format!("{} — {}", j.name, j.between.join(" ↔ "))
282                    };
283                    Choice::new(j.name.clone(), format!("{} · {what}", j.kind))
284                }))
285                .collect();
286            if options.len() == 1 {
287                f.form.dialog.text(
288                    &format!("mate:{id}:no_joint{k}"),
289                    format!(
290                        "No joint {}s yet: add one first — Joint, then {} — to the parts placed so far.",
291                        motions[k].verb(),
292                        (Joint::ALL.iter())
293                            .filter(|j| JointKind::new(**j).moves(motions[k]))
294                            .map(|j| j.label())
295                            .collect::<Vec<_>>()
296                            .join(" or "),
297                    ),
298                    Tone::Error,
299                );
300            }
301            let mate_id = id.to_string();
302            let value = mate.joints.get(k).cloned().unwrap_or_default();
303            f.select(
304                &format!("mate:{id}:joint{k}"),
305                label,
306                value,
307                options,
308                true,
309                move |Edit { args, .. }, joint| {
310                    if let Some(mate) = args.mates.get_mut(&mate_id) {
311                        mate.joints.resize(2, String::new());
312                        mate.joints[k] = joint.to_string();
313                        if mate.joints.iter().all(String::is_empty) {
314                            mate.joints.clear();
315                        }
316                    }
317                },
318            );
319        }
320        let mate_id = id.to_string();
321        f.number(
322            &format!("mate:{id}:value"),
323            Number::new(value_name, kind.value.to_f64(), unit),
324            move |Edit { args, .. }, v| {
325                if let Some(mate) = args.mates.get_mut(&mate_id) {
326                    set_value(&mut mate.kind, v);
327                }
328            },
329        );
330        let mate_id = id.to_string();
331        f.checkbox(
332            &format!("mate:{id}:reverse"),
333            "the other way round",
334            kind.reverse,
335            move |Edit { args, .. }, r| {
336                if let Some(MateKind::Coupling(CouplingKind { reverse, .. })) =
337                    args.mates.get_mut(&mate_id).map(|m| &mut m.kind)
338                {
339                    *reverse = r;
340                }
341            },
342        );
343        // Where the driven joint is when the driving one is at zero.
344        let mate_id = id.to_string();
345        let driven = kind.transmission.motions()[1];
346        let unit = match driven {
347            Motion::Turn => Unit::Angle,
348            Motion::Slide | Motion::SlideX | Motion::SlideY => Unit::Length,
349        };
350        let offset = kind.offset.map_or(0.0, |o| o.to_f64());
351        f.number(
352            &format!("mate:{id}:offset"),
353            Number::new("offset", offset, unit),
354            move |Edit { args, .. }, v| {
355                if let Some(MateKind::Coupling(CouplingKind { offset, .. })) =
356                    args.mates.get_mut(&mate_id).map(|m| &mut m.kind)
357                {
358                    *offset = (v != 0.0).then(|| Scalar::from_f64(v));
359                }
360            },
361        );
362        f.form.dialog.text(
363            "mate_hint",
364            format!(
365                "Choose {} from the joints of the parts placed so far, here or in an earlier step.",
366                mate.needs()
367            ),
368            Tone::Hint,
369        );
370        return;
371    }
372
373    if mate.is_fixed() {
374        f.form.dialog.text(
375            "mate_hint",
376            "No mate moves this part: it goes where it is dragged.",
377            Tone::Hint,
378        );
379        return;
380    }
381
382    let mate_id = id.to_string();
383    let label = if matches!(mate.kind, MateKind::Joint(_)) {
384        "frames"
385    } else {
386        "elements"
387    };
388    f.pick(
389        &format!("mate:{id}:entities"),
390        label,
391        reader,
392        &mate.entities,
393        true,
394        move |edit, mut entities| {
395            // Two at most: a third pick replaces the oldest.
396            let extra = entities.len().saturating_sub(2);
397            entities.drain(..extra);
398            let complete = entities.len() == 2;
399            if let Some(mate) = edit.args.mates.get_mut(&mate_id) {
400                mate.entities = entities;
401            }
402            if complete {
403                edit.session.selected = None;
404            }
405        },
406    );
407    let hint = match mate.kind {
408        MateKind::Joint(_) => {
409            "Click a face, an edge, a point or a datum on each of the two parts: where you click, a \
410             frame is put, and the joint turns or slides along its z axis, the blue arrow. A planar \
411             face has it at its middle, or at the corner or side you are near, along its normal; a \
412             cylinder, on its axis; a circular edge, at its center, along its axis; a straight edge, \
413             at its middle, or at the end you are near, along it; a point, along the world's z. \
414             The first part holds, the second moves."
415                .to_string()
416        }
417        MateKind::Constraint(kind) if kind.needs(0).is_some() => format!(
418            "Pick {} — of the placed part, of a part placed before, or of this part itself.",
419            mate.needs()
420        ),
421        _ => format!(
422            "Pick two entities, each {} — of the placed part, of a part placed before, or of this part itself.",
423            mate.needs()
424        ),
425    };
426    f.form.dialog.text("mate_hint", hint, Tone::Hint);
427
428    let MateKind::Joint(kind) = mate.kind else {
429        return;
430    };
431    for motion in kind.motions() {
432        let unit = match motion {
433            Motion::Turn => Unit::Angle,
434            Motion::Slide | Motion::SlideX | Motion::SlideY => Unit::Length,
435        };
436        let value = joint
437            .and_then(|j| (j.values.iter()).find(|v| v.state == joint_parameter(&j.name, motion)));
438        if let Some(value) = value {
439            let key = format!("mate:{id}:{}", motion.name());
440            let parameter = value.state.clone();
441            let number = Number::new(motion.name(), value.value, unit);
442            f.number(&key, number, move |edit, v| {
443                set_joint(edit.session, &parameter, v);
444            });
445        }
446        let [min, max] = kind.limits(motion);
447        let (mate_id, id_set) = (id.to_string(), id.to_string());
448        // Limited to either side of where it is now, to start with.
449        let here = value.map_or(0.0, |v| v.value);
450        let reach = match motion {
451            Motion::Turn => 90.0,
452            Motion::Slide | Motion::SlideX | Motion::SlideY => 1.0,
453        };
454        f.checkbox(
455            &format!("mate:{id}:limited:{}", motion.name()),
456            format!("limit the {}", motion.name()),
457            min.is_some() || max.is_some(),
458            move |Edit { args, .. }, on| {
459                if let Some(limits) = limits_of(args, &id_set, motion) {
460                    *limits = match on {
461                        true => Limits::between(
462                            Scalar::from_f64(here - reach),
463                            Scalar::from_f64(here + reach),
464                        ),
465                        false => Limits::NONE,
466                    };
467                }
468            },
469        );
470        for (k, (bound, label)) in [(min, "min"), (max, "max")].into_iter().enumerate() {
471            let Some(bound) = bound else {
472                continue;
473            };
474            let mate_id = mate_id.clone();
475            f.number(
476                &format!("mate:{id}:{label}:{}", motion.name()),
477                Number::new(format!("{label} {}", motion.name()), bound.to_f64(), unit),
478                move |Edit { args, .. }, v| {
479                    if let Some(limits) = limits_of(args, &mate_id, motion) {
480                        *[&mut limits.min, &mut limits.max][k] = Some(Scalar::from_f64(v));
481                    }
482                },
483            );
484        }
485    }
486}
487
488/// The limits of the coordinate of `motion` of the joint `mate` of `args`,
489/// to set — none if it is no joint.
490fn limits_of<'a>(args: &'a mut AddPartArgs, mate: &str, motion: Motion) -> Option<&'a mut Limits> {
491    match args.mates.get_mut(mate) {
492        Some(Mate {
493            kind: MateKind::Joint(kind),
494            ..
495        }) => Some(&mut kind.limits[motion.index()]),
496        _ => None,
497    }
498}
499
500/// The lowest `m1`, `m2`, ... not yet a mate's id.
501fn fresh_mate_id(mates: &BTreeMap<String, Mate>) -> String {
502    (1..)
503        .map(|n| format!("m{n}"))
504        .find(|id| !mates.contains_key(id))
505        .expect("some number is free")
506}
507
508/// The key of the field of the placed part's parameter `name`.
509fn parameter_key(name: &str) -> String {
510    format!("parameter:{name}")
511}
512
513/// Whether the part is placed mirrored, and in which plane of its own
514/// origin: the other hand of it.
515fn mirror(f: &mut PartForm, args: &AddPartArgs) {
516    if args.file.is_empty() {
517        return;
518    }
519    let chosen = args.mirror.map_or("", plane_name);
520    let options = (std::iter::once(Choice::new("", "Not mirrored")))
521        .chain(plane_choices())
522        .collect();
523    f.select(
524        "mirror",
525        "Mirrored",
526        chosen,
527        options,
528        false,
529        |Edit { args, .. }, plane| {
530            args.mirror = FrameAxis::ALL.into_iter().find(|n| plane_name(*n) == plane);
531        },
532    );
533}
534
535/// The placed part's own parameters, each as what it is given here: its
536/// colour picked, a number on a slider, a table's row chosen from a list
537/// to search.
538fn parameters(f: &mut PartForm, context: &Context, args: &AddPartArgs) {
539    if args.file.is_empty() {
540        return;
541    }
542    let Ok((program, _)) = context.runner.program(&args.file) else {
543        return;
544    };
545    let defined = parameters_of(&program);
546    if defined.is_empty() {
547        return;
548    }
549    // What each comes to as the step placed it, set or not.
550    let placed = (context.built.as_ref()).and_then(|built| built.placed_part(context.step).ok());
551    let mut chosen = Settings::new();
552    if let Some(placed) = &placed {
553        for (name, value) in &read_parameter_values(placed).unwrap_or_default().0 {
554            let value = match value {
555                ParameterValue::Number(n) => Setting::Number(*n),
556                ParameterValue::Text(name) => Setting::Text(name.clone()),
557            };
558            chosen.insert(name.clone(), value);
559        }
560        if let Some(color) = read_properties(placed).ok().and_then(|p| p.color.clone()) {
561            chosen.insert(COLOR.into(), Setting::Text(color));
562        }
563    }
564    let given = |name: &str| (args.parameters.get(name)).or(chosen.get(name)).cloned();
565    f.form.dialog.heading("parameters_heading", "Parameters");
566    let set = |name: String| {
567        move |args: &mut AddPartArgs, value: Setting| {
568            args.parameters.insert(name.clone(), value);
569        }
570    };
571    if defined.color.is_some() || args.parameters.contains_key(COLOR) {
572        let color = match given(COLOR) {
573            Some(Setting::Text(c)) => c,
574            _ => "#a0a8b8".into(),
575        };
576        let set = set(COLOR.into());
577        f.color(
578            &parameter_key(COLOR),
579            COLOR,
580            color,
581            move |Edit { args, .. }, c| {
582                if validate_color(c).is_ok() {
583                    set(args, Setting::Text(c.to_string()));
584                }
585            },
586        );
587    }
588    for p in &defined.values {
589        let key = parameter_key(&p.name);
590        let set = set(p.name.clone());
591        match &p.kind {
592            ParameterKind::Number { min, max, .. } => {
593                let value = match given(&p.name) {
594                    Some(Setting::Number(v)) => v,
595                    _ => continue,
596                };
597                // A slider over what the parameter offers — or, with no
598                // range of its own, from nothing to twice its value.
599                let lo = min.unwrap_or(0.0_f64.min(2.0 * value));
600                let hi = max.unwrap_or(0.0_f64.max(2.0 * value)).max(lo + 1e-9);
601                let number = Number::new(&p.name, value, Unit::Length).range(lo, hi);
602                f.number(&key, number, move |Edit { args, .. }, v| {
603                    set(args, Setting::Number(v));
604                });
605            }
606            ParameterKind::Table { rows, selected, .. } => {
607                let value = match given(&p.name) {
608                    Some(Setting::Text(row)) => row,
609                    _ => selected.clone(),
610                };
611                let options = (rows.iter())
612                    .map(|r| Choice::new(r.name.clone(), r.name.clone()))
613                    .collect();
614                f.select(
615                    &key,
616                    &p.name,
617                    value,
618                    options,
619                    true,
620                    move |Edit { args, .. }, row| {
621                        set(args, Setting::Text(row.to_string()));
622                    },
623                );
624            }
625        }
626    }
627}
628
629/// The file to place, whether it stays put, where it goes, its mates — the
630/// one selected with its entities to pick — and the part as placed, to
631/// drag.
632pub(crate) fn form<'a>(
633    context: &'a Context<'a>,
634    args: &AddPartArgs,
635    session: &PartSession,
636) -> PartForm<'a> {
637    let mut f = PartForm::default();
638    let step = context.step.to_string();
639    let built = (context.built.as_ref()).filter(|view| instance(view, &step).is_ok());
640    let files = file_choices(context.runner, is_program);
641    if !files.iter().any(|file| file.accepted) {
642        f.form.dialog.text(
643            "no_files",
644            "There are no other program files to place: make one first.",
645            Tone::Hint,
646        );
647    }
648    f.file(
649        "file",
650        "file",
651        args.file.clone(),
652        files,
653        &[],
654        |Edit { args, .. }, file| {
655            args.file = file.to_string();
656        },
657    );
658
659    parameters(&mut f, context, args);
660    mirror(&mut f, args);
661
662    // Shown as a position and angles — what people read — though a pose is
663    // a quaternion: setting one angle rebuilds it from the three shown.
664    f.form.dialog.heading("placement_heading", "Placement");
665    let pose = pose_of(context);
666    let (position, angles) = (pose.position(), pose.euler_degrees());
667    for (k, axis) in ["x", "y", "z"].into_iter().enumerate() {
668        let number = Number::new(axis, position[k].to_f64(), Unit::Length);
669        let step = step.clone();
670        f.number(axis, number, move |edit, v| {
671            let mut position = position;
672            position[k] = Scalar::from_f64(v);
673            edit.session.pulls = pulled_to(&step, pose.with_position(position));
674            edit.session.released = true;
675        });
676    }
677    for (k, axis) in ["x", "y", "z"].into_iter().enumerate() {
678        let key = format!("rotation_{axis}");
679        let number = Number::new(format!("rotation {axis}"), angles[k], Unit::Angle);
680        let step = step.clone();
681        f.number(&key, number, move |edit, v| {
682            let mut angles = angles;
683            angles[k] = v;
684            if let Ok(turned) = Pose::from_euler(position, angles.map(Scalar::from_f64)) {
685                edit.session.pulls = pulled_to(&step, turned);
686                edit.session.released = true;
687            }
688        });
689    }
690
691    f.form.dialog.heading("mates_heading", "Mates");
692    // Its own mates — named after the step — not every mate of the part.
693    let namer = |id: &str| match Namer::new("add_part", &step) {
694        Ok(namer) => namer.name(&[id]),
695        Err(_) => String::new(),
696    };
697    // Every joint of the part as built, its coordinates where they are.
698    let joints = built.map(settings).unwrap_or_default();
699    let items = (args.mates.iter())
700        .map(|(id, mate)| {
701            let key = format!("mate:{id}");
702            let mate_id = id.clone();
703            let joint = joints.iter().find(|j| j.name == namer(id)).cloned();
704            let first = joint.as_ref().and_then(|j| j.values.first().cloned());
705            let parameter = first.as_ref().map(|v| v.state.clone());
706            f.on(key.clone(), move |edit, value| {
707                let args = &mut *edit.args;
708                match value {
709                    Value::Press => {
710                        edit.session.selected = (edit.session.selected.as_deref()
711                            != Some(&mate_id))
712                        .then(|| mate_id.clone());
713                    }
714                    Value::Remove => {
715                        args.mates.remove(&mate_id);
716                        if edit.session.selected.as_deref() == Some(&mate_id) {
717                            edit.session.selected = None;
718                        }
719                    }
720                    Value::Number(v) => match (args.mates.get_mut(&mate_id), &parameter) {
721                        (Some(mate), None) => set_value(&mut mate.kind, v),
722                        (Some(_), Some(parameter)) => set_joint(edit.session, parameter, v),
723                        (None, _) => {}
724                    },
725                    _ => {}
726                }
727            });
728            let mut item = ListItem::new(key, mate.kind.label());
729            // A fixed mate holds this step's part: it needs no entity, and
730            // puts no constraint on where it is.
731            let complete = mate.is_complete() || mate.is_fixed();
732            let holds = mate.is_fixed() || built.is_some_and(|view| mate_holds(view, &namer(id)));
733            // Two elements of one part keep where they are to each other
734            // whatever moves: such a mate holds nothing together.
735            let one_part = mate.pair().and_then(|[a, b]| {
736                (!a.part().is_empty() && a.part() == b.part()).then(|| a.part().to_string())
737            });
738            let what = match mate.kind {
739                MateKind::Anchor(_) => "holds this part where it is".to_string(),
740                MateKind::Coupling(_) => mate.joints.join(" & "),
741                _ => {
742                    let entities: Vec<String> = mate.entities.iter().map(|e| e.label()).collect();
743                    entities.join(" & ")
744                }
745            };
746            item.detail = Some(match (complete, &one_part, holds) {
747                (false, _, _) => match mate.kind {
748                    MateKind::Coupling(_) => format!("choose {}", mate.needs()),
749                    _ => format!("pick {} — {}", 2 - mate.entities.len().min(2), mate.needs()),
750                },
751                (true, Some(part), _) => {
752                    format!("{what} — both of {part}: holds nothing together")
753                }
754                (true, None, true) => what,
755                (true, None, false) => format!("{what} — cannot hold"),
756            });
757            let holds = holds && one_part.is_none();
758            item.tone = match (complete, holds) {
759                (false, _) => Tone::Hint,
760                (true, true) => Tone::Normal,
761                (true, false) => Tone::Error,
762            };
763            item.selected = session.selected.as_deref() == Some(id);
764            item.removable = true;
765            item.value = (first.map(|v| v.value)).or_else(|| value_of(&mate.kind));
766            item
767        })
768        .collect();
769    f.form.dialog.list("mates", items, "None yet.");
770    f.actions(
771        "add_mate",
772        (kinds().iter())
773            .map(|(name, kind, doc, group)| {
774                Action::new(*name, kind.label()).title(*doc).group(*group)
775            })
776            .collect(),
777        |edit, name| {
778            if let Some((_, kind, _, _)) = kinds().iter().find(|(n, ..)| *n == name) {
779                // A part is held where it is or by its mates, not both: a
780                // joint or a constraint chosen for it holds it, and the
781                // part held fixed — the first placed is, by default —
782                // could move on none of it.
783                if matches!(kind, MateKind::Joint(_) | MateKind::Constraint(_)) {
784                    edit.args.set_fixed(false);
785                }
786                let id = fresh_mate_id(&edit.args.mates);
787                let mate = Mate {
788                    kind: *kind,
789                    entities: Vec::new(),
790                    joints: Vec::new(),
791                };
792                edit.args.mates.insert(id.clone(), mate);
793                edit.session.selected = Some(id);
794            }
795        },
796    );
797
798    if let Some((id, mate)) =
799        (session.selected.as_ref()).and_then(|id| args.mates.get_key_value(id))
800    {
801        let joint = joints.iter().find(|j| j.name == namer(id));
802        // Its entities are of the part placed, of one placed before or of
803        // the part itself: all in the part as the step built it.
804        selected(
805            &mut f,
806            built.unwrap_or(&context.before),
807            id,
808            mate,
809            joint,
810            &joints,
811        );
812    }
813
814    if let Some(view) = built
815        && let Ok(report) = check(view)
816        && !report.failed.is_empty()
817    {
818        f.form.dialog.text(
819            "conflict",
820            format!("These mates do not hold: {}.", report.failed.join(", ")),
821            Tone::Error,
822        );
823    }
824    f.form.pulls.extend(session.pulls.iter().cloned());
825    f.form
826        .pulls
827        .extend(session.held.iter().map(|(parameter, value)| Pull::Value {
828            parameter: parameter.clone(),
829            value: Scalar::from_f64(*value),
830        }));
831    if built.is_some() {
832        let shape = Shape::Instance { name: step.clone() };
833        f.form
834            .visuals
835            .push(Visual::new(PART, shape, Style::Free).draggable());
836        if let Ok(axes) = pose.rotation().rotation_columns()
837            && let (Ok(x), Ok(y), Ok(z)) = (
838                axes[0].normalize(),
839                axes[1].normalize(),
840                axes[2].normalize(),
841            )
842        {
843            f.form.gizmo = Some(Gizmo::new(position).translate().rotate().local([x, y, z]));
844        }
845        f.form.dialog.text(
846            "drag_hint",
847            "Drag the part, or the gizmo at its origin, to move it as far as its mates let it.",
848            Tone::Hint,
849        );
850    }
851    f
852}
853
854/// A pointer or key event: the part dragged (see the module docs); Delete
855/// removes the mate selected, Escape stops editing it.
856pub(crate) fn canvas(
857    context: &Context,
858    edit: Edit<'_, AddPartArgs, PartSession>,
859    event: &CanvasEvent,
860) {
861    let Edit { args, session, .. } = edit;
862    match event {
863        CanvasEvent::Move {
864            key,
865            from,
866            to,
867            done,
868            ..
869        } if key == PART => {
870            let pose = pose_of(context);
871            // Where the pointer is: a free choice, made by whoever moved it.
872            let target = to.sharpen();
873            let local = match session.pulls.first() {
874                Some(Pull::Point { local, .. }) => *local,
875                _ => pose.inverse().apply(from).sharpen(),
876            };
877            // A drag moves the joints set in the dialog too.
878            session.held.clear();
879            session.pulls = vec![Pull::Point {
880                parameter: pose_state(context.step),
881                local,
882                target,
883            }];
884            session.released = *done;
885        }
886        // Moved or turned by the gizmo, from where it stood when the drag
887        // started: at the part's origin, along its own axes — where the
888        // part was.
889        CanvasEvent::Gizmo { drag, from, done } => {
890            let (Some(motion), Some(axes)) = (drag.turn(), from.axes) else {
891                return;
892            };
893            let Ok(start) = Quaternion::from_rotation_columns(axes)
894                .and_then(|rotation| Pose::new(from.at, rotation))
895            else {
896                return;
897            };
898            session.held.clear();
899            session.pulls = pulled_to(context.step, motion.compose(&start));
900            session.released = *done;
901        }
902        CanvasEvent::Cancel => session.selected = None,
903        CanvasEvent::Delete => {
904            if let Some(id) = session.selected.take() {
905                args.mates.remove(&id);
906            }
907        }
908        _ => {}
909    }
910}