1use 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
57type PartForm<'a> = EditForm<'a, AddPartArgs, PartSession>;
59
60pub const PART: &str = "part";
62
63fn 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#[derive(Clone, Debug, Default)]
172pub struct PartSession {
173 pub selected: Option<String>,
175 pulls: Vec<Pull>,
180 released: bool,
184 held: BTreeMap<String, f64>,
187}
188
189impl PartSession {
190 pub(crate) fn let_go(&mut self) {
194 if std::mem::take(&mut self.released) {
195 self.pulls.clear();
196 }
197 }
198}
199
200pub type PartEditing = FormState<PartSession>;
202
203fn 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
212fn 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
220fn 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
229fn set_joint(session: &mut PartSession, parameter: &str, value: f64) {
232 session.held.insert(parameter.to_string(), value);
233}
234
235fn 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
245fn 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 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 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 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 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, ¶meter, v);
444 });
445 }
446 let [min, max] = kind.limits(motion);
447 let (mate_id, id_set) = (id.to_string(), id.to_string());
448 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
488fn 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
500fn 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
508fn parameter_key(name: &str) -> String {
510 format!("parameter:{name}")
511}
512
513fn 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
535fn 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 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 ¶meter_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 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
629pub(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 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 let namer = |id: &str| match Namer::new("add_part", &step) {
694 Ok(namer) => namer.name(&[id]),
695 Err(_) => String::new(),
696 };
697 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), ¶meter) {
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 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 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 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 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
854pub(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 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 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 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}