Skip to main content

geop_ops_assembly/
motion.rs

1//! Motion studies: joint coordinates driven by a time table — a step,
2//! [`Drive`], whose study ([`MotionStudy`]) the model's vocabulary holds —
3//! played and checked for collisions in the motion panel
4//! ([`MotionPanel`]), and written frame by frame ([`MotionExport`]).
5//!
6//! A motion is played by solving: at each time its drivers' values are
7//! set, as the joints panel sets a coordinate, and everything else that is
8//! free gives way (see [`solving::set`]). Each frame starts from the one
9//! before, so a linkage keeps the branch it is on.
10
11use std::{cell::RefCell, collections::BTreeMap};
12
13use geop_core_editor::{
14    edit_form::{Edit, EditForm, FormState},
15    editable::{Context, OperationGroup, OperationInfo, OperationTier},
16    editing::{
17        Cell, Choice, Column, ColumnKind, ListItem, Number, Pull, Table, TableEdit, TableRow, Tone,
18        Unit, Value,
19    },
20    editor::{Changed, EditorContext, Rerun},
21    output::{Content, Export, Exporter, ExporterInfo, Output},
22    part_operation,
23    part_operation::PartOperation,
24    registry::Panel,
25};
26use geop_core_math::{
27    geop_error::{GeopError, GeopResult, WithContext},
28    scalars::Scalar,
29    with_context,
30};
31use geop_core_program::{part::StepContext, program::Program, reader::Reader, values::StateValues};
32use geop_ops_model::{
33    parameters::stem,
34    placed::{
35        motion::{MotionStudy, motion_studies, motion_study},
36        placed_parts,
37    },
38};
39use serde::{Deserialize, Serialize};
40
41use crate::solving::{self, Measure, SettingsState, settings};
42
43/// Drives joint coordinates by a time table: a motion study — see the
44/// module docs.
45#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
46pub struct Drive;
47
48/// A motion study, as its step holds it.
49#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
50pub struct MotionArgs {
51    #[serde(flatten)]
52    pub study: MotionStudy,
53}
54
55/// The joint coordinates of the part `reader` reads a motion can drive,
56/// each with what it is called: `fore m1 angle`.
57fn coordinates(reader: &Reader) -> Vec<(String, String, Measure, SettingsState)> {
58    (settings(reader).into_iter())
59        .flat_map(|joint| {
60            (joint.values.clone().into_iter()).map(move |value| {
61                let coordinate = value.state.rsplit('.').next().unwrap_or_default();
62                let label = format!("{} {coordinate}", joint.label);
63                (value.state.clone(), label, value.measure, joint.clone())
64            })
65        })
66        .collect()
67}
68
69/// The study `args` holds, checked against the part `reader` reads: every
70/// driver one of its joint coordinates. The generator of its target.
71fn study(reader: &Reader, args: &MotionArgs) -> GeopResult<MotionStudy> {
72    args.study.validate()?;
73    let known = coordinates(reader);
74    for driver in &args.study.drivers {
75        if !known.iter().any(|(state, ..)| state == driver) {
76            let names: Vec<&str> = known.iter().map(|(state, ..)| state.as_str()).collect();
77            return Err(GeopError::new(format!(
78                "{driver:?} is no joint coordinate of this part: a motion drives one of {}",
79                match names.is_empty() {
80                    true => "its joints' coordinates, and it has no joint yet".to_string(),
81                    false => names.join(", "),
82                }
83            )));
84        }
85    }
86    Ok(args.study.clone())
87}
88
89/// What the dialog keeps while a motion is edited: the time it is shown
90/// at, if one was chosen.
91#[derive(Clone, Debug, Default)]
92pub struct MotionSession {
93    preview: Option<f64>,
94}
95
96part_operation!(Drive(MotionArgs));
97
98impl PartOperation for Drive {
99    const KIND: &'static str = "motion";
100    const INFO: OperationInfo = OperationInfo::new(
101        "Motion",
102        "Drive joints by a time table, to play in the motion panel, check for collisions and export frame by frame.",
103        OperationGroup::Assembly,
104        OperationTier::Small,
105    )
106    .icon(crate::icons::MOTION);
107    type Args = MotionArgs;
108    type Session = MotionSession;
109
110    /// The first joint coordinate of the part driven from where it is: an
111    /// angle a whole turn, or up to its limit, over two seconds; a
112    /// distance by one.
113    fn new_args(context: &Context) -> MotionArgs {
114        let first = coordinates(&context.before).into_iter().next();
115        let (drivers, keys) = match first {
116            None => (Vec::new(), vec![vec![0.0], vec![2.0]]),
117            Some((state, _, measure, joint)) => {
118                let value = joint.values.iter().find(|v| v.state == state);
119                let (at, max) = value.map_or((0.0, None), |v| (v.value, v.max));
120                let to = max.unwrap_or(match measure {
121                    Measure::Angle => at + 360.0,
122                    Measure::Distance => at + 1.0,
123                });
124                (vec![state], vec![vec![0.0, at], vec![2.0, to]])
125            }
126        };
127        MotionArgs {
128            study: MotionStudy {
129                drivers,
130                keys,
131                frames: 25,
132            },
133        }
134    }
135
136    fn run(ctx: &mut StepContext, args: &MotionArgs) -> GeopResult<()> {
137        let step = ctx.step().to_string();
138        ctx.produce(&motion_study(&step), args, study)
139            .with_context(with_context!("motion({step})"))
140    }
141
142    /// The drivers, the time table — a row per keyframe, a column per
143    /// driver — how many frames, and a time to show the motion at: the
144    /// joints held at their values then, everything else giving way.
145    fn form<'a>(
146        context: &'a Context<'a>,
147        args: &MotionArgs,
148        editing: &FormState<MotionSession>,
149    ) -> EditForm<'a, MotionArgs, MotionSession> {
150        let mut f = EditForm::<MotionArgs, MotionSession>::default();
151        let known = coordinates(&context.before);
152        let label = |state: &str| {
153            (known.iter().find(|(s, ..)| s == state)).map_or(state.to_string(), |k| k.1.clone())
154        };
155        let study = &args.study;
156        let items = (study.drivers.iter().enumerate())
157            .map(|(d, driver)| {
158                let key = format!("driver:{driver}");
159                f.on(key.clone(), move |Edit { args, .. }, value| {
160                    if matches!(value, Value::Remove) {
161                        let study = &mut args.study;
162                        study.drivers.remove(d);
163                        for key in &mut study.keys {
164                            key.remove(d + 1);
165                        }
166                    }
167                });
168                let mut item = ListItem::new(key, label(driver));
169                item.removable = true;
170                item
171            })
172            .collect();
173        f.form
174            .dialog
175            .list("drivers", items, "None yet: add a joint to drive.");
176        let options: Vec<Choice> = std::iter::once(Choice::new("", "Drive a joint…"))
177            .chain(
178                (known.iter())
179                    .filter(|(state, ..)| !study.drivers.contains(state))
180                    .map(|(state, label, ..)| Choice::new(state.clone(), label.clone())),
181            )
182            .collect();
183        if options.len() == 1 && study.drivers.is_empty() {
184            f.form.dialog.text(
185                "no_joint",
186                "This part has no joint to drive yet: place parts and join them first.",
187                Tone::Error,
188            );
189        }
190        let values: BTreeMap<String, f64> = (known.iter())
191            .filter_map(|(state, _, _, joint)| {
192                let value = joint.values.iter().find(|v| v.state == *state)?;
193                Some((state.clone(), value.value))
194            })
195            .collect();
196        f.select(
197            "add_driver",
198            "drive",
199            "",
200            options,
201            true,
202            move |Edit { args, .. }, state| {
203                if state.is_empty() || args.study.drivers.iter().any(|d| d == state) {
204                    return;
205                }
206                // Held where it is at every keyframe, to start with.
207                let at = values.get(state).copied().unwrap_or(0.0);
208                args.study.drivers.push(state.to_string());
209                for key in &mut args.study.keys {
210                    key.push(at);
211                }
212            },
213        );
214        let unit = |driver: &str| match known.iter().find(|(s, ..)| s == driver) {
215            Some((_, _, Measure::Angle, _)) => Unit::Angle,
216            _ => Unit::Length,
217        };
218        let seconds = ColumnKind::Number {
219            unit: Unit::Fraction,
220        };
221        let mut columns = vec![Column::new("time (s)", seconds)];
222        columns.extend((study.drivers.iter()).map(|driver| {
223            let kind = ColumnKind::Number { unit: unit(driver) };
224            Column::new(label(driver), kind)
225        }));
226        let rows = (study.keys.iter())
227            .map(|key| {
228                let mut row = TableRow::new(key.iter().map(|v| Cell::number(*v)).collect());
229                row.removable = study.keys.len() > 1;
230                row
231            })
232            .collect();
233        f.table(
234            "keys",
235            Table::new(columns, rows, "No keyframe: add a row."),
236            |Edit { args, .. }, edit| {
237                let keys = &mut args.study.keys;
238                match edit {
239                    TableEdit::Cell { row, column, text } => {
240                        if let (Some(key), Ok(value)) = (keys.get_mut(row), text.trim().parse())
241                            && column < key.len()
242                        {
243                            key[column] = value;
244                        }
245                    }
246                    // A second on, holding where the last one is.
247                    TableEdit::AddRow => {
248                        let next = match keys.last() {
249                            Some(last) => {
250                                let mut next = last.clone();
251                                next[0] += 1.0;
252                                next
253                            }
254                            None => vec![0.0; args.study.drivers.len() + 1],
255                        };
256                        keys.push(next);
257                    }
258                    TableEdit::RemoveRow { row } if row < keys.len() && keys.len() > 1 => {
259                        keys.remove(row);
260                    }
261                    TableEdit::MoveRow { row, to } if row < keys.len() && to < keys.len() => {
262                        let key = keys.remove(row);
263                        keys.insert(to, key);
264                    }
265                    _ => {}
266                }
267            },
268        );
269        f.number(
270            "frames",
271            Number::new("frames", study.frames as f64, Unit::Count).range(2.0, 200.0),
272            |Edit { args, .. }, frames| args.study.frames = frames.round().max(2.0) as usize,
273        );
274        let (start, end) = study.span();
275        let shown = editing.session.preview.unwrap_or(start);
276        f.number(
277            "time",
278            Number::new("show at (s)", shown, Unit::Fraction).range(start, end.max(start + 1e-9)),
279            |Edit { session, .. }, t| session.preview = Some(t),
280        );
281        if let Some(t) = editing.session.preview
282            && study.validate().is_ok()
283        {
284            f.form.pulls.extend(
285                study
286                    .at(t)
287                    .into_iter()
288                    .map(|(parameter, value)| Pull::Value {
289                        parameter,
290                        value: Scalar::from_f64(value),
291                    }),
292            );
293        }
294        f.form.dialog.text(
295            "hint",
296            "Each row is a keyframe: its time, then where each joint driven is; between two, they move evenly. Play it, check it for collisions and export it in the motion panel.",
297            Tone::Hint,
298        );
299        f
300    }
301}
302
303/// The values a study `study` drives, at the time `t`, as a solve sets
304/// them.
305fn driven(study: &MotionStudy, t: f64) -> BTreeMap<String, Scalar> {
306    (study.at(t).into_iter())
307        .map(|(state, value)| (state, Scalar::from_f64(value)))
308        .collect()
309}
310
311/// The program `rerun` runs, last run with `state`, solved with the
312/// drivers of `study` set to their values at `t`: the values of the state
313/// that change. Fails, naming the time, where the joints cannot be there.
314fn solved_at(
315    rerun: &mut Rerun,
316    state: &StateValues,
317    study: &MotionStudy,
318    t: f64,
319) -> GeopResult<StateValues> {
320    let (moved, report) = solving::set(rerun, state, &driven(study, t))?;
321    if !report.converged {
322        return Err(GeopError::new(format!(
323            "at {t} s the joints cannot be where the motion drives them: {} do not hold",
324            report.failed.join(", ")
325        )));
326    }
327    Ok(moved)
328}
329
330/// The motion study `name` of the part `reader` reads — or, with no name,
331/// its last.
332fn find(reader: &Reader, name: Option<&str>) -> GeopResult<MotionStudy> {
333    let studies = motion_studies(reader);
334    let found = match name {
335        Some(name) => studies.iter().find(|(step, _)| step == name),
336        None => studies.last(),
337    };
338    found.map(|(_, study)| (*study).clone()).ok_or_else(|| {
339        let names: Vec<&str> = studies.iter().map(|(step, _)| step.as_str()).collect();
340        GeopError::new(match (name, names.is_empty()) {
341            (_, true) => "this part has no motion study: add a Motion step".to_string(),
342            (Some(name), false) => {
343                format!("no motion study {name:?}: there are {}", names.join(", "))
344            }
345            (None, false) => unreachable!("a last study where there are some"),
346        })
347    })
348}
349
350/// A motion study as the panel lists it.
351#[derive(Clone, Debug, PartialEq, Serialize)]
352pub struct StudyState {
353    /// The id of its step.
354    pub name: String,
355    /// When it starts and ends, in seconds.
356    pub start: f64,
357    pub end: f64,
358    pub frames: usize,
359    /// The joint coordinates it drives, by state.
360    pub drivers: Vec<String>,
361}
362
363/// Two parts found overlapping at a time of a motion.
364#[derive(Clone, Debug, PartialEq, Serialize)]
365pub struct Collision {
366    pub time: f64,
367    pub a: String,
368    pub b: String,
369}
370
371/// What the last check of a study found.
372#[derive(Clone, Debug, PartialEq, Serialize)]
373pub struct Checked {
374    pub study: String,
375    /// Each pair of solids that overlap, at each frame they do, in time.
376    pub collisions: Vec<Collision>,
377}
378
379/// What the motion panel shows: the studies of the part shown, the one
380/// played last and the time it was shown at, and what the last check
381/// found.
382#[derive(Clone, Debug, Default, PartialEq, Serialize)]
383pub struct MotionState {
384    pub studies: Vec<StudyState>,
385    /// The study played last, and the time it was shown at.
386    pub shown: Option<(String, f64)>,
387    pub checked: Option<Checked>,
388}
389
390/// What the motion panel is sent.
391#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
392#[serde(tag = "command", rename_all = "snake_case")]
393pub enum MotionCommand {
394    /// Show the study `study` at the time `time`, in seconds: its joints
395    /// set to their values then, from where they are, everything else free
396    /// giving way.
397    Seek { study: String, time: f64 },
398    /// Play the study `study` through, frame by frame, and find the parts
399    /// that overlap at each: then show it where it was.
400    Check { study: String },
401}
402
403/// The motion panel: see the module docs.
404#[derive(Default)]
405pub struct MotionPanel {
406    shown: RefCell<Option<(String, f64)>>,
407    checked: RefCell<Option<Checked>>,
408}
409
410/// The pairs of solids an interference report — as the inspector of that
411/// kind writes it — says overlap.
412fn overlapping(report: &serde_json::Value) -> Vec<(String, String)> {
413    let found = report["found"].as_array().cloned().unwrap_or_default();
414    (found.iter())
415        .filter(|pair| pair["contact"] == "overlap")
416        .filter_map(|pair| {
417            let name = |k: &str| pair[k].as_str().map(str::to_string);
418            Some((name("a")?, name("b")?))
419        })
420        .collect()
421}
422
423/// What checks a motion for collisions, by kind: the interference
424/// inspector, wherever an editor is composed with one.
425const INTERFERENCE: &str = "interference";
426
427impl Panel for MotionPanel {
428    const KEY: &'static str = "motion";
429    type State = MotionState;
430    type Command = MotionCommand;
431
432    fn state(&self, _: &Program, part: &Reader) -> MotionState {
433        let studies: Vec<StudyState> = (motion_studies(part).into_iter())
434            .map(|(name, study)| {
435                let (start, end) = study.span();
436                StudyState {
437                    name,
438                    start,
439                    end,
440                    frames: study.frames,
441                    drivers: study.drivers.clone(),
442                }
443            })
444            .collect();
445        let known = |name: &str| studies.iter().any(|s| s.name == name);
446        let shown = (self.shown.borrow().clone()).filter(|(name, _)| known(name));
447        let checked = (self.checked.borrow().clone()).filter(|c| known(&c.study));
448        MotionState {
449            studies,
450            shown,
451            checked,
452        }
453    }
454
455    fn command(&self, ctx: &mut EditorContext, command: MotionCommand) -> GeopResult<Changed> {
456        ctx.idle()?;
457        ctx.run_all();
458        match command {
459            MotionCommand::Seek { study: name, time } => {
460                let study = find(&ctx.part(), Some(&name))?;
461                let mut rerun = ctx.rerun();
462                let state = rerun.program.state.clone();
463                let moved = solved_at(&mut rerun, &state, &study, time)?;
464                ctx.move_state(moved);
465                *self.shown.borrow_mut() = Some((name, time));
466                Ok(Changed::Run)
467            }
468            MotionCommand::Check { study: name } => {
469                let study = find(&ctx.part(), Some(&name))?;
470                let before = ctx.program().state.clone();
471                let mut collisions = Vec::new();
472                let mut checked = Ok(());
473                for time in study.times() {
474                    let mut rerun = ctx.rerun();
475                    let state = rerun.program.state.clone();
476                    let moved = match solved_at(&mut rerun, &state, &study, time) {
477                        Ok(moved) => moved,
478                        Err(e) => {
479                            checked = Err(e);
480                            break;
481                        }
482                    };
483                    ctx.move_state(moved);
484                    ctx.run_all();
485                    let inspection = ctx.inspect(INTERFERENCE, &[]).map_err(|e| {
486                        e.with_context("collisions are checked by the interference inspector")
487                    });
488                    match inspection {
489                        Ok(inspection) => {
490                            collisions.extend(
491                                (overlapping(&inspection.report).into_iter())
492                                    .map(|(a, b)| Collision { time, a, b }),
493                            )
494                        }
495                        Err(e) => {
496                            checked = Err(e);
497                            break;
498                        }
499                    }
500                }
501                // Shown where it was before the check.
502                ctx.move_state(before);
503                ctx.run_all();
504                checked?;
505                *self.checked.borrow_mut() = Some(Checked {
506                    study: name,
507                    collisions,
508                });
509                Ok(Changed::Run)
510            }
511        }
512    }
513}
514
515/// Writes a motion study frame by frame: at each, the values its drivers
516/// are set to and where every placed part is.
517pub struct MotionExport;
518
519/// Which motion study to write.
520#[derive(Clone, Debug, Default, PartialEq, Deserialize)]
521#[serde(default)]
522pub struct MotionOptions {
523    /// The id of its step: the part's last, if not given.
524    pub study: Option<String>,
525}
526
527/// One frame of a motion, as written: the time, the drivers' values, and
528/// every placed part's pose — its position, and its turn as a unit
529/// quaternion `[w, x, y, z]`.
530#[derive(Serialize)]
531struct Frame {
532    time: f64,
533    values: BTreeMap<String, f64>,
534    poses: BTreeMap<String, [f64; 7]>,
535}
536
537impl Exporter for MotionExport {
538    const INFO: ExporterInfo = ExporterInfo {
539        kind: "motion",
540        label: "Motion",
541        doc: "joint values and where each placed part is, at every frame of a motion study",
542        formats: &["csv", "json"],
543    };
544    type Options = MotionOptions;
545
546    fn export(output: &mut Output, format: &str, options: MotionOptions) -> GeopResult<Export> {
547        let study = find(&output.part(), options.study.as_deref())?;
548        let mut state = output.file.state.clone();
549        let mut frames = Vec::new();
550        let mut made = Ok(());
551        for time in study.times() {
552            let mut rerun = Rerun {
553                runner: output.runner,
554                part: &mut *output.part,
555                program: output.file,
556            };
557            match solved_at(&mut rerun, &state, &study, time) {
558                Ok(moved) => state.extend(moved),
559                Err(e) => {
560                    made = Err(e);
561                    break;
562                }
563            }
564            // A solve leaves the part as it was: built at the frame's state.
565            let frame = Program {
566                steps: output.file.steps.clone(),
567                state: state.clone(),
568                display: output.file.display.clone(),
569            };
570            (output.runner).run_with(output.part, &frame, &Default::default(), None);
571            let reader = output.part.current();
572            let poses = (placed_parts(&reader)?.into_iter())
573                .map(|placed| {
574                    let pose = placed.located.world;
575                    let (p, q) = (pose.position(), pose.rotation());
576                    let f = |s: Scalar| s.to_f64();
577                    let numbers = [p[0], p[1], p[2], q.w, q.x, q.y, q.z].map(f);
578                    (placed.name, numbers)
579                })
580                .collect();
581            let values = study.at(time).into_iter().collect();
582            frames.push(Frame {
583                time,
584                values,
585                poses,
586            });
587        }
588        // The part as it was, for whatever is asked of it next.
589        let inputs = Default::default();
590        (output.runner).run_with(output.part, output.file, &inputs, None);
591        made?;
592        let name = stem(output.file_name());
593        let (text, media_type) = match format {
594            "json" => {
595                let text = serde_json::to_string_pretty(&frames)
596                    .map_err(|e| GeopError::new(format!("writing the motion: {e}")))?;
597                (text, "application/json")
598            }
599            _ => (csv(&study, &frames), "text/csv"),
600        };
601        Ok(Export {
602            name: format!("{name}.{format}"),
603            media_type: media_type.into(),
604            content: Content::Text(text),
605        })
606    }
607}
608
609/// `frames` as comma separated values: a row a frame, its time, the
610/// drivers' values in `study`'s order, then each part's position and turn.
611fn csv(study: &MotionStudy, frames: &[Frame]) -> String {
612    let parts: Vec<&String> = frames
613        .first()
614        .map_or(Vec::new(), |f| f.poses.keys().collect());
615    let mut header = vec!["time".to_string()];
616    header.extend(study.drivers.iter().cloned());
617    for part in &parts {
618        for field in ["x", "y", "z", "qw", "qx", "qy", "qz"] {
619            header.push(format!("{part}.{field}"));
620        }
621    }
622    let quoted: Vec<String> = header
623        .iter()
624        .map(|h| format!("\"{}\"", h.replace('"', "\"\"")))
625        .collect();
626    let mut out = quoted.join(",") + "\n";
627    for frame in frames {
628        let mut row = vec![frame.time];
629        row.extend(study.drivers.iter().map(|d| frame.values[d]));
630        for part in &parts {
631            row.extend(frame.poses[*part]);
632        }
633        let row: Vec<String> = row.iter().map(|v| v.to_string()).collect();
634        out += &(row.join(",") + "\n");
635    }
636    out
637}