Motion Trace: a point's path as a joint is driven or a study plays, kept as a 3D spline (#1529)
Thanks @SychPL.
This commit is contained in:
1 parent
dcffab09b1
commit
571122c4ba
19 files changed
+769
-9
No files matched your search
@@ -1179,5 +1179,235 @@ fn a_new_joint_starts_inside_its_limits_and_says_so() {
|
||||
assert!(r.get("warnings").is_none(), "{r}");
|
||||
}
|
||||
|
||||
/// The trace's points as world points.
|
||||
fn traced(r: &Value) -> Vec<Vec3> {
|
||||
r["points"].as_array().into_iter().flatten().filter_map(crate::params::vec3).collect()
|
||||
}
|
||||
|
||||
/// A point rigidly carried from segment (a0, b0) to segment (a, b) in the XY plane.
|
||||
fn carried(p0: Vec3, a0: Vec3, b0: Vec3, a: Vec3, b: Vec3) -> Vec3 {
|
||||
let turn = (b.y - a.y).atan2(b.x - a.x) - (b0.y - a0.y).atan2(b0.x - a0.x);
|
||||
let d = p0 - a0;
|
||||
a + Vec3::new(d.x * turn.cos() - d.y * turn.sin(), d.x * turn.sin() + d.y * turn.cos(), d.z)
|
||||
}
|
||||
|
||||
/// A point on the four-bar's coupler, 10 mm off the line between its pins, traces the coupler
|
||||
/// curve: the crank pin on its circle, the rocker pin where the coupler and rocker circles meet
|
||||
/// (on the open branch), and the point carried with the pins. A motion study turning the crank
|
||||
/// traces the same curve, and the design is left as it was.
|
||||
#[test]
|
||||
fn four_bar_coupler_point_traces_its_closed_form_curve() {
|
||||
let (mut s, p0, q0) = four_bar();
|
||||
let d = (q0 - p0) * (1.0 / q0.dist(p0));
|
||||
let c0 = (p0 + q0) * 0.5 + Vec3::new(-d.y, d.x, 0.0) * 10.0;
|
||||
let before = s.doc.clone();
|
||||
let p = json!({"occurrence": "Coupler", "point": [c0.x, c0.y, c0.z], "joint": "Crank", "from": 0, "to": 360, "samples": 73});
|
||||
let r = run(&mut s, "motion.trace", p);
|
||||
assert_eq!(*s.doc, *before, "a trace leaves the design as it was");
|
||||
assert!(r["stopped_at"].is_null() && r["samples"] == 73, "{r}");
|
||||
let pts = traced(&r);
|
||||
assert_eq!(pts.len(), 73);
|
||||
let mut q = q0;
|
||||
for (k, got) in pts.iter().enumerate() {
|
||||
let v = 5.0 * k as f64;
|
||||
if k > 0 {
|
||||
q = rocker_pin(v - 5.0, v, q);
|
||||
}
|
||||
let th = (90.0 + v).to_radians();
|
||||
let want = carried(c0, p0, q0, Vec3::new(20.0 * th.cos(), 20.0 * th.sin(), 0.0), q);
|
||||
assert!(near(*got, want), "{v}°: {got:?} wants {want:?}");
|
||||
}
|
||||
assert!(pts.first().zip(pts.last()).is_some_and(|(a, b)| near(*a, *b)), "a full turn closes the curve");
|
||||
run(&mut s, "motion.study", json!({"name": "Turn", "steps": 72, "keys": [{"joint": "Crank", "points": [[0, 0], [72, 360]]}]}));
|
||||
let r = run(&mut s, "motion.trace", json!({"occurrence": "Coupler", "point": [c0.x, c0.y, c0.z], "study": "Turn"}));
|
||||
let from_study = traced(&r);
|
||||
assert_eq!(from_study.len(), 73, "{r}");
|
||||
assert!(from_study.iter().zip(&pts).all(|(a, b)| near(*a, *b)), "{r}");
|
||||
}
|
||||
|
||||
/// The middle of a slider-crank's rod: halfway between the crank pin on its circle and the wrist
|
||||
/// pin at a·cos θ + √(b² − (a·sin θ)²).
|
||||
#[test]
|
||||
fn slider_crank_rod_traces_its_closed_form_curve() {
|
||||
let (mut s, p0, s0) = slider_crank();
|
||||
let m0 = (p0 + s0) * 0.5;
|
||||
let r =
|
||||
run(&mut s, "motion.trace", json!({"occurrence": "Rod", "point": [m0.x, m0.y, m0.z], "joint": "Crank", "from": 0, "to": 360, "samples": 37}));
|
||||
let pts = traced(&r);
|
||||
assert_eq!(pts.len(), 37, "{r}");
|
||||
for (k, got) in pts.iter().enumerate() {
|
||||
let th = (90.0 + 10.0 * k as f64).to_radians();
|
||||
let x = 15.0 * th.cos() + (40.0f64.powi(2) - (15.0 * th.sin()).powi(2)).sqrt();
|
||||
let want = (Vec3::new(15.0 * th.cos(), 15.0 * th.sin(), 0.0) + Vec3::new(x, 0.0, 0.0)) * 0.5;
|
||||
assert!(near(*got, want), "{}°: {got:?} wants {want:?}", 10 * k);
|
||||
}
|
||||
}
|
||||
|
||||
/// Driving the rocker past its swing stops where the loop can't close: the trace ends there.
|
||||
#[test]
|
||||
fn a_trace_ends_where_the_loop_stops_the_motion() {
|
||||
let (mut s, _, q0) = four_bar();
|
||||
let start = q0.y.atan2(q0.x - 45.0).to_degrees();
|
||||
let r = run(
|
||||
&mut s,
|
||||
"motion.trace",
|
||||
json!({"occurrence": "Rocker", "point": [q0.x, q0.y, q0.z], "joint": "Rocker", "from": 0, "to": 100, "samples": 21}),
|
||||
);
|
||||
assert!(r["stopped_by_loop"].is_object(), "{r}");
|
||||
let k = r["stopped_at"].as_u64().unwrap_or(u64::MAX) as usize;
|
||||
let pts = traced(&r);
|
||||
assert!(k > 0 && k < 20 && pts.len() == k + 1 && r["samples"] == k + 1, "{r}");
|
||||
let angle = |p: &Vec3| p.y.atan2(p.x - 45.0).to_degrees();
|
||||
for (i, p) in pts.iter().take(k).enumerate() {
|
||||
assert!((angle(p) - (start + 5.0 * i as f64)).abs() < 1e-6, "sample {i}: {}", angle(p));
|
||||
}
|
||||
let limit = ((900.0f64 - 3625.0) / 3600.0).acos().to_degrees();
|
||||
let last = pts.last().map(angle).unwrap_or(f64::NAN);
|
||||
assert!(last > limit - 5.0 && last <= limit + 1e-9, "stopped at {last}°, the limit is {limit}°");
|
||||
}
|
||||
|
||||
/// Keep makes a 3D sketch spline through the trace, in one undo step.
|
||||
#[test]
|
||||
fn a_kept_trace_is_a_3d_sketch_spline() {
|
||||
let (mut s, p0, q0) = four_bar();
|
||||
let c0 = (p0 + q0) * 0.5;
|
||||
let p = json!({"occurrence": "Coupler", "point": [c0.x, c0.y, c0.z], "joint": "Crank", "from": 0, "to": 180, "samples": 19});
|
||||
let pts = traced(&run(&mut s, "motion.trace", p.clone()));
|
||||
let features = s.doc.features.len();
|
||||
let r = run(&mut s, "motion.trace.keep", p);
|
||||
assert_eq!((r["name"].as_str(), r["points"].as_u64()), (Some("Motion Trace1"), Some(19)), "{r}");
|
||||
assert!(s.active_sketch.is_none(), "the sketch is left");
|
||||
let id = r["sketch"].as_u64().unwrap_or(0);
|
||||
let sk = s.doc.sketch(id).cloned().unwrap_or_default();
|
||||
assert!(sk.view.three_d && sk.wires.len() == 1, "{r}");
|
||||
let fit = sk.wires.first().map(|w| w.fit.clone()).unwrap_or_default();
|
||||
assert!(fit.len() == pts.len() && fit.iter().zip(&pts).all(|(a, b)| a.dist(*b) < 1e-9), "{fit:?}");
|
||||
let again = json!({"occurrence": "Coupler", "point": [0, 0, 0], "joint": "Crank", "from": 0, "to": 90});
|
||||
assert_eq!(run(&mut s, "motion.trace.keep", again)["name"], "Motion Trace2");
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
assert_eq!(s.doc.features.len(), features);
|
||||
// A point on the ground does not move: there is nothing to keep.
|
||||
let e = s.execute("motion.trace.keep", &json!({"occurrence": "Ground", "point": [0, 0, 0], "joint": "Crank", "from": 0, "to": 90})).unwrap_err();
|
||||
assert!(e.to_string().contains("does not move"), "{e}");
|
||||
}
|
||||
|
||||
/// A full turn kept closes exactly on its start, also when the sample before the end lies a hair
|
||||
/// from it (a study keyed to 359.99999° one step before 360°).
|
||||
#[test]
|
||||
fn a_kept_full_turn_closes_on_its_start() {
|
||||
let mut s = Session::default();
|
||||
part(&mut s, "Ground", [-1.0, -1.0, -5.0]);
|
||||
part(&mut s, "Arm", [0.0, -1.0, 0.0]);
|
||||
let g = occ(&s, "Ground");
|
||||
run(&mut s, "occurrence.ground", json!({"occurrence": g, "grounded": true}));
|
||||
pin(&mut s, "revolute", "Pivot", "Ground", "Arm", Vec3::ZERO, Vec3::Z);
|
||||
run(&mut s, "motion.study", json!({"name": "Turn", "steps": 5, "keys": [{"joint": "Pivot", "points": [[0, 0], [4, 359.99999], [5, 360]]}]}));
|
||||
let r = run(&mut s, "motion.trace.keep", json!({"occurrence": "Arm", "point": [1, 0, 0], "study": "Turn"}));
|
||||
let id = r["sketch"].as_u64().unwrap_or(0);
|
||||
let fit = s.doc.sketch(id).ok().and_then(|sk| sk.wires.first().map(|w| w.fit.clone())).unwrap_or_default();
|
||||
assert_eq!(fit.len(), 5, "{fit:?}");
|
||||
assert_eq!(fit.first(), fit.last(), "{fit:?}");
|
||||
}
|
||||
|
||||
/// Kept while a component is active and shown moved (a move not captured yet): the spline goes
|
||||
/// in the root, where no joint moves it, through the traced points; the component stays active.
|
||||
#[test]
|
||||
fn a_trace_kept_in_a_moved_component_stays_on_the_path() {
|
||||
let (mut s, p0, q0) = four_bar();
|
||||
let crank = occ(&s, "Crank");
|
||||
let c0 = (p0 + q0) * 0.5;
|
||||
let p = json!({"occurrence": "Coupler", "point": [c0.x, c0.y, c0.z], "joint": "Crank", "from": 0, "to": 90, "samples": 4});
|
||||
let pts = traced(&run(&mut s, "motion.trace", p.clone()));
|
||||
run(&mut s, "component.activate", json!({"component": "Crank", "occurrence": crank, "path": [crank]}));
|
||||
run(&mut s, "occurrence.move", json!({"occurrence": crank, "translate": [0, 0, 100], "capture": false}));
|
||||
let active = (s.active_component, s.active_occurrence_path.clone());
|
||||
let r = run(&mut s, "motion.trace.keep", p);
|
||||
assert_eq!((s.active_component, s.active_occurrence_path.clone()), active);
|
||||
let id = r["sketch"].as_u64().unwrap_or(0);
|
||||
assert_eq!(s.doc.feature(id).map(|f| f.component), Some(0), "{r}");
|
||||
let fit = s.doc.sketch(id).ok().and_then(|sk| sk.wires.first().map(|w| w.fit.clone())).unwrap_or_default();
|
||||
assert!(fit.len() == pts.len() && fit.iter().zip(&pts).all(|(a, b)| near(*a, *b)), "{fit:?} wants {pts:?}");
|
||||
}
|
||||
|
||||
/// A part inside a sub-assembly placed twice: the occurrence path picks the instance traced. The
|
||||
/// second Arm turns about its pivot and its Tip goes round; the first Arm, and so the Tip at its
|
||||
/// first placement, stays put.
|
||||
#[test]
|
||||
fn a_trace_follows_the_instance_its_path_names() {
|
||||
let mut s = Session::default();
|
||||
part(&mut s, "Ground", [-1.0, -1.0, -5.0]);
|
||||
let g = occ(&s, "Ground");
|
||||
run(&mut s, "occurrence.ground", json!({"occurrence": g, "grounded": true}));
|
||||
run(&mut s, "component.activate", json!({"component": "root"}));
|
||||
let arm = run(&mut s, "component.create", json!({"name": "Arm"}));
|
||||
run(&mut s, "component.create", json!({"name": "Tip"}));
|
||||
run(&mut s, "solid.box", json!({"length": 2, "width": 2, "height": 2, "corner": [10, 0, 0]}));
|
||||
run(&mut s, "component.activate", json!({"component": "root"}));
|
||||
let second = run(&mut s, "occurrence.copy", json!({"component": arm["component"], "translate": [0, 50, 0]}))["occurrence"].as_u64().unwrap_or(0);
|
||||
let at = |o: Value| json!({"occurrence": o, "point": [0, 50, 0], "z": [0, 0, 1]});
|
||||
run(&mut s, "joint.create", json!({"type": "revolute", "a": at(json!(g)), "b": at(json!(second)), "flip": true, "name": "Pivot"}));
|
||||
let tip = occ(&s, "Tip");
|
||||
let trace = |s: &mut Session, o: Value| {
|
||||
traced(&run(s, "motion.trace", json!({"occurrence": o, "point": [11, 51, 1], "joint": "Pivot", "from": 0, "to": 90, "samples": 3})))
|
||||
};
|
||||
let turned = trace(&mut s, json!([second, tip]));
|
||||
let pivot = Vec3::new(0.0, 50.0, 1.0);
|
||||
assert!(turned.iter().all(|p| (p.dist(pivot) - 122f64.sqrt()).abs() < 1e-6), "{turned:?}");
|
||||
assert!(turned.first().zip(turned.last()).is_some_and(|(a, b)| (a.dist(*b) - 244f64.sqrt()).abs() < 1e-6), "a quarter turn: {turned:?}");
|
||||
let first = trace(&mut s, json!(tip));
|
||||
assert!(first.iter().all(|p| p.dist(Vec3::new(11.0, 51.0, 1.0)) < 1e-9), "{first:?}");
|
||||
assert!(s.execute("motion.trace", &json!({"occurrence": [tip, second], "point": [0, 0, 0], "joint": "Pivot", "from": 0, "to": 90})).is_err());
|
||||
assert!(s.execute("motion.trace", &json!({"occurrence": vec![second; 65], "point": [0, 0, 0], "joint": "Pivot", "from": 0, "to": 90})).is_err());
|
||||
}
|
||||
|
||||
/// Trace params come from scripts and the control channel: bad ones are refused, never a panic.
|
||||
#[test]
|
||||
fn hostile_trace_params_are_refused() {
|
||||
let (mut s, _, _) = four_bar();
|
||||
run(&mut s, "motion.study", json!({"name": "Turn", "steps": 4, "keys": [{"joint": "Crank", "points": [[0, 0], [4, 90]]}]}));
|
||||
let base = json!({"occurrence": "Coupler", "point": [0, 0, 0], "joint": "Crank", "from": 0, "to": 90});
|
||||
let with = |k: &str, v: Value| {
|
||||
let mut p = base.clone();
|
||||
p[k] = v;
|
||||
p
|
||||
};
|
||||
let without = |k: &str| {
|
||||
let mut p = base.clone();
|
||||
p.as_object_mut().map(|o| o.remove(k));
|
||||
p
|
||||
};
|
||||
for p in [
|
||||
with("samples", json!(0)),
|
||||
with("samples", json!(1)),
|
||||
with("samples", json!(501)),
|
||||
with("samples", json!(1e300)),
|
||||
with("samples", json!(-3)),
|
||||
with("samples", json!("many")),
|
||||
with("point", json!([f64::MAX, 0, 0])),
|
||||
with("point", json!([0, 0])),
|
||||
with("point", json!("here")),
|
||||
without("point"),
|
||||
with("occurrence", json!("Nowhere")),
|
||||
without("occurrence"),
|
||||
with("joint", json!("Nowhere")),
|
||||
with("index", json!(1)),
|
||||
with("index", json!(-1)),
|
||||
with("from", json!("nonsense +")),
|
||||
with("to", json!(1e300)),
|
||||
without("from"),
|
||||
without("to"),
|
||||
with("study", json!("Turn")),
|
||||
with("study", json!(null)),
|
||||
without("joint"),
|
||||
] {
|
||||
for cmd in ["motion.trace", "motion.trace.keep"] {
|
||||
assert!(s.execute(cmd, &p).is_err(), "{cmd} {p}");
|
||||
}
|
||||
}
|
||||
run(&mut s, "joint.as_built", json!({"type": "rigid", "a": "Ground", "b": "Crank", "name": "Lock"}));
|
||||
assert!(s.execute("motion.trace", &with("joint", json!("Lock"))).is_err(), "a rigid joint does not move");
|
||||
}
|
||||
|
||||
#[path = "joints_coupling_tests.rs"]
|
||||
mod coupling;
|
||||
@@ -17,6 +17,14 @@ use crate::{EngineError, Result, Session};
|
||||
#[path = "contact_sweep.rs"]
|
||||
mod contact_sweep;
|
||||
|
||||
macro_rules! trace_doc {
|
||||
() => {
|
||||
"occurrence: id|name, or an occurrence path [ids] (an instance inside sub-assemblies); point: [x,y,z] world point on it, where it is placed now; study: id|name, or joint: id|name with from and \
|
||||
to (degrees for angles, mm for distances, or expressions) and index? (which value, default 0); samples? (2…500, default \
|
||||
the study's steps + 1, or 100) - the point's path as the study plays or the joint is driven, closed loops following"
|
||||
};
|
||||
}
|
||||
|
||||
pub static COMMANDS: &[CommandSpec] = &[
|
||||
CommandSpec::new("contact.enable_sets", "Enable Contact Sets", enable_sets).at("SOLID", "ASSEMBLE").icon("joint").params(
|
||||
"enabled?: bool (default true) — driving joints and motion studies stop where bodies in one contact set would pass through each other",
|
||||
@@ -46,6 +54,16 @@ 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.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 \
|
||||
ends there), stopped_by_loop, stopped_by_contact (the design is not changed)"
|
||||
)),
|
||||
CommandSpec::new("motion.trace.keep", "Keep Motion Trace", keep_trace).params(concat!(
|
||||
trace_doc!(),
|
||||
"; name? (default Motion TraceN) - keeps the trace as a 3D sketch spline through its points (a new sketch with 3D Sketch \
|
||||
on, in the root component, where no joint moves it; a fixed curve, it does not follow later changes) → sketch, name, curves, points"
|
||||
)),
|
||||
CommandSpec::new("motion.list", "List Motion Studies", list_studies).noundo(),
|
||||
CommandSpec::new("motion.delete", "Delete Motion Study", delete_study).params("study"),
|
||||
CommandSpec::new("explode.create", "Exploded View", explode_create).at("SOLID", "ASSEMBLE").icon("joint").params(
|
||||
@@ -658,6 +676,166 @@ fn export(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Most samples of a motion trace: each is a solve, and a kept trace keeps every one as a fit
|
||||
/// point of its spline.
|
||||
const MAX_TRACE: usize = solidcraft_sketch::MAX_FIT_POINTS;
|
||||
|
||||
/// The joint values a trace drives to at sample `k` of `n`.
|
||||
type Plan = Box<dyn Fn(&Session, usize, usize) -> Vec<(u64, Vec<f64>)>>;
|
||||
|
||||
/// Where a point of an occurrence goes as a study plays or a joint is driven: its world
|
||||
/// positions, sampled on a copy (the design is not changed). Each sample is driven from the one
|
||||
/// before, so closed loops follow on their branch and contact sets stop them; the trace ends at
|
||||
/// the sample where the motion stops short. Returns the points and the answer without them.
|
||||
fn sample_trace(s: &Session, p: &Value, cmd: &str) -> Result<(Vec<Vec3>, Value)> {
|
||||
// The placement traced: an occurrence path through nested sub-assemblies (one instance of
|
||||
// each), or an occurrence at its first placement (none: the root).
|
||||
let path: Vec<u64> = match p.get("occurrence") {
|
||||
Some(Value::Array(ids)) if ids.len() <= 64 => {
|
||||
ids.iter().map(|v| v.as_u64().ok_or_else(|| bad(cmd, "an occurrence path lists occurrence ids"))).collect::<Result<_>>()?
|
||||
}
|
||||
Some(Value::Array(_)) => return Err(bad(cmd, "an occurrence path has at most 64 ids")),
|
||||
v => {
|
||||
let occ = occurrence_id(s, v, cmd)?;
|
||||
match s.doc.occurrences.iter().find(|o| o.id == occ) {
|
||||
Some(o) => s.doc.first_path(o.parent).into_iter().chain([o.id]).collect(),
|
||||
None => Vec::new(),
|
||||
}
|
||||
}
|
||||
};
|
||||
let at = p.get("point").and_then(vec3).filter(|v| v.len() < 1e7).ok_or_else(|| bad(cmd, "`point` must be [x, y, z], a world point"))?;
|
||||
let mut t = s.scratch();
|
||||
// Traced from where the parts are placed: moves only shown (an exploded view, a move not
|
||||
// captured yet) are not where the joints are solved.
|
||||
t.pending_moves.clear();
|
||||
let world = |t: &Session| -> Result<Mat> {
|
||||
if path.is_empty() {
|
||||
Ok(solidcraft_doc::IDENTITY)
|
||||
} else {
|
||||
t.occurrence_placement(&path).map(|o| o.world).map_err(|e| bad(cmd, e.to_string()))
|
||||
}
|
||||
};
|
||||
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") {
|
||||
Some(v) => v
|
||||
.as_f64()
|
||||
.filter(|n| n.is_finite() && *n >= 2.0 && *n <= MAX_TRACE 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")) {
|
||||
(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))?;
|
||||
(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)) => {
|
||||
let id = joint_id(s, Some(j), cmd)?;
|
||||
let kind = s.doc.assembly.joints.iter().find(|j| j.id == id).map(|j| j.kind).ok_or_else(|| bad(cmd, "joint"))?;
|
||||
let dofs = kind.dofs().len();
|
||||
if dofs == 0 {
|
||||
return Err(bad(cmd, "a rigid joint does not move"));
|
||||
}
|
||||
let index = match p.get("index") {
|
||||
None => 0,
|
||||
Some(v) => v
|
||||
.as_u64()
|
||||
.and_then(|i| usize::try_from(i).ok())
|
||||
.filter(|i| *i < dofs)
|
||||
.ok_or_else(|| bad(cmd, format!("`index` must be below {dofs}, the joint's value count")))?,
|
||||
};
|
||||
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}`)"))),
|
||||
};
|
||||
let (from, to) = (end("from")?, end("to")?);
|
||||
let plan: Plan = Box::new(move |t, k, n| {
|
||||
let mut q = t.doc.assembly.joints.iter().find(|j| j.id == id).map(|j| j.values.clone()).unwrap_or_default();
|
||||
q.resize(q.len().max(dofs), 0.0);
|
||||
if let Some(x) = q.get_mut(index) {
|
||||
*x = from + (to - from) * k as f64 / (n - 1) as f64;
|
||||
}
|
||||
vec![(id, q)]
|
||||
});
|
||||
(samples(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> {
|
||||
let (points, mut out) = sample_trace(s, p, "motion.trace")?;
|
||||
out["points"] = points.iter().map(|v| json!([v.x, v.y, v.z])).collect();
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// The trace as a 3D sketch spline, in one undo step: a new sketch of the root component (the
|
||||
/// world, which no joint moves) with 3D Sketch on and the spline through the trace's points.
|
||||
fn keep_trace(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "motion.trace.keep";
|
||||
let (mut points, mut out) = sample_trace(s, p, cmd)?;
|
||||
// Where the motion stood still (at a limit or a stop), one fit point.
|
||||
points.dedup_by(|a, b| a.dist(*b) < 1e-6);
|
||||
// A full cycle comes back to where it started (to the loop solver's tolerance): the spline
|
||||
// closes there, smoothly, through the start itself (and a sample that would double it goes).
|
||||
if points.len() > 3
|
||||
&& let (Some(&first), Some(&last)) = (points.first(), points.last())
|
||||
&& first.dist(last) < 1e-4
|
||||
{
|
||||
points.pop();
|
||||
if points.last().is_some_and(|p| p.dist(first) < 1e-6) {
|
||||
points.pop();
|
||||
}
|
||||
points.push(first);
|
||||
}
|
||||
if points.len() < 2 {
|
||||
return Err(bad(cmd, "the point does not move: there is no trace to keep"));
|
||||
}
|
||||
let name = match str_(p, "name").map(str::trim).filter(|n| !n.is_empty()) {
|
||||
Some(n) => n.to_string(),
|
||||
None => (1..=s.doc.features.len() + 1).map(|k| format!("Motion Trace{k}")).find(|n| s.doc.find_feature(n).is_none()).unwrap_or_default(),
|
||||
};
|
||||
let pts: Vec<Value> = points.iter().map(|v| json!([v.x, v.y, v.z])).collect();
|
||||
// Made in the root, whatever is being edited, which is then as it was (also on an error).
|
||||
let before = (s.active_sketch, s.active_component, s.active_occurrence_path.take());
|
||||
s.active_component = 0;
|
||||
let made = (|| {
|
||||
let run = |s: &mut Session, id: &str, q: Value| super::find_command(id).ok_or_else(|| bad(cmd, id.to_string()))?.run_nested(s, &q);
|
||||
let made = run(s, "sketch.create", json!({"plane": "XY", "project_edges": false, "name": name}))?;
|
||||
let sketch = made.get("sketch").cloned().unwrap_or(Value::Null);
|
||||
run(s, "sketch.options", json!({"sketch": sketch, "sketch_3d": true}))?;
|
||||
let spline = run(s, "sketch.spline3d", json!({"sketch": sketch, "points": pts}))?;
|
||||
Ok::<_, EngineError>((made, spline))
|
||||
})();
|
||||
(s.active_sketch, s.active_component, s.active_occurrence_path) = before;
|
||||
s.revision += 1;
|
||||
let (made, spline) = made?;
|
||||
let sketch = made.get("sketch").cloned().unwrap_or(Value::Null);
|
||||
out["sketch"] = sketch;
|
||||
out["name"] = made.get("name").cloned().unwrap_or(Value::Null);
|
||||
out["curves"] = spline.get("curves").cloned().unwrap_or(Value::Null);
|
||||
out["points"] = json!(points.len());
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn list_studies(s: &mut Session, _p: &Value) -> Result<Value> {
|
||||
Ok(json!({"studies": s.doc.assembly.studies}))
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user