Motion analysis review round 1: unwrap only followers and past half a turn, studies by id, refuse keys between steps, count mesh placement work, duration checked in the dialog, Browser joint pick charted
This commit is contained in:
1 parent
f5871608f4
commit
7119d60d9d
7 files changed
+94
-25
No files matched your search
@@ -14,7 +14,9 @@
|
|||||||
"Bends": "Biegungen",
|
"Bends": "Biegungen",
|
||||||
"Boundary": "Begrenzung",
|
"Boundary": "Begrenzung",
|
||||||
"Centre to centre": "Mitte zu Mitte",
|
"Centre to centre": "Mitte zu Mitte",
|
||||||
|
"Check collisions": "Kollisionen prüfen",
|
||||||
"Circumference": "Umfang",
|
"Circumference": "Umfang",
|
||||||
|
"Collisions": "Kollisionen",
|
||||||
"Construction": "Konstruktionslinie",
|
"Construction": "Konstruktionslinie",
|
||||||
"Contact sets": "Kontaktsätze",
|
"Contact sets": "Kontaktsätze",
|
||||||
"Continuity": "Stetigkeit",
|
"Continuity": "Stetigkeit",
|
||||||
|
|||||||
@@ -14,7 +14,9 @@
|
|||||||
"Bends": "벤딩",
|
"Bends": "벤딩",
|
||||||
"Boundary": "경계",
|
"Boundary": "경계",
|
||||||
"Centre to centre": "중심 간",
|
"Centre to centre": "중심 간",
|
||||||
|
"Check collisions": "충돌 확인",
|
||||||
"Circumference": "원주",
|
"Circumference": "원주",
|
||||||
|
"Collisions": "충돌",
|
||||||
"Construction": "구성선",
|
"Construction": "구성선",
|
||||||
"Contact sets": "접촉 집합",
|
"Contact sets": "접촉 집합",
|
||||||
"Continuity": "연속성",
|
"Continuity": "연속성",
|
||||||
|
|||||||
@@ -155,6 +155,11 @@ impl ContactGeometry {
|
|||||||
self.pairs.retain(|(_, _, a, b)| keep(a.0, b.0));
|
self.pairs.retain(|(_, _, a, b)| keep(a.0, b.0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// How many mesh points are placed for each check.
|
||||||
|
pub(super) fn vertex_count(&self) -> usize {
|
||||||
|
self.bodies.iter().map(|b| b.mesh.positions.len()).fold(0, usize::saturating_add)
|
||||||
|
}
|
||||||
|
|
||||||
/// How many pairs of set members are checked against each other.
|
/// How many pairs of set members are checked against each other.
|
||||||
pub(super) fn pair_count(&self) -> usize {
|
pub(super) fn pair_count(&self) -> usize {
|
||||||
self.pairs.iter().map(|(_, _, a, b)| (a.0, b.0)).collect::<BTreeSet<_>>().len()
|
self.pairs.iter().map(|(_, _, a, b)| (a.0, b.0)).collect::<BTreeSet<_>>().len()
|
||||||
@@ -242,6 +247,8 @@ impl ContactGeometry {
|
|||||||
Ok(Mesh { positions, triangles: body.mesh.triangles.clone(), ..Mesh::default() })
|
Ok(Mesh { positions, triangles: body.mesh.triangles.clone(), ..Mesh::default() })
|
||||||
})
|
})
|
||||||
.collect::<Result<_>>()?;
|
.collect::<Result<_>>()?;
|
||||||
|
// Each mesh's box once, not once per pair it is in.
|
||||||
|
let boxes: Vec<Aabb3> = meshes.iter().map(Mesh::bounds).collect();
|
||||||
for (a, b, oa, ob) in &self.pairs {
|
for (a, b, oa, ob) in &self.pairs {
|
||||||
// Members already found colliding need no more of their bodies tested.
|
// Members already found colliding need no more of their bodies tested.
|
||||||
if out.iter().any(|(x, y, _)| (x.0, y.0) == (oa.0, ob.0) || (x.0, y.0) == (ob.0, oa.0)) {
|
if out.iter().any(|(x, y, _)| (x.0, y.0) == (oa.0, ob.0) || (x.0, y.0) == (ob.0, oa.0)) {
|
||||||
@@ -250,7 +257,9 @@ impl ContactGeometry {
|
|||||||
let (Some(ma), Some(mb), Some(ba), Some(bb)) = (meshes.get(*a), meshes.get(*b), self.bodies.get(*a), self.bodies.get(*b)) else {
|
let (Some(ma), Some(mb), Some(ba), Some(bb)) = (meshes.get(*a), meshes.get(*b), self.bodies.get(*a), self.bodies.get(*b)) else {
|
||||||
return Err(limit("invalid contact geometry pair"));
|
return Err(limit("invalid contact geometry pair"));
|
||||||
};
|
};
|
||||||
let (aabb, bbb) = (ma.bounds(), mb.bounds());
|
let (Some(aabb), Some(bbb)) = (boxes.get(*a), boxes.get(*b)) else {
|
||||||
|
return Err(limit("invalid contact geometry pair"));
|
||||||
|
};
|
||||||
if aabb.max.x < bbb.min.x
|
if aabb.max.x < bbb.min.x
|
||||||
|| bbb.max.x < aabb.min.x
|
|| bbb.max.x < aabb.min.x
|
||||||
|| aabb.max.y < bbb.min.y
|
|| aabb.max.y < bbb.min.y
|
||||||
|
|||||||
@@ -128,12 +128,14 @@ fn second_derivative(v: &[f64], h: f64) -> Vec<f64> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Angles in degrees made continuous: a jump of more than half a turn between samples is taken
|
/// Angles in degrees made continuous: a jump of more than half a turn between samples is taken
|
||||||
/// as the angle going round.
|
/// as the angle going round (half a turn exactly is a real step).
|
||||||
fn unwrap_degrees(v: &mut [f64]) {
|
fn unwrap_degrees(v: &mut [f64]) {
|
||||||
let (mut turns, mut prev) = (0.0, None::<f64>);
|
let (mut turns, mut prev) = (0.0, None::<f64>);
|
||||||
for x in v.iter_mut() {
|
for x in v.iter_mut() {
|
||||||
let raw = *x;
|
let raw = *x;
|
||||||
if let Some(p) = prev {
|
if let Some(p) = prev
|
||||||
|
&& (raw - p).abs() > 180.0
|
||||||
|
{
|
||||||
turns -= 360.0 * ((raw - p) / 360.0).round();
|
turns -= 360.0 * ((raw - p) / 360.0).round();
|
||||||
}
|
}
|
||||||
prev = Some(raw);
|
prev = Some(raw);
|
||||||
@@ -175,6 +177,7 @@ pub(super) fn plot(s: &mut Session, p: &Value) -> Result<Value> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
let series = plotted(s, p, cmd)?;
|
let series = plotted(s, p, cmd)?;
|
||||||
|
let given = keyed(&m);
|
||||||
let point = if p.get("point").is_some() || p.get("occurrence").is_some() { Some(traced_point(s, p, cmd)?) } else { None };
|
let point = if p.get("point").is_some() || p.get("occurrence").is_some() { Some(traced_point(s, p, cmd)?) } else { None };
|
||||||
let mut values: Vec<Vec<f64>> = vec![Vec::new(); series.len()];
|
let mut values: Vec<Vec<f64>> = vec![Vec::new(); series.len()];
|
||||||
let mut path: Vec<Vec3> = Vec::new();
|
let mut path: Vec<Vec3> = Vec::new();
|
||||||
@@ -208,7 +211,11 @@ pub(super) fn plot(s: &mut Session, p: &Value) -> Result<Value> {
|
|||||||
let unit = if angle { "deg" } else { "mm" };
|
let unit = if angle { "deg" } else { "mm" };
|
||||||
if angle {
|
if angle {
|
||||||
v.iter_mut().for_each(|x| *x = x.to_degrees());
|
v.iter_mut().for_each(|x| *x = x.to_degrees());
|
||||||
unwrap_degrees(v);
|
// Keyed values are as the study sets them; loops can hand back a follower's
|
||||||
|
// angle a turn round.
|
||||||
|
if !given.contains(&(*id, *i)) {
|
||||||
|
unwrap_degrees(v);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
json!({"joint": id, "name": j.map(|j| j.name.clone()), "index": i, "dof": j.and_then(|j| j.kind.dofs().get(*i).copied()),
|
json!({"joint": id, "name": j.map(|j| j.name.clone()), "index": i, "dof": j.and_then(|j| j.kind.dofs().get(*i).copied()),
|
||||||
"unit": unit, "velocity_unit": format!("{unit}/{per}"), "acceleration_unit": format!("{unit}/{per}²"),
|
"unit": unit, "velocity_unit": format!("{unit}/{per}"), "acceleration_unit": format!("{unit}/{per}²"),
|
||||||
@@ -347,6 +354,12 @@ pub(super) fn interference(s: &mut Session, p: &Value) -> Result<Value> {
|
|||||||
let mut found: BTreeMap<(u64, u64), Collision> = BTreeMap::new();
|
let mut found: BTreeMap<(u64, u64), Collision> = BTreeMap::new();
|
||||||
let (mut work, mut checked, mut truncated) = (0usize, 0usize, false);
|
let (mut work, mut checked, mut truncated) = (0usize, 0usize, false);
|
||||||
let stop = play(s, &m, n, false, |t, k| {
|
let stop = play(s, &m, n, false, |t, k| {
|
||||||
|
// Placing the meshes is work too, also where nothing comes near.
|
||||||
|
work = work.saturating_add(geometry.vertex_count());
|
||||||
|
if work > MAX_WORK {
|
||||||
|
truncated = true;
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
let hits: Vec<(Member, Member, f64)> = match geometry.hits(t, &mut work, MAX_WORK, true) {
|
let hits: Vec<(Member, Member, f64)> = match geometry.hits(t, &mut work, MAX_WORK, true) {
|
||||||
Ok(h) => h,
|
Ok(h) => h,
|
||||||
Err(_) if work > MAX_WORK => {
|
Err(_) if work > MAX_WORK => {
|
||||||
@@ -428,5 +441,9 @@ mod tests {
|
|||||||
let mut v = vec![170.0, 179.0, -172.0, -160.0, 175.0];
|
let mut v = vec![170.0, 179.0, -172.0, -160.0, 175.0];
|
||||||
unwrap_degrees(&mut v);
|
unwrap_degrees(&mut v);
|
||||||
assert_eq!(v, vec![170.0, 179.0, 188.0, 200.0, 175.0]);
|
assert_eq!(v, vec![170.0, 179.0, 188.0, 200.0, 175.0]);
|
||||||
|
// Half a turn a sample is a step, not a wrap.
|
||||||
|
let mut v = vec![0.0, 180.0, 360.0];
|
||||||
|
unwrap_degrees(&mut v);
|
||||||
|
assert_eq!(v, vec![0.0, 180.0, 360.0]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -40,7 +40,7 @@ pub enum Mo {
|
|||||||
},
|
},
|
||||||
MotionStudy {
|
MotionStudy {
|
||||||
name: String,
|
name: String,
|
||||||
/// Editing this study (its name).
|
/// Editing this study (its id).
|
||||||
study: Option<String>,
|
study: Option<String>,
|
||||||
steps: String,
|
steps: String,
|
||||||
tracks: Vec<Track>,
|
tracks: Vec<Track>,
|
||||||
@@ -203,10 +203,19 @@ fn transport(ui: &mut egui::Ui, what: Transport, tip: &str) -> egui::Response {
|
|||||||
resp.on_hover_text(tip)
|
resp.on_hover_text(tip)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Study `key` (an id or a name) as the Motion Study dialog edits it: its name, steps and keys
|
/// Study `key` (an id or a name) as the Motion Study dialog edits it: (id, name, steps, keys as
|
||||||
/// as tracks (values in degrees and mm).
|
/// tracks with values in degrees and mm). The dialog keys whole steps, so a study keyed between
|
||||||
pub fn study_tracks(s: &Session, key: &str) -> Result<(String, String, Vec<Track>), String> {
|
/// them (made by a command) is refused rather than rounded.
|
||||||
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}`"))?;
|
pub fn study_tracks(s: &Session, key: &str) -> Result<(u64, String, String, Vec<Track>), String> {
|
||||||
|
let studies = &s.doc.assembly.studies;
|
||||||
|
let m = studies
|
||||||
|
.iter()
|
||||||
|
.find(|m| m.id.to_string() == key)
|
||||||
|
.or_else(|| studies.iter().find(|m| m.name == key))
|
||||||
|
.ok_or_else(|| format!("no motion study `{key}`"))?;
|
||||||
|
if m.keys.iter().flat_map(|k| &k.points).any(|(st, _)| st.fract() != 0.0) {
|
||||||
|
return Err(format!("{} has keys between whole steps, which this dialog can't keep: edit it with motion.study", m.name));
|
||||||
|
}
|
||||||
let tracks = m
|
let tracks = m
|
||||||
.keys
|
.keys
|
||||||
.iter()
|
.iter()
|
||||||
@@ -218,7 +227,7 @@ pub fn study_tracks(s: &Session, key: &str) -> Result<(String, String, Vec<Track
|
|||||||
Some(Track { joint, index: k.index, points })
|
Some(Track { joint, index: k.index, points })
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
Ok((m.name.clone(), m.steps.to_string(), tracks))
|
Ok((m.id, m.name.clone(), m.steps.to_string(), tracks))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// What the Motion Study dialog opens on when the control channel's `ui.start` passes `params`:
|
/// What the Motion Study dialog opens on when the control channel's `ui.start` passes `params`:
|
||||||
@@ -235,8 +244,8 @@ pub fn preset(s: &Session, k: &mut Mo, p: &Value) -> Result<(), String> {
|
|||||||
Some(_) => return Err("`study` must be an id or a name".into()),
|
Some(_) => return Err("`study` must be an id or a name".into()),
|
||||||
None => return Ok(()),
|
None => return Ok(()),
|
||||||
};
|
};
|
||||||
let (n, st, t) = study_tracks(s, &key)?;
|
let (id, n, st, t) = study_tracks(s, &key)?;
|
||||||
(*study, *name, *steps, *tracks, *shown) = (Some(n.clone()), n, st, t, None);
|
(*study, *name, *steps, *tracks, *shown) = (Some(id.to_string()), n, st, t, None);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -481,18 +490,21 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Mo, inputs: &mut [Sel
|
|||||||
}
|
}
|
||||||
Mo::MotionStudy { name, study, steps, tracks, step, playing, looping, speed, shown, saved, error, analysis } => {
|
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.
|
// 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();
|
// By id: two studies may share a name.
|
||||||
if !names.is_empty() {
|
let studies: Vec<(String, String)> = app.session.doc.assembly.studies.iter().map(|m| (m.id.to_string(), m.name.clone())).collect();
|
||||||
let labels: Vec<&str> = std::iter::once("New study").chain(names.iter().map(String::as_str)).collect();
|
if !studies.is_empty() {
|
||||||
let was = study.as_ref().and_then(|n| names.iter().position(|x| x == n)).map_or(0, |i| i + 1);
|
let labels: Vec<&str> = std::iter::once("New study").chain(studies.iter().map(|(_, n)| n.as_str())).collect();
|
||||||
|
let was = study.as_ref().and_then(|id| studies.iter().position(|(x, _)| x == id)).map_or(0, |i| i + 1);
|
||||||
let mut pick = was;
|
let mut pick = was;
|
||||||
row_label(ui, "Study");
|
row_label(ui, "Study");
|
||||||
combo(ui, "ms_study", &labels, &mut pick);
|
combo(ui, "ms_study", &labels, &mut pick);
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
if pick != was {
|
if pick != was {
|
||||||
match pick.checked_sub(1).and_then(|i| names.get(i)).map(|n| study_tracks(&app.session, n)) {
|
match pick.checked_sub(1).and_then(|i| studies.get(i)).map(|(id, _)| study_tracks(&app.session, id)) {
|
||||||
Some(Ok((n, st, t))) => (*study, *name, *steps, *tracks, *shown, *playing) = (Some(n.clone()), n, st, t, None, false),
|
Some(Ok((id, n, st, t))) => {
|
||||||
Some(Err(e)) => *error = Some(e),
|
(*study, *name, *steps, *tracks, *shown, *playing) = (Some(id.to_string()), n, st, t, None, false)
|
||||||
|
}
|
||||||
|
Some(Err(e)) => app.set_status(e, true),
|
||||||
None => *study = None,
|
None => *study = None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ pub struct Analysis {
|
|||||||
pub duration: String,
|
pub duration: String,
|
||||||
/// Collisions among all components (0) or in the design's contact sets (1).
|
/// Collisions among all components (0) or in the design's contact sets (1).
|
||||||
pub among: usize,
|
pub among: usize,
|
||||||
|
/// The joint last picked in the browser, which the chart then shows.
|
||||||
|
followed: Option<u64>,
|
||||||
/// The chart's `motion.plot` answer (or why none) and what it was for.
|
/// The chart's `motion.plot` answer (or why none) and what it was for.
|
||||||
plot: Option<(u64, Result<Value, String>)>,
|
plot: Option<(u64, Result<Value, String>)>,
|
||||||
/// The last collision check's answer and what it was for.
|
/// The last collision check's answer and what it was for.
|
||||||
@@ -77,7 +79,8 @@ pub fn plot_params(s: &Session, what: Plotted, study: &Value, duration: &str, in
|
|||||||
}
|
}
|
||||||
let d = duration.trim();
|
let d = duration.trim();
|
||||||
if !d.is_empty() {
|
if !d.is_empty() {
|
||||||
p["duration"] = json!(d.parse::<f64>().map_err(|_| "the duration must be a number of seconds".to_string())?);
|
let seconds = d.parse::<f64>().ok().filter(|x| x.is_finite() && *x > 0.0).ok_or("the duration must be a number of seconds, more than 0")?;
|
||||||
|
p["duration"] = json!(seconds);
|
||||||
}
|
}
|
||||||
Ok(p)
|
Ok(p)
|
||||||
}
|
}
|
||||||
@@ -230,6 +233,14 @@ pub fn rows(
|
|||||||
let picked = inputs.first().is_some_and(|i| !i.items.is_empty());
|
let picked = inputs.first().is_some_and(|i| !i.items.is_empty());
|
||||||
let list = choices(s, picked);
|
let list = choices(s, picked);
|
||||||
a.what = a.what.min(list.len().saturating_sub(1));
|
a.what = a.what.min(list.len().saturating_sub(1));
|
||||||
|
// A joint picked in the browser is charted (until the box or another pick says otherwise).
|
||||||
|
let browsed = app.tree.picked_joints.first().copied();
|
||||||
|
if browsed != a.followed {
|
||||||
|
a.followed = browsed;
|
||||||
|
if let Some(i) = browsed.and_then(|id| list.iter().position(|(_, w)| *w == Plotted::Joint(id, 0))) {
|
||||||
|
a.what = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
row_label(ui, "Plot");
|
row_label(ui, "Plot");
|
||||||
let labels: Vec<&str> = list.iter().map(|(l, _)| l.as_str()).collect();
|
let labels: Vec<&str> = list.iter().map(|(l, _)| l.as_str()).collect();
|
||||||
combo(ui, "ms_plot", &labels, &mut a.what);
|
combo(ui, "ms_plot", &labels, &mut a.what);
|
||||||
@@ -377,6 +388,15 @@ mod tests {
|
|||||||
assert_eq!(value_at(&[0.0, 2.0], &[1.0, 3.0], 9.0), None);
|
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]]}]});
|
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]]);
|
assert_eq!(collision_bands(&hits), vec![[11.0, 15.0], [20.0, 20.0]]);
|
||||||
|
// A duration is a number of seconds, more than 0; empty is per step.
|
||||||
|
let s = Session::default();
|
||||||
|
let study = json!({"name": "S", "steps": 10});
|
||||||
|
let p = |d: &str| plot_params(&s, Plotted::Joint(7, 0), &study, d, &[]);
|
||||||
|
assert_eq!(p("2").unwrap()["duration"], json!(2.0));
|
||||||
|
assert!(p("").unwrap().get("duration").is_none());
|
||||||
|
for bad in ["1e400", "nan", "-1", "0", "soon"] {
|
||||||
|
assert!(p(bad).is_err(), "{bad}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The analysis of the open Motion Study dialog, and its step.
|
/// The analysis of the open Motion Study dialog, and its step.
|
||||||
@@ -402,8 +422,14 @@ mod tests {
|
|||||||
h.app.run("occurrence.ground", json!({"occurrence": "A:1", "grounded": true})).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.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("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();
|
let push = h
|
||||||
for bad in [json!({"study": "Nope"}), json!({"study": "Push", "step": 3}), json!({"study": [1]})] {
|
.app
|
||||||
|
.run("motion.study", json!({"name": "Push", "steps": 15, "keys": [{"joint": "Rail", "points": [[0, 10], [15, -5]]}]}))
|
||||||
|
.unwrap()["study"]
|
||||||
|
.to_string();
|
||||||
|
// A study keyed between whole steps is not opened (the dialog would round its keys).
|
||||||
|
h.app.run("motion.study", json!({"name": "Fine", "steps": 15, "keys": [{"joint": "Rail", "points": [[0, 10], [7.5, -5]]}]})).unwrap();
|
||||||
|
for bad in [json!({"study": "Nope"}), json!({"study": "Push", "step": 3}), json!({"study": [1]}), json!({"study": "Fine"})] {
|
||||||
let r = h.call("ui.start", json!({"command": "motion.study", "params": bad}));
|
let r = h.call("ui.start", json!({"command": "motion.study", "params": bad}));
|
||||||
assert!(r["ok"] == false && h.app.dialog.is_none(), "{bad}: {r}");
|
assert!(r["ok"] == false && h.app.dialog.is_none(), "{bad}: {r}");
|
||||||
}
|
}
|
||||||
@@ -413,7 +439,7 @@ mod tests {
|
|||||||
if let Some(crate::dialogs::Kind::Motion(crate::dialogs_motion::Mo::MotionStudy { study, steps, tracks, analysis, .. })) =
|
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)
|
h.app.dialog.as_mut().map(|d| &mut d.kind)
|
||||||
{
|
{
|
||||||
assert_eq!((study.as_deref(), steps.as_str()), (Some("Push"), "15"));
|
assert_eq!((study.as_deref(), steps.as_str()), (Some(push.as_str()), "15"));
|
||||||
assert_eq!(*tracks, vec![crate::dialogs_motion::Track { joint: 0, index: 0, points: vec![(0, "10 mm".into()), (15, "-5 mm".into())] }]);
|
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);
|
(analysis.what, analysis.quantity) = (1, 1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -168,8 +168,9 @@ what changed from one frame to the next. Frames are capped: at most 1000, and a
|
|||||||
|
|
||||||
**Motion analysis.** The **Motion Study** dialog's **Study** box opens a saved study to edit, and
|
**Motion analysis.** The **Motion Study** dialog's **Study** box opens a saved study to edit, and
|
||||||
by its timeline the dialog charts what the study does.
|
by its timeline the dialog charts what the study does.
|
||||||
**Plot** picks a moving joint value (or **Picked point**, once a point is picked in the dialog's
|
**Plot** picks a moving joint value (a joint picked in the Browser is picked here too), or
|
||||||
**Point** box) and whether to show its value, velocity or acceleration; the chart runs over the
|
**Picked point** once a point is picked in the dialog's **Point** box, and **Quantity** whether to
|
||||||
|
show its value, velocity or acceleration; the chart runs over the
|
||||||
study's steps with a cursor at the step shown, and a click or a drag on it moves to that step.
|
study's steps with a cursor at the step shown, and a click or a drag on it moves to that step.
|
||||||
Values are in degrees and millimetres; rates are per step, or per second once **Duration** says
|
Values are in degrees and millimetres; rates are per step, or per second once **Duration** says
|
||||||
how many seconds the whole study takes. A picked point charts its distance from where it
|
how many seconds the whole study takes. A picked point charts its distance from where it
|
||||||
|
|||||||
Reference in new issue
Block a user