- 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.
205 lines
9.5 KiB
Rust
205 lines
9.5 KiB
Rust
//! Motion Trace: the path a picked point of a component takes while a joint is driven between
|
|
//! two values or a motion study plays. The path is computed on a scratch copy (`motion.trace`)
|
|
//! and drawn in the viewport; OK keeps it as a 3D sketch spline (`motion.trace.keep`).
|
|
|
|
use egui::{Pos2, RichText, Stroke};
|
|
use serde_json::{Value, json};
|
|
use solidcraft_engine::doc::{IDENTITY, Mat, apply_point, mat_inverse, mat_mul};
|
|
use solidcraft_engine::geom::Vec3;
|
|
use solidcraft_engine::{Sel, Session};
|
|
|
|
use super::{Hover, body_of, capital, movable, num, pt, sel_items, snap_of};
|
|
use crate::dialogs::{combo, field, row_label};
|
|
use crate::selection::SelInput;
|
|
use crate::theme::Tokens;
|
|
use crate::viewport::Proj;
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct TraceForm {
|
|
/// Traced as a motion study plays (else as a joint is driven from `from` to `to`).
|
|
pub by_study: bool,
|
|
/// Indices into the design's joints and motion studies.
|
|
pub joint: usize,
|
|
pub index: usize,
|
|
pub from: String,
|
|
pub to: String,
|
|
pub study: usize,
|
|
/// Empty: the command's default.
|
|
pub samples: String,
|
|
pub hover: Hover,
|
|
/// The trace shown: what it was computed for, and `motion.trace`'s answer (or why none).
|
|
pub shown: Option<(String, Result<Value, String>)>,
|
|
/// The picked part's frame as shown and the inverse of its placement: the traced path (where
|
|
/// the parts are placed) is drawn moved with the part, through the picked point.
|
|
pub view: Option<(Mat, Mat)>,
|
|
}
|
|
|
|
/// The dialog on a joint (an index into the design's joints), from its value now over a turn.
|
|
pub fn form(s: &Session, joint: usize) -> TraceForm {
|
|
let (from, to) = range(s, joint, 0);
|
|
TraceForm { by_study: false, joint, index: 0, from, to, study: 0, samples: String::new(), hover: None, shown: None, view: None }
|
|
}
|
|
|
|
/// From a joint's value now over a turn (an angle) or 20 mm (a distance).
|
|
fn range(s: &Session, joint: usize, index: usize) -> (String, String) {
|
|
let Some(j) = s.doc.assembly.joints.get(joint) else { return Default::default() };
|
|
let v = j.values.get(index).copied().unwrap_or(0.0);
|
|
if j.kind.is_angle(index) { (num(v.to_degrees(), "deg"), num(v.to_degrees() + 360.0, "deg")) } else { (num(v, "mm"), num(v + 20.0, "mm")) }
|
|
}
|
|
|
|
/// The occurrence path placing the picked body: the instance clicked, also when its component
|
|
/// (or a sub-assembly holding it) is placed more than once (empty: the root design).
|
|
fn path(s: &Session, sel: &Sel) -> Option<Vec<u64>> {
|
|
let body = body_of(sel)?;
|
|
s.doc.body_frames(&s.model.state()).into_iter().find(|(name, ..)| name == body).map(|(_, _, path, _)| path)
|
|
}
|
|
|
|
/// The pick: its occurrence path, the point where the part shows, and the part's frame as shown
|
|
/// and as placed (they differ under an exploded view or a move not captured yet).
|
|
pub(super) fn placement(s: &Session, inputs: &[SelInput]) -> Option<(Vec<u64>, Vec3, Mat, Mat)> {
|
|
let pick = sel_items(inputs, 0).first()?;
|
|
let (snap, path) = (snap_of(s, pick)?, path(s, pick)?);
|
|
let world = |shown: bool| {
|
|
path.iter().try_fold(IDENTITY, |m, id| {
|
|
let o = s.doc.occurrences.iter().find(|o| o.id == *id)?;
|
|
Some(mat_mul(&m, s.pending_moves.get(id).filter(|_| shown).unwrap_or(&o.transform)))
|
|
})
|
|
};
|
|
let (shown, placed) = (world(true)?, world(false)?);
|
|
Some((path, snap.at, shown, placed))
|
|
}
|
|
|
|
/// The trace's parameters (OK's `motion.trace.keep` and the preview's `motion.trace`).
|
|
pub fn params(s: &Session, f: &TraceForm, inputs: &[SelInput]) -> Result<Value, String> {
|
|
let mut p = picked_point(s, inputs)?;
|
|
if f.by_study {
|
|
p["study"] = json!(s.doc.assembly.studies.get(f.study).ok_or("no motion study yet")?.id);
|
|
} else {
|
|
let j = s.doc.assembly.joints.get(f.joint).ok_or("no joint that moves yet")?;
|
|
p["joint"] = json!(j.id);
|
|
p["index"] = json!(f.index);
|
|
p["from"] = json!(f.from.trim());
|
|
p["to"] = json!(f.to.trim());
|
|
}
|
|
if !f.samples.trim().is_empty() {
|
|
p["samples"] = json!(f.samples.trim().parse::<f64>().map_err(|_| "samples must be a number")?);
|
|
}
|
|
Ok(p)
|
|
}
|
|
|
|
/// The picked point as `motion.trace` and `motion.plot` take it: `{occurrence, point}`.
|
|
pub fn picked_point(s: &Session, inputs: &[SelInput]) -> Result<Value, String> {
|
|
let (path, at, shown, placed) = placement(s, inputs).ok_or("select a point on a component")?;
|
|
// The point starts where the part is placed, not where it may be shown.
|
|
let at = mat_inverse(&shown).map(|m| apply_point(&placed, apply_point(&m, at))).ok_or("select a point on a component")?;
|
|
let occurrence = if path.is_empty() { json!(0) } else { json!(path) };
|
|
Ok(json!({"occurrence": occurrence, "point": pt(at)}))
|
|
}
|
|
|
|
/// Where and why the motion stopped short, ending the trace (`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"]))]))
|
|
}
|
|
|
|
/// The dialog's rows; true when Enter was pressed in a value.
|
|
pub fn rows(app: &mut crate::SolveApp, ui: &mut egui::Ui, f: &mut TraceForm, inputs: &[SelInput]) -> bool {
|
|
let t = Tokens::get();
|
|
let s = &app.session;
|
|
let mut enter = false;
|
|
let studies: Vec<&str> = s.doc.assembly.studies.iter().map(|m| m.name.as_str()).collect();
|
|
f.by_study &= !studies.is_empty();
|
|
if !studies.is_empty() {
|
|
row_label(ui, "Motion");
|
|
let mut by = usize::from(f.by_study);
|
|
combo(ui, "tr_by", &["Joint", "Motion Study"], &mut by);
|
|
f.by_study = by == 1;
|
|
ui.end_row();
|
|
}
|
|
if f.by_study {
|
|
row_label(ui, "Study");
|
|
combo(ui, "tr_study", &studies, &mut f.study);
|
|
ui.end_row();
|
|
} else {
|
|
let list = movable(s);
|
|
if list.is_empty() {
|
|
row_label(ui, "");
|
|
ui.label(RichText::new(crate::i18n::tr_label("no joint that moves yet")).color(t.text_dim));
|
|
ui.end_row();
|
|
return false;
|
|
}
|
|
let labels: Vec<&str> = list.iter().filter_map(|i| s.doc.assembly.joints.get(*i)).map(|j| j.name.as_str()).collect();
|
|
let mut pos = list.iter().position(|i| *i == f.joint).unwrap_or(0);
|
|
row_label(ui, "Joint");
|
|
combo(ui, "tr_joint", &labels, &mut pos);
|
|
ui.end_row();
|
|
let joint = list.get(pos).copied().unwrap_or(0);
|
|
let dofs = s.doc.assembly.joints.get(joint).map(|j| j.kind.dofs()).unwrap_or(&[]);
|
|
let mut index = if joint == f.joint { f.index.min(dofs.len().saturating_sub(1)) } else { 0 };
|
|
if dofs.len() > 1 {
|
|
row_label(ui, " Of");
|
|
let caps: Vec<String> = dofs.iter().map(|d| capital(d)).collect();
|
|
let refs: Vec<&str> = caps.iter().map(String::as_str).collect();
|
|
combo(ui, "tr_index", &refs, &mut index);
|
|
ui.end_row();
|
|
}
|
|
if joint != f.joint || index != f.index {
|
|
(f.joint, f.index) = (joint, index);
|
|
(f.from, f.to) = range(s, joint, index);
|
|
}
|
|
row_label(ui, "From");
|
|
enter |= field(ui, &mut f.from);
|
|
ui.end_row();
|
|
row_label(ui, "To");
|
|
enter |= field(ui, &mut f.to);
|
|
ui.end_row();
|
|
}
|
|
let default = match s.doc.assembly.studies.get(f.study).filter(|_| f.by_study) {
|
|
Some(m) => (u64::from(m.steps) + 1).clamp(2, 500),
|
|
None => 100,
|
|
};
|
|
row_label(ui, "Samples");
|
|
let r = ui.add(egui::TextEdit::singleline(&mut f.samples).hint_text(default.to_string()));
|
|
enter |= r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter));
|
|
ui.end_row();
|
|
// The preview, traced again when what it traces (or the design) changes.
|
|
let p = params(s, f, inputs);
|
|
let key = format!("{}:{p:?}", s.revision);
|
|
if f.shown.as_ref().is_none_or(|(k, _)| *k != key) {
|
|
let r = p.and_then(|p| s.scratch().execute("motion.trace", &p).map_err(|e| e.to_string()));
|
|
f.shown = Some((key, r));
|
|
f.view = placement(s, inputs).and_then(|(_, _, shown, placed)| Some((shown, mat_inverse(&placed)?)));
|
|
}
|
|
if let Some((_, r)) = &f.shown {
|
|
let (text, colour) = match r {
|
|
Ok(v) => match stop_text(v) {
|
|
Some(why) => (why, t.warning),
|
|
None => (crate::i18n::format("trace.points", &[("count", &v["samples"].to_string())]), t.text_dim),
|
|
},
|
|
Err(e) => (e.clone(), t.text_dim),
|
|
};
|
|
row_label(ui, "");
|
|
ui.add(egui::Label::new(RichText::new(text).color(colour)).wrap());
|
|
ui.end_row();
|
|
}
|
|
enter
|
|
}
|
|
|
|
/// The traced path in the viewport, with a dot where it starts.
|
|
pub fn overlay(painter: &egui::Painter, proj: &Proj, f: &TraceForm) {
|
|
let Some((_, Ok(v))) = &f.shown else { return };
|
|
let at = |p: &Value| p.as_array().and_then(|a| Some(Vec3::new(a.first()?.as_f64()?, a.get(1)?.as_f64()?, a.get(2)?.as_f64()?)));
|
|
let shown = |p: Vec3| f.view.map_or(p, |(shown, inverse)| apply_point(&shown, apply_point(&inverse, p)));
|
|
let pts: Vec<Pos2> = v["points"].as_array().into_iter().flatten().filter_map(at).map(shown).filter_map(|p| proj.to_screen(p)).collect();
|
|
let t = Tokens::get();
|
|
if let Some(first) = pts.first() {
|
|
painter.circle_filled(*first, 3.5, t.preview_sketch);
|
|
}
|
|
painter.add(egui::Shape::line(pts, Stroke::new(2.0, t.preview_sketch)));
|
|
}
|