Motion analysis: plots of a study's joint values and a point's path, and the parts that collide along it

- motion.plot: joint values and a point's path sampled as the study plays (closed loops
  following), with velocities and accelerations by finite differences, per step or per
  second with a duration.
- motion.interference: the pairs of parts that pass through each other along a study, with
  the first step, the step ranges, the depth and the shared volume; occurrences, against
  or contact sets choose the pairs; bounded samples and work.
- Motion Study dialog: a Study box opens a saved study; a chart of a joint value or a
  picked point with a cursor at the step shown (click to move there); Check collisions
  with the steps shaded on the chart. ui.start motion.study takes study.
This commit is contained in:
MateuszSychXpl committed 2026-10-16 17:52:38 +02:00
1 parent 8a055afdd6
commit 8fdf6a61a2
23 files changed
+1398 -40

No files matched your search

+70 -2
View File
@@ -18,6 +18,8 @@ use crate::theme::Tokens;
#[path = "configuration_rules_ui.rs"]
mod configuration_rules_ui;
#[path = "dialogs_motion_plot.rs"]
pub mod plot;
/// One keyed value of a motion study: a joint's value (index) at steps.
#[derive(Clone, Debug, PartialEq)]
@@ -52,6 +54,8 @@ pub enum Mo {
shown: Option<(u64, u64)>,
saved: Option<BTreeMap<u64, Mat>>,
error: Option<String>,
/// The chart and the collision check by the timeline.
analysis: Box<plot::Analysis>,
},
Explode {
name: String,
@@ -114,8 +118,14 @@ pub fn start(app: &SolveApp, id: &str) -> Option<(Kind, Vec<SelInput>)> {
shown: None,
saved: None,
error: None,
analysis: Box::default(),
},
vec![],
// A point to plot, and the parts to check for collisions (each optional).
vec![
SelInput::new("Point", crate::dialogs_assembly::SNAPS, false),
SelInput::new("Components", BODIES, true),
SelInput::new("Against", BODIES, true),
],
),
"explode.create" => (Mo::Explode { name: n("Exploded View", "explode.list"), scale: 1.0, shown: None, saved: None, error: None }, vec![]),
"config.table" | "config.configure" => (Mo::Configurations { new_row: String::new(), error: None, rule_editor: Default::default() }, vec![]),
@@ -193,6 +203,43 @@ fn transport(ui: &mut egui::Ui, what: Transport, tip: &str) -> egui::Response {
resp.on_hover_text(tip)
}
/// Study `key` (an id or a name) as the Motion Study dialog edits it: its name, steps and keys
/// as tracks (values in degrees and mm).
pub fn study_tracks(s: &Session, key: &str) -> Result<(String, String, Vec<Track>), String> {
let m = s.doc.assembly.studies.iter().find(|m| m.id.to_string() == key || m.name == key).ok_or_else(|| format!("no motion study `{key}`"))?;
let tracks = m
.keys
.iter()
.filter_map(|k| {
let joint = s.doc.assembly.joints.iter().position(|j| j.id == k.joint)?;
let angle = s.doc.assembly.joints.get(joint)?.kind.is_angle(k.index);
let value = |v: f64| if angle { crate::dialogs_assembly::num(v.to_degrees(), "deg") } else { crate::dialogs_assembly::num(v, "mm") };
let points = k.points.iter().map(|(st, v)| (st.round().clamp(0.0, f64::from(m.steps)) as u32, value(*v))).collect();
Some(Track { joint, index: k.index, points })
})
.collect();
Ok((m.name.clone(), m.steps.to_string(), tracks))
}
/// What the Motion Study dialog opens on when the control channel's `ui.start` passes `params`:
/// `study` (an id or a name), opened to edit.
pub fn preset(s: &Session, k: &mut Mo, p: &Value) -> Result<(), String> {
let Mo::MotionStudy { name, study, steps, tracks, shown, .. } = k else { return Err("not a Motion Study dialog".into()) };
let o = p.as_object().ok_or("`params` must be an object")?;
if let Some(key) = o.keys().find(|k| k.as_str() != "study") {
return Err(format!("unknown param `{key}` (Motion Study takes study)"));
}
let key = match o.get("study") {
Some(Value::Number(n)) => n.to_string(),
Some(Value::String(x)) => x.clone(),
Some(_) => return Err("`study` must be an id or a name".into()),
None => return Ok(()),
};
let (n, st, t) = study_tracks(s, &key)?;
(*study, *name, *steps, *tracks, *shown) = (Some(n.clone()), n, st, t, None);
Ok(())
}
/// The study command's parameters.
pub fn study_params(s: &Session, name: &str, study: Option<&str>, steps: &str, tracks: &[Track]) -> Result<Value, String> {
let steps: u32 = steps.trim().parse().map_err(|_| "steps must be a whole number".to_string())?;
@@ -432,7 +479,24 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Mo, inputs: &mut [Sel
ui.end_row();
}
}
Mo::MotionStudy { name, study, steps, tracks, step, playing, looping, speed, shown, saved, error } => {
Mo::MotionStudy { name, study, steps, tracks, step, playing, looping, speed, shown, saved, error, analysis } => {
// An existing study opens to edit (and to chart); New makes another from these keys.
let names: Vec<String> = app.session.doc.assembly.studies.iter().map(|m| m.name.clone()).collect();
if !names.is_empty() {
let labels: Vec<&str> = std::iter::once("New study").chain(names.iter().map(String::as_str)).collect();
let was = study.as_ref().and_then(|n| names.iter().position(|x| x == n)).map_or(0, |i| i + 1);
let mut pick = was;
row_label(ui, "Study");
combo(ui, "ms_study", &labels, &mut pick);
ui.end_row();
if pick != was {
match pick.checked_sub(1).and_then(|i| names.get(i)).map(|n| study_tracks(&app.session, n)) {
Some(Ok((n, st, t))) => (*study, *name, *steps, *tracks, *shown, *playing) = (Some(n.clone()), n, st, t, None, false),
Some(Err(e)) => *error = Some(e),
None => *study = None,
}
}
}
row_label(ui, "Name");
ui.text_edit_singleline(name);
ui.end_row();
@@ -497,6 +561,9 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Mo, inputs: &mut [Sel
(*step, *playing) = advance(*step, dt * fps, total, *looping);
ui.ctx().request_repaint();
}
// The chart and the collision check, by the timeline.
let study_now = study_params(&app.session, name, study.as_deref(), steps, tracks);
plot::rows(app, ui, analysis, &study_now, step, playing, total, inputs);
// Tracks: a row per keyed joint value, its keys on a strip under the steps.
let now = step.round() as u32;
let mut remove: Option<usize> = None;
@@ -1043,6 +1110,7 @@ mod tests {
shown: None,
saved,
error: None,
analysis: Box::default(),
}
} else {
Mo::Explode { name: "E".into(), scale: 1.0, shown: None, saved, error: None }