Motion animation export: a study or a joint drive as an animated GIF (#1680)
Thanks @SychPL.
This commit is contained in:
1 parent
8207f29fc0
commit
2ae21143ee
24 files changed
+902
-33
No files matched your search
@@ -14,6 +14,8 @@ use super::joints::{cannot_move, effective_values, joint_id, occurrence_id, reso
|
||||
use crate::params::{bad, bool_, str_, vec3};
|
||||
use crate::{EngineError, Result, Session};
|
||||
|
||||
#[path = "motion_animation.rs"]
|
||||
mod animation;
|
||||
#[path = "contact_sweep.rs"]
|
||||
mod contact_sweep;
|
||||
|
||||
@@ -54,6 +56,9 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
CommandSpec::new("motion.export", "Export Motion Study", export)
|
||||
.noundo()
|
||||
.params("study; samples? (default steps + 1); path? (.json or .csv) → occurrence positions per step (the design is not changed)"),
|
||||
CommandSpec::new("motion.export_animation", "Export Motion Animation", animation::export).noundo().params(
|
||||
"study: id|name, or joint: id|name with from and to (degrees for angles, mm for distances, or expressions) and index?; path: a .gif file, or a folder for PNG frames (frame-0001.png, frame-0002.png, …); samples? (frames, 2…1000; default the study's steps + 1, or 100); width?, height? (16…4096 pixels, default 800 × 500); camera?: {target, yaw, pitch, roll?, distance, fov?, up?} (ui.camera's reply; fields left out keep the default view: view.apply's camera, or the home view framing the model); fps? (1…50, default 20); grid?, axes? (default true: the ground grid and the origin's axes) - draws the study as it plays, or the joint as it is driven, with the headless renderer → path, format, frames, width, height, fps (GIF), bytes, renderer, stopped_at, stopped_by_loop, stopped_by_contact (the design is not changed)",
|
||||
),
|
||||
CommandSpec::new("motion.trace", "Motion Trace", trace).at("SOLID", "ASSEMBLE").icon("trace").noundo().params(concat!(
|
||||
trace_doc!(),
|
||||
" → points: [[x,y,z]…] world positions, samples (taken), stopped_at (the sample where the motion stopped short; the trace \
|
||||
@@ -718,19 +723,45 @@ fn sample_trace(s: &Session, p: &Value, cmd: &str) -> Result<(Vec<Vec3>, Value)>
|
||||
let local = solidcraft_doc::mat_inverse(&world(&t)?)
|
||||
.map(|m| solidcraft_doc::apply_point(&m, at))
|
||||
.ok_or_else(|| bad(cmd, "the occurrence's placement is not invertible"))?;
|
||||
let samples = |default: usize| match p.get("samples") {
|
||||
let (default, given, plan) = motion_plan(s, p, cmd)?;
|
||||
let n = sample_count(p, default.min(MAX_TRACE), MAX_TRACE, cmd)?;
|
||||
let mut points = Vec::new();
|
||||
let mut stop = (Value::Null, Value::Null, Value::Null);
|
||||
for k in 0..n {
|
||||
let want = plan(&t, k, n);
|
||||
let (r, _) = drive_with_contact(&mut t, &want, &given)?;
|
||||
points.push(solidcraft_doc::apply_point(&world(&t)?, local));
|
||||
if r["stopped_by_loop"].is_object() || r["stopped_by_contact"].is_object() {
|
||||
stop = (json!(k), r["stopped_by_loop"].clone(), r["stopped_by_contact"].clone());
|
||||
break;
|
||||
}
|
||||
}
|
||||
let name = s.doc.occurrences.iter().find(|o| Some(&o.id) == path.last()).map_or_else(|| "root".to_string(), |o| o.name.clone());
|
||||
let out = json!({"occurrence": name, "samples": points.len(), "stopped_at": stop.0, "stopped_by_loop": stop.1, "stopped_by_contact": stop.2});
|
||||
Ok((points, out))
|
||||
}
|
||||
|
||||
/// `samples`, 2…`max`, or `default` without it.
|
||||
fn sample_count(p: &Value, default: usize, max: usize, cmd: &str) -> Result<usize> {
|
||||
match p.get("samples") {
|
||||
Some(v) => v
|
||||
.as_f64()
|
||||
.filter(|n| n.is_finite() && *n >= 2.0 && *n <= MAX_TRACE as f64)
|
||||
.filter(|n| n.is_finite() && *n >= 2.0 && *n <= max as f64)
|
||||
.map(|n| n.round() as usize)
|
||||
.ok_or_else(|| bad(cmd, format!("`samples` must be 2…{MAX_TRACE}"))),
|
||||
None => Ok(default),
|
||||
};
|
||||
let (n, given, plan): (usize, BTreeSet<(u64, usize)>, Plan) = match (p.get("study"), p.get("joint")) {
|
||||
.ok_or_else(|| bad(cmd, format!("`samples` must be 2…{max}"))),
|
||||
None => Ok(default.max(2)),
|
||||
}
|
||||
}
|
||||
|
||||
/// The motion a trace or an animation samples: a study played from start to end, or one joint
|
||||
/// value driven from `from` to `to`. Returns the default sample count (the study's steps + 1, or
|
||||
/// 100), the values driven and what they are at sample `k` of `n`.
|
||||
fn motion_plan(s: &Session, p: &Value, cmd: &str) -> Result<(usize, BTreeSet<(u64, usize)>, Plan)> {
|
||||
Ok(match (p.get("study"), p.get("joint")) {
|
||||
(Some(_), Some(_)) => return Err(bad(cmd, "give `study` or `joint`, not both")),
|
||||
(Some(_), None) => {
|
||||
let m = s.doc.assembly.studies.get(study_index(s, p, cmd)?).cloned().ok_or_else(|| bad(cmd, "study"))?;
|
||||
let n = samples((m.steps as usize).saturating_add(1).clamp(2, MAX_TRACE))?;
|
||||
let n = (m.steps as usize).saturating_add(1);
|
||||
(n, keyed(&m), Box::new(move |t, k, n| values_at(t, &m, f64::from(m.steps) * k as f64 / (n - 1) as f64)))
|
||||
}
|
||||
(None, Some(j)) => {
|
||||
@@ -750,7 +781,7 @@ fn sample_trace(s: &Session, p: &Value, cmd: &str) -> Result<(Vec<Vec3>, Value)>
|
||||
};
|
||||
let end = |key: &str| match p.get(key) {
|
||||
Some(v) => value(s, v, kind.is_angle(index), cmd),
|
||||
None => Err(bad(cmd, format!("give `from` and `to`: the joint's values the trace runs between (no `{key}`)"))),
|
||||
None => Err(bad(cmd, format!("give `from` and `to`: the joint's values the motion runs between (no `{key}`)"))),
|
||||
};
|
||||
let (from, to) = (end("from")?, end("to")?);
|
||||
let plan: Plan = Box::new(move |t, k, n| {
|
||||
@@ -761,24 +792,10 @@ fn sample_trace(s: &Session, p: &Value, cmd: &str) -> Result<(Vec<Vec3>, Value)>
|
||||
}
|
||||
vec![(id, q)]
|
||||
});
|
||||
(samples(100)?, BTreeSet::from([(id, index)]), plan)
|
||||
(100, BTreeSet::from([(id, index)]), plan)
|
||||
}
|
||||
(None, None) => return Err(bad(cmd, "give a `study`, or a `joint` with `from` and `to`")),
|
||||
};
|
||||
let mut points = Vec::new();
|
||||
let mut stop = (Value::Null, Value::Null, Value::Null);
|
||||
for k in 0..n {
|
||||
let want = plan(&t, k, n);
|
||||
let (r, _) = drive_with_contact(&mut t, &want, &given)?;
|
||||
points.push(solidcraft_doc::apply_point(&world(&t)?, local));
|
||||
if r["stopped_by_loop"].is_object() || r["stopped_by_contact"].is_object() {
|
||||
stop = (json!(k), r["stopped_by_loop"].clone(), r["stopped_by_contact"].clone());
|
||||
break;
|
||||
}
|
||||
}
|
||||
let name = s.doc.occurrences.iter().find(|o| Some(&o.id) == path.last()).map_or_else(|| "root".to_string(), |o| o.name.clone());
|
||||
let out = json!({"occurrence": name, "samples": points.len(), "stopped_at": stop.0, "stopped_by_loop": stop.1, "stopped_by_contact": stop.2});
|
||||
Ok((points, out))
|
||||
})
|
||||
}
|
||||
|
||||
fn trace(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
Reference in new issue
Block a user