1use 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#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
46pub struct Drive;
47
48#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
50pub struct MotionArgs {
51 #[serde(flatten)]
52 pub study: MotionStudy,
53}
54
55fn 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
69fn 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#[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 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 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 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 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
303fn 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
311fn 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
330fn 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#[derive(Clone, Debug, PartialEq, Serialize)]
352pub struct StudyState {
353 pub name: String,
355 pub start: f64,
357 pub end: f64,
358 pub frames: usize,
359 pub drivers: Vec<String>,
361}
362
363#[derive(Clone, Debug, PartialEq, Serialize)]
365pub struct Collision {
366 pub time: f64,
367 pub a: String,
368 pub b: String,
369}
370
371#[derive(Clone, Debug, PartialEq, Serialize)]
373pub struct Checked {
374 pub study: String,
375 pub collisions: Vec<Collision>,
377}
378
379#[derive(Clone, Debug, Default, PartialEq, Serialize)]
383pub struct MotionState {
384 pub studies: Vec<StudyState>,
385 pub shown: Option<(String, f64)>,
387 pub checked: Option<Checked>,
388}
389
390#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
392#[serde(tag = "command", rename_all = "snake_case")]
393pub enum MotionCommand {
394 Seek { study: String, time: f64 },
398 Check { study: String },
401}
402
403#[derive(Default)]
405pub struct MotionPanel {
406 shown: RefCell<Option<(String, f64)>>,
407 checked: RefCell<Option<Checked>>,
408}
409
410fn 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
423const 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 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
515pub struct MotionExport;
518
519#[derive(Clone, Debug, Default, PartialEq, Deserialize)]
521#[serde(default)]
522pub struct MotionOptions {
523 pub study: Option<String>,
525}
526
527#[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 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 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
609fn 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}