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:
1 parent
8a055afdd6
commit
8fdf6a61a2
23 files changed
+1398
-40
No files matched your search
@@ -0,0 +1,431 @@
|
||||
//! Motion Study analysis rows: a chart over the study's timeline of a joint's value, or of a
|
||||
//! picked point's travel, as value, velocity or acceleration (`motion.plot`), with a cursor at
|
||||
//! the step shown; and the parts that collide as the study plays (`motion.interference`). Both
|
||||
//! are worked out on a scratch copy, again only when what they are for changes.
|
||||
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
use egui::{Align2, FontId, Rect, RichText, Sense, Stroke, pos2, vec2};
|
||||
use serde_json::{Value, json};
|
||||
use solidcraft_engine::Session;
|
||||
use solidcraft_engine::doc::joints::JointKind;
|
||||
|
||||
use crate::SolveApp;
|
||||
use crate::dialogs::{combo, row_label};
|
||||
use crate::selection::SelInput;
|
||||
use crate::theme::Tokens;
|
||||
|
||||
/// Most samples the chart takes (each is a solve).
|
||||
const SAMPLES: u64 = 201;
|
||||
pub const QUANTITIES: [&str; 3] = ["Value", "Velocity", "Acceleration"];
|
||||
|
||||
/// What the chart shows.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Plotted {
|
||||
Nothing,
|
||||
/// A joint's value: (joint id, value index).
|
||||
Joint(u64, usize),
|
||||
/// The picked point: its distance from where it starts.
|
||||
Point,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Analysis {
|
||||
/// Index into `choices`.
|
||||
pub what: usize,
|
||||
/// Index into `QUANTITIES`.
|
||||
pub quantity: usize,
|
||||
/// Seconds the whole study takes (empty: rates per step).
|
||||
pub duration: String,
|
||||
/// Collisions among all components (0) or in the design's contact sets (1).
|
||||
pub among: usize,
|
||||
/// The chart's `motion.plot` answer (or why none) and what it was for.
|
||||
plot: Option<(u64, Result<Value, String>)>,
|
||||
/// The last collision check's answer and what it was for.
|
||||
collisions: Option<(u64, Result<Value, String>)>,
|
||||
}
|
||||
|
||||
/// What the chart can show: (label, what).
|
||||
pub fn choices(s: &Session, point: bool) -> Vec<(String, Plotted)> {
|
||||
let mut out = vec![("None".to_string(), Plotted::Nothing)];
|
||||
for j in s.doc.assembly.joints.iter().filter(|j| j.kind != JointKind::Rigid && !j.suppressed) {
|
||||
let dofs = j.kind.dofs();
|
||||
for (i, d) in dofs.iter().enumerate() {
|
||||
out.push((if dofs.len() > 1 { format!("{} · {d}", j.name) } else { j.name.clone() }, Plotted::Joint(j.id, i)));
|
||||
}
|
||||
}
|
||||
if point {
|
||||
out.push(("Picked point".into(), Plotted::Point));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// `motion.plot`'s parameters for `study` (the study command's parameters).
|
||||
pub fn plot_params(s: &Session, what: Plotted, study: &Value, duration: &str, inputs: &[SelInput]) -> Result<Value, String> {
|
||||
let name = study.get("study").or_else(|| study.get("name")).cloned().unwrap_or(Value::Null);
|
||||
let steps = study["steps"].as_u64().unwrap_or(100);
|
||||
let mut p = json!({"study": name, "samples": steps.saturating_add(1).clamp(3, SAMPLES)});
|
||||
match what {
|
||||
Plotted::Nothing => return Err(String::new()),
|
||||
Plotted::Joint(id, _) => p["joints"] = json!([id]),
|
||||
Plotted::Point => {
|
||||
let picked = crate::dialogs_assembly::picked_point(s, inputs)?;
|
||||
p["joints"] = json!([]);
|
||||
p["occurrence"] = picked["occurrence"].clone();
|
||||
p["point"] = picked["point"].clone();
|
||||
}
|
||||
}
|
||||
let d = duration.trim();
|
||||
if !d.is_empty() {
|
||||
p["duration"] = json!(d.parse::<f64>().map_err(|_| "the duration must be a number of seconds".to_string())?);
|
||||
}
|
||||
Ok(p)
|
||||
}
|
||||
|
||||
fn nums(v: &Value) -> Vec<f64> {
|
||||
v.as_array().into_iter().flatten().map(|x| x.as_f64().unwrap_or(f64::NAN)).collect()
|
||||
}
|
||||
|
||||
/// The chart's line from `motion.plot`'s answer: (study steps, values, unit).
|
||||
pub fn line(v: &Value, what: Plotted, quantity: usize) -> Option<(Vec<f64>, Vec<f64>, String)> {
|
||||
let series = match what {
|
||||
Plotted::Nothing => return None,
|
||||
Plotted::Joint(_, i) => v["joints"].as_array()?.iter().find(|j| j["index"] == i)?,
|
||||
Plotted::Point => &v["point"],
|
||||
};
|
||||
let unit = series[["unit", "velocity_unit", "acceleration_unit"].get(quantity)?].as_str()?.to_string();
|
||||
let ys = match (what, quantity) {
|
||||
(Plotted::Point, 0) => {
|
||||
let at = |p: &Value| [0, 1, 2].map(|c| p[c].as_f64().unwrap_or(f64::NAN));
|
||||
let pts: Vec<[f64; 3]> = series["position"].as_array()?.iter().map(at).collect();
|
||||
let start = *pts.first()?;
|
||||
pts.iter().map(|p| p.iter().zip(&start).map(|(a, b)| (a - b).powi(2)).sum::<f64>().sqrt()).collect()
|
||||
}
|
||||
(Plotted::Point, 1) => nums(&series["speed"]),
|
||||
(Plotted::Point, _) => nums(&series["acceleration_norm"]),
|
||||
(_, q) => nums(&series[*["value", "velocity", "acceleration"].get(q)?]),
|
||||
};
|
||||
Some((nums(&v["step"]), ys, unit))
|
||||
}
|
||||
|
||||
/// A short number for an axis or a readout.
|
||||
fn short(v: f64) -> String {
|
||||
let a = v.abs();
|
||||
if a >= 1000.0 {
|
||||
format!("{v:.0}")
|
||||
} else if a >= 10.0 {
|
||||
format!("{v:.1}")
|
||||
} else {
|
||||
format!("{v:.3}")
|
||||
}
|
||||
}
|
||||
|
||||
/// The value of a line at step `at` (linear between samples; `None` outside it).
|
||||
pub fn value_at(xs: &[f64], ys: &[f64], at: f64) -> Option<f64> {
|
||||
let k = xs.iter().position(|x| *x >= at)?;
|
||||
let (x1, y1) = (*xs.get(k)?, *ys.get(k)?);
|
||||
if k == 0 || x1 <= at {
|
||||
return Some(y1);
|
||||
}
|
||||
let (x0, y0) = (*xs.get(k - 1)?, *ys.get(k - 1)?);
|
||||
Some(y0 + (y1 - y0) * (at - x0) / (x1 - x0).max(1e-12))
|
||||
}
|
||||
|
||||
/// A line chart of `ys` over study steps `xs` (0…`total`), with the step shown as a cursor and
|
||||
/// `bands` (step ranges where parts collide) shaded. Returns the step clicked or dragged to.
|
||||
fn chart(ui: &mut egui::Ui, xs: &[f64], ys: &[f64], unit: &str, total: u32, now: f64, bands: &[[f64; 2]], t: &Tokens) -> Option<f64> {
|
||||
let (r, resp) = ui.allocate_exact_size(vec2(300.0, 120.0), Sense::click_and_drag());
|
||||
crate::scenario::publish_rect("motion:chart", r);
|
||||
let p = ui.painter_at(r);
|
||||
p.rect(r, 3.0, t.field, Stroke::new(1.0, t.border), egui::StrokeKind::Inside);
|
||||
let plot = Rect::from_min_max(r.min + vec2(6.0, 16.0), r.max - vec2(6.0, 16.0));
|
||||
let total = f64::from(total.max(1));
|
||||
let x = |s: f64| plot.left() + plot.width() * (s / total).clamp(0.0, 1.0) as f32;
|
||||
for [a, b] in bands {
|
||||
let (a, b) = (x(*a), x(*b));
|
||||
p.rect_filled(Rect::from_x_y_ranges((a - 1.0)..=(b + 1.0), r.y_range()), 0.0, t.interference.gamma_multiply(0.35));
|
||||
}
|
||||
let (lo, hi) = ys.iter().filter(|v| v.is_finite()).fold((f64::INFINITY, f64::NEG_INFINITY), |(a, b), v| (a.min(*v), b.max(*v)));
|
||||
if lo.is_finite() {
|
||||
// A flat line sits in the middle.
|
||||
let pad = if hi - lo < 1e-9 * hi.abs().max(1.0) { 1.0f64.max(hi.abs() * 0.1) } else { 0.0 };
|
||||
let (lo, hi) = (lo - pad, hi + pad);
|
||||
let y = |v: f64| plot.bottom() - plot.height() * ((v - lo) / (hi - lo)) as f32;
|
||||
if lo < 0.0 && hi > 0.0 {
|
||||
p.line_segment([pos2(plot.left(), y(0.0)), pos2(plot.right(), y(0.0))], Stroke::new(1.0, t.border));
|
||||
}
|
||||
let pts: Vec<egui::Pos2> = xs.iter().zip(ys).filter(|(_, v)| v.is_finite()).map(|(s, v)| pos2(x(*s), y(*v))).collect();
|
||||
p.add(egui::Shape::line(pts, Stroke::new(1.5, t.accent)));
|
||||
let font = FontId::proportional(10.0);
|
||||
p.text(r.left_top() + vec2(4.0, 2.0), Align2::LEFT_TOP, format!("{} {unit}", short(hi)), font.clone(), t.text_dim);
|
||||
p.text(r.left_bottom() + vec2(4.0, -2.0), Align2::LEFT_BOTTOM, format!("{} {unit}", short(lo)), font.clone(), t.text_dim);
|
||||
if let Some(v) = value_at(xs, ys, now) {
|
||||
p.text(r.right_top() + vec2(-4.0, 2.0), Align2::RIGHT_TOP, format!("{} {unit}", short(v)), font, t.text);
|
||||
}
|
||||
}
|
||||
let cx = x(now);
|
||||
p.line_segment([pos2(cx, r.top()), pos2(cx, r.bottom())], Stroke::new(1.5, t.warning));
|
||||
let pos = resp.interact_pointer_pos().filter(|_| resp.clicked() || resp.dragged())?;
|
||||
Some((f64::from((pos.x - plot.left()) / plot.width().max(1.0)).clamp(0.0, 1.0) * total).round())
|
||||
}
|
||||
|
||||
/// What the motion stopping short ends the chart with (`None`: it went all the way).
|
||||
fn stop_text(v: &Value) -> Option<String> {
|
||||
let at = v["stopped_at"].as_u64()?.to_string();
|
||||
let name = |x: &Value| x.as_str().unwrap_or_default().to_string();
|
||||
if let Some(l) = v["stopped_by_loop"].as_object() {
|
||||
return Some(crate::i18n::format("trace.stopped_by_loop", &[("sample", &at), ("joint", &name(&l["joint"]))]));
|
||||
}
|
||||
let c = v["stopped_by_contact"].as_object()?;
|
||||
Some(crate::i18n::format("trace.stopped_by_contact", &[("sample", &at), ("a", &name(&c["a"])), ("b", &name(&c["b"]))]))
|
||||
}
|
||||
|
||||
fn key_of(x: impl std::fmt::Debug) -> u64 {
|
||||
let mut h = std::collections::hash_map::DefaultHasher::new();
|
||||
format!("{x:?}").hash(&mut h);
|
||||
h.finish()
|
||||
}
|
||||
|
||||
/// The occurrences of the bodies picked in input `i` (the instance each pick is on).
|
||||
fn picked_occurrences(s: &Session, inputs: &[SelInput], i: usize) -> Vec<u64> {
|
||||
let names: Vec<&str> = inputs.get(i).map(|inp| inp.items.iter().filter_map(crate::dialogs_assembly::body_of).collect()).unwrap_or_default();
|
||||
if names.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
let mut out = Vec::new();
|
||||
for (name, _, path, _) in s.doc.body_frames(&s.model.state()) {
|
||||
if let Some(o) = path.last().filter(|_| names.contains(&name.as_str()))
|
||||
&& !out.contains(o)
|
||||
{
|
||||
out.push(*o);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The step ranges where any pair collides, from `motion.interference`'s answer.
|
||||
pub fn collision_bands(v: &Value) -> Vec<[f64; 2]> {
|
||||
let ranges = v["collisions"].as_array().into_iter().flatten().flat_map(|c| c["steps"].as_array().cloned().unwrap_or_default());
|
||||
ranges.filter_map(|r| Some([r.get(0)?.as_f64()?, r.get(1)?.as_f64()?])).collect()
|
||||
}
|
||||
|
||||
/// The analysis rows under a study's tracks: `study` is the study command's parameters (or why
|
||||
/// there are none yet), `step` the step shown (a click on the chart or a collision moves it).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn rows(
|
||||
app: &SolveApp,
|
||||
ui: &mut egui::Ui,
|
||||
a: &mut Analysis,
|
||||
study: &Result<Value, String>,
|
||||
step: &mut f64,
|
||||
playing: &mut bool,
|
||||
total: u32,
|
||||
inputs: &[SelInput],
|
||||
) {
|
||||
let t = Tokens::get();
|
||||
let s = &app.session;
|
||||
let picked = inputs.first().is_some_and(|i| !i.items.is_empty());
|
||||
let list = choices(s, picked);
|
||||
a.what = a.what.min(list.len().saturating_sub(1));
|
||||
row_label(ui, "Plot");
|
||||
let labels: Vec<&str> = list.iter().map(|(l, _)| l.as_str()).collect();
|
||||
combo(ui, "ms_plot", &labels, &mut a.what);
|
||||
ui.end_row();
|
||||
let what = list.get(a.what).map_or(Plotted::Nothing, |(_, w)| *w);
|
||||
let bands = a.collisions.as_ref().and_then(|(_, r)| r.as_ref().ok()).map(collision_bands).unwrap_or_default();
|
||||
if what != Plotted::Nothing {
|
||||
row_label(ui, "Quantity");
|
||||
combo(ui, "ms_quantity", &QUANTITIES, &mut a.quantity);
|
||||
ui.end_row();
|
||||
row_label(ui, "Duration");
|
||||
ui.add(egui::TextEdit::singleline(&mut a.duration).hint_text(crate::i18n::tr("motion.duration_hint")));
|
||||
ui.end_row();
|
||||
let params = study.clone().and_then(|st| plot_params(s, what, &st, &a.duration, inputs).map(|p| (st, p)));
|
||||
let key = key_of(¶ms);
|
||||
if a.plot.as_ref().is_none_or(|(k, _)| *k != key) {
|
||||
let r = params.and_then(|(st, p)| {
|
||||
let mut sc = s.scratch();
|
||||
sc.execute("motion.study", &st).map_err(|e| e.to_string())?;
|
||||
sc.execute("motion.plot", &p).map_err(|e| e.to_string())
|
||||
});
|
||||
a.plot = Some((key, r));
|
||||
}
|
||||
row_label(ui, "");
|
||||
match a.plot.as_ref().map(|(_, r)| r) {
|
||||
Some(Ok(v)) => {
|
||||
ui.vertical(|ui| {
|
||||
if let Some((xs, ys, unit)) = line(v, what, a.quantity) {
|
||||
if let Some(to) = chart(ui, &xs, &ys, &unit, total, *step, &bands, &t) {
|
||||
(*step, *playing) = (to, false);
|
||||
}
|
||||
}
|
||||
if let Some(why) = stop_text(v) {
|
||||
ui.add(egui::Label::new(RichText::new(why).color(t.warning)).wrap());
|
||||
}
|
||||
});
|
||||
}
|
||||
Some(Err(e)) => {
|
||||
ui.add(egui::Label::new(RichText::new(e.as_str()).color(t.text_dim)).wrap());
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
ui.end_row();
|
||||
}
|
||||
// Collisions over the whole study: of the picked components (against the other picks, if
|
||||
// any), else among all components or in the contact sets.
|
||||
let sets = !app.session.doc.assembly.contact_sets.is_empty() || app.session.doc.assembly.contact_all;
|
||||
a.among = if sets { a.among.min(1) } else { 0 };
|
||||
let (parts, against) = (picked_occurrences(&app.session, inputs, 1), picked_occurrences(&app.session, inputs, 2));
|
||||
let among = if !parts.is_empty() {
|
||||
let mut p = json!({"occurrences": parts});
|
||||
if !against.is_empty() {
|
||||
p["against"] = json!(against);
|
||||
}
|
||||
p
|
||||
} else {
|
||||
json!({"contact_sets": a.among == 1})
|
||||
};
|
||||
let check = study.as_ref().ok().map(|st| (st.clone(), among));
|
||||
let key = key_of(&check);
|
||||
if a.collisions.as_ref().is_some_and(|(k, _)| *k != key) {
|
||||
a.collisions = None;
|
||||
}
|
||||
row_label(ui, "Collisions");
|
||||
ui.horizontal(|ui| {
|
||||
if sets && parts.is_empty() {
|
||||
combo(ui, "ms_among", &["All components", "Contact sets"], &mut a.among);
|
||||
}
|
||||
if ui.add_enabled(check.is_some(), egui::Button::new(crate::i18n::tr_label("Check collisions"))).clicked()
|
||||
&& let Some((st, mut p)) = check
|
||||
{
|
||||
let mut sc = app.session.scratch();
|
||||
p["study"] = st.get("study").or_else(|| st.get("name")).cloned().unwrap_or(Value::Null);
|
||||
let r = sc.execute("motion.study", &st).and_then(|_| sc.execute("motion.interference", &p)).map_err(|e| e.to_string());
|
||||
a.collisions = Some((key, r));
|
||||
}
|
||||
});
|
||||
ui.end_row();
|
||||
match a.collisions.as_ref().map(|(_, r)| r) {
|
||||
Some(Ok(v)) => {
|
||||
let hits = v["collisions"].as_array().cloned().unwrap_or_default();
|
||||
row_label(ui, "");
|
||||
ui.vertical(|ui| {
|
||||
if hits.is_empty() {
|
||||
ui.label(RichText::new(crate::i18n::tr("motion.no_collisions")).color(t.text_dim));
|
||||
}
|
||||
for c in &hits {
|
||||
let steps: Vec<String> = c["steps"]
|
||||
.as_array()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|r| {
|
||||
let (from, to) = (r[0].as_f64().unwrap_or(0.0).round(), r[1].as_f64().unwrap_or(0.0).round());
|
||||
if from == to { format!("{from}") } else { format!("{from}-{to}") }
|
||||
})
|
||||
.collect();
|
||||
let name = |k: &str| c[k].as_str().unwrap_or_default().to_string();
|
||||
let text = crate::i18n::format("motion.collision", &[("a", &name("a")), ("b", &name("b")), ("steps", &steps.join(", "))]);
|
||||
// A click shows where they first collide.
|
||||
if ui.link(RichText::new(text).color(t.error)).clicked()
|
||||
&& let Some(first) = c["first_step"].as_f64()
|
||||
{
|
||||
(*step, *playing) = (first, false);
|
||||
}
|
||||
}
|
||||
if v["truncated"] == true {
|
||||
let to = v["checked_to"].as_f64().map(|x| x.round().to_string()).unwrap_or_default();
|
||||
ui.label(RichText::new(crate::i18n::format("motion.checked_to", &[("step", &to)])).color(t.warning));
|
||||
}
|
||||
});
|
||||
ui.end_row();
|
||||
}
|
||||
Some(Err(e)) => {
|
||||
row_label(ui, "");
|
||||
ui.add(egui::Label::new(RichText::new(e.as_str()).color(t.error)).wrap());
|
||||
ui.end_row();
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// The chart's line: a joint's quantity with its unit, or a point's distance from its start.
|
||||
#[test]
|
||||
fn lines_come_from_the_plot_answer() {
|
||||
let v = json!({"step": [0.0, 1.0, 2.0],
|
||||
"joints": [{"index": 0, "unit": "deg", "velocity_unit": "deg/s", "acceleration_unit": "deg/s²",
|
||||
"value": [0, 10, 30], "velocity": [5, 15, 25], "acceleration": [10, 10, 10]}],
|
||||
"point": {"unit": "mm", "velocity_unit": "mm/step", "acceleration_unit": "mm/step²",
|
||||
"position": [[0, 0, 0], [3, 4, 0], [6, 8, 0]], "speed": [5, 5, 5], "acceleration_norm": [0, 0, 0]}});
|
||||
assert_eq!(line(&v, Plotted::Joint(7, 0), 1), Some((vec![0.0, 1.0, 2.0], vec![5.0, 15.0, 25.0], "deg/s".into())));
|
||||
assert_eq!(line(&v, Plotted::Point, 0).map(|l| l.1), Some(vec![0.0, 5.0, 10.0]));
|
||||
assert_eq!(line(&v, Plotted::Point, 2).map(|l| l.2), Some("mm/step²".into()));
|
||||
assert_eq!(line(&v, Plotted::Joint(7, 1), 0), None);
|
||||
assert_eq!(line(&v, Plotted::Nothing, 0), None);
|
||||
assert_eq!(value_at(&[0.0, 2.0, 4.0], &[0.0, 10.0, 30.0], 3.0), Some(20.0));
|
||||
assert_eq!(value_at(&[0.0, 2.0], &[1.0, 3.0], 9.0), None);
|
||||
let hits = json!({"collisions": [{"steps": [[11.0, 15.0], [20.0, 20.0]]}]});
|
||||
assert_eq!(collision_bands(&hits), vec![[11.0, 15.0], [20.0, 20.0]]);
|
||||
}
|
||||
|
||||
/// The analysis of the open Motion Study dialog, and its step.
|
||||
fn shown(h: &crate::scenario::Harness) -> (Analysis, f64) {
|
||||
match h.app.dialog.as_ref().map(|d| &d.kind) {
|
||||
Some(crate::dialogs::Kind::Motion(crate::dialogs_motion::Mo::MotionStudy { analysis, step, .. })) => ((**analysis).clone(), *step),
|
||||
_ => panic!("no Motion Study dialog"),
|
||||
}
|
||||
}
|
||||
|
||||
/// In the dialog: a slider pushing B 1 mm a step from 10 mm off A to 5 mm into it. Plotting
|
||||
/// the slider's velocity draws a chart of -1 mm/step; a click on the chart moves the step
|
||||
/// there, and Check collisions names A and B from step 11.
|
||||
#[test]
|
||||
fn the_study_dialog_charts_a_joint_and_finds_collisions() {
|
||||
let mut h = crate::scenario::Harness::new();
|
||||
for (name, x) in [("A", 0), ("B", 20)] {
|
||||
h.app.run("component.activate", json!({"component": "root"})).unwrap();
|
||||
h.app.run("component.create", json!({ "name": name })).unwrap();
|
||||
h.app.run("solid.box", json!({"length": 10, "width": 10, "height": 10, "corner": [x, 0, 0]})).unwrap();
|
||||
}
|
||||
h.app.run("component.activate", json!({"component": "root"})).unwrap();
|
||||
h.app.run("occurrence.ground", json!({"occurrence": "A:1", "grounded": true})).unwrap();
|
||||
h.app.run("joint.create", json!({"type": "slider", "a": {"face": [10, 5, 5]}, "b": {"face": [20, 5, 5]}, "name": "Rail"})).unwrap();
|
||||
h.app.run("joint.drive", json!({"joint": "Rail", "value": 10})).unwrap();
|
||||
h.app.run("motion.study", json!({"name": "Push", "steps": 15, "keys": [{"joint": "Rail", "points": [[0, 10], [15, -5]]}]})).unwrap();
|
||||
for bad in [json!({"study": "Nope"}), json!({"study": "Push", "step": 3}), json!({"study": [1]})] {
|
||||
let r = h.call("ui.start", json!({"command": "motion.study", "params": bad}));
|
||||
assert!(r["ok"] == false && h.app.dialog.is_none(), "{bad}: {r}");
|
||||
}
|
||||
// The control channel opens the saved study to edit (and chart).
|
||||
let r = h.call("ui.start", json!({"command": "motion.study", "params": {"study": "Push"}}));
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
if let Some(crate::dialogs::Kind::Motion(crate::dialogs_motion::Mo::MotionStudy { study, steps, tracks, analysis, .. })) =
|
||||
h.app.dialog.as_mut().map(|d| &mut d.kind)
|
||||
{
|
||||
assert_eq!((study.as_deref(), steps.as_str()), (Some("Push"), "15"));
|
||||
assert_eq!(*tracks, vec![crate::dialogs_motion::Track { joint: 0, index: 0, points: vec![(0, "10 mm".into()), (15, "-5 mm".into())] }]);
|
||||
(analysis.what, analysis.quantity) = (1, 1);
|
||||
}
|
||||
h.frames(4);
|
||||
let (a, _) = shown(&h);
|
||||
let plot = a.plot.as_ref().and_then(|(_, r)| r.as_ref().ok()).cloned().unwrap_or_default();
|
||||
let (_, ys, unit) = line(&plot, Plotted::Joint(0, 0), 1).unwrap();
|
||||
assert!(unit == "mm/step" && ys.len() == 16 && ys.iter().all(|v| (v + 1.0).abs() < 1e-9), "{plot}");
|
||||
let chart = crate::scenario::rect_of("motion:chart").unwrap();
|
||||
// The plot area starts 6 points in: two thirds of the way along is step 10.
|
||||
let x = chart.left() + 6.0 + (chart.width() - 12.0) * 2.0 / 3.0;
|
||||
h.step(&json!({"click": [x, chart.center().y]})).unwrap();
|
||||
h.frames(2);
|
||||
assert_eq!(shown(&h).1, 10.0);
|
||||
let [bx, by] = h.widget("Check collisions").unwrap();
|
||||
h.step(&json!({"click": [bx, by]})).unwrap();
|
||||
h.frames(2);
|
||||
let (a, _) = shown(&h);
|
||||
let found = a.collisions.as_ref().and_then(|(_, r)| r.as_ref().ok()).cloned().unwrap_or_default();
|
||||
assert_eq!((&found["collisions"][0]["a"], &found["collisions"][0]["first_step"]), (&json!("A:1"), &json!(11.0)), "{:?}", a.collisions);
|
||||
assert_eq!(collision_bands(&found), vec![[11.0, 15.0]]);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user