Degrees of freedom count closed loops (#1206)
Degrees of freedom count closed loops.
This commit is contained in:
1 parent
a629236650
commit
099d7e298a
3 files changed
+326
-79
No files matched your search
+248
-74
@@ -544,35 +544,7 @@ fn close_loops(tree: &Tree, closing: &[u64], asm: &Assembly, held: &Held, vals:
|
||||
return (Values::new(), stuck);
|
||||
}
|
||||
let linked: BTreeSet<(u64, usize)> = asm.links.iter().map(|l| (l.b, l.ib)).collect();
|
||||
// Each loop: its closing joint and the joints on the tree paths between its sides; loops that
|
||||
// share a joint (or a value feeding them through links) are solved together.
|
||||
let mut groups: Vec<(Vec<u64>, BTreeSet<u64>, BTreeSet<(u64, usize)>)> = Vec::new();
|
||||
for id in closing {
|
||||
let Some(j) = tree.joints.get(id) else { continue };
|
||||
// A joint between an occurrence and itself (only a hand-edited file has one) is only checked.
|
||||
if j.a.occurrence == j.b.occurrence {
|
||||
continue;
|
||||
}
|
||||
let side = |occ: u64| -> BTreeSet<u64> { tree.path(occ).1.iter().map(|(k, _)| *k).collect() };
|
||||
let mut around: BTreeSet<u64> = side(j.a.occurrence).symmetric_difference(&side(j.b.occurrence)).copied().collect();
|
||||
around.insert(*id);
|
||||
// The values the loop depends on: its joints' and those its links read. Loops tie together
|
||||
// through a shared value only (a rigid joint on both paths has none; two values of one
|
||||
// joint can feed two independent loops).
|
||||
let mut touches = link_chains(asm, tree, &around, &linked).0;
|
||||
touches.extend(around.iter().filter_map(|id| tree.joints.get(id)).flat_map(|j| (0..j.kind.dofs().len()).map(|i| (j.id, i))));
|
||||
let mut members = vec![*id];
|
||||
groups.retain(|(c, a, t)| {
|
||||
if t.is_disjoint(&touches) {
|
||||
return true;
|
||||
}
|
||||
members.extend(c.iter().copied());
|
||||
around.extend(a.iter().copied());
|
||||
touches.extend(t.iter().copied());
|
||||
false
|
||||
});
|
||||
groups.push((members, around, touches));
|
||||
}
|
||||
let groups = loop_groups(tree, closing, asm, &linked);
|
||||
let spent = std::cell::Cell::new(0usize);
|
||||
for (group, around, touches) in &groups {
|
||||
let moves = held_moves(asm, held, touches, vals);
|
||||
@@ -599,6 +571,225 @@ fn close_loops(tree: &Tree, closing: &[u64], asm: &Assembly, held: &Held, vals:
|
||||
(moved, stuck)
|
||||
}
|
||||
|
||||
/// A group of loops solved together: its closing joints, the joints around them, and the values
|
||||
/// it touches.
|
||||
type Group = (Vec<u64>, BTreeSet<u64>, BTreeSet<(u64, usize)>);
|
||||
|
||||
/// The loops' groups. Each loop: its closing joint and the joints on the tree paths between its
|
||||
/// sides; loops that share a value (of a joint, or one feeding them through links) are solved
|
||||
/// together.
|
||||
fn loop_groups(tree: &Tree, closing: &[u64], asm: &Assembly, linked: &BTreeSet<(u64, usize)>) -> Vec<Group> {
|
||||
let mut groups: Vec<Group> = Vec::new();
|
||||
for id in closing {
|
||||
let Some(j) = tree.joints.get(id) else { continue };
|
||||
// A joint between an occurrence and itself (only a hand-edited file has one) is only checked.
|
||||
if j.a.occurrence == j.b.occurrence {
|
||||
continue;
|
||||
}
|
||||
let side = |occ: u64| -> BTreeSet<u64> { tree.path(occ).1.iter().map(|(k, _)| *k).collect() };
|
||||
let mut around: BTreeSet<u64> = side(j.a.occurrence).symmetric_difference(&side(j.b.occurrence)).copied().collect();
|
||||
around.insert(*id);
|
||||
// The values the loop depends on: its joints' and those its links read. Loops tie together
|
||||
// through a shared value only (a rigid joint on both paths has none; two values of one
|
||||
// joint can feed two independent loops).
|
||||
let mut touches = link_chains(asm, tree, &around, linked).0;
|
||||
touches.extend(around.iter().filter_map(|id| tree.joints.get(id)).flat_map(|j| (0..j.kind.dofs().len()).map(|i| (j.id, i))));
|
||||
let mut members = vec![*id];
|
||||
groups.retain(|(c, a, t)| {
|
||||
if t.is_disjoint(&touches) {
|
||||
return true;
|
||||
}
|
||||
members.extend(c.iter().copied());
|
||||
around.extend(a.iter().copied());
|
||||
touches.extend(t.iter().copied());
|
||||
false
|
||||
});
|
||||
groups.push((members, around, touches));
|
||||
}
|
||||
groups
|
||||
}
|
||||
|
||||
/// The values a solve of the loops `around` moves: every value of their joints but the `held`
|
||||
/// ones and motion-link targets, and the values the targets' link chains start from.
|
||||
fn unknowns(tree: &Tree, around: &BTreeSet<u64>, asm: &Assembly, linked: &BTreeSet<(u64, usize)>, held: &Held) -> Vec<(u64, usize)> {
|
||||
let mut vars: Vec<(u64, usize)> = Vec::new();
|
||||
for j in around.iter().filter_map(|id| tree.joints.get(id)) {
|
||||
vars.extend((0..j.kind.dofs().len()).map(|i| (j.id, i)).filter(|v| !linked.contains(v) && !held.contains_key(v)));
|
||||
}
|
||||
for v in link_chains(asm, tree, around, linked).1 {
|
||||
if !held.contains_key(&v) && !vars.contains(&v) {
|
||||
vars.push(v);
|
||||
}
|
||||
}
|
||||
vars
|
||||
}
|
||||
|
||||
/// The loops' size: the diagonal of the box around their joints' A frames, 1 mm to 1 km. Points
|
||||
/// that far apart weigh turns like moves.
|
||||
fn loop_scale(tree: &Tree, around: &BTreeSet<u64>, vals: &Values) -> f64 {
|
||||
let (mut lo, mut hi) = (Vec3::new(f64::MAX, f64::MAX, f64::MAX), Vec3::new(f64::MIN, f64::MIN, f64::MIN));
|
||||
for id in around {
|
||||
let (Some(j), Some((fa, _))) = (tree.joints.get(id), tree.frames.get(id)) else { continue };
|
||||
let Some(t) = tree.world(j.a.occurrence, vals) else { continue };
|
||||
let p = apply_point(&mat_mul(&t, fa), Vec3::ZERO);
|
||||
lo = Vec3::new(lo.x.min(p.x), lo.y.min(p.y), lo.z.min(p.z));
|
||||
hi = Vec3::new(hi.x.max(p.x), hi.y.max(p.y), hi.z.max(p.z));
|
||||
}
|
||||
if lo.x <= hi.x { (hi - lo).len().clamp(1.0, 1e6) } else { 1.0 }
|
||||
}
|
||||
|
||||
/// How far each closing joint is from meeting: three points of B's joint frame, `scale` apart,
|
||||
/// where the joint wants them against where the tree puts them (appended to `out`).
|
||||
fn closure_gaps(tree: &Tree, closing: &[u64], vals: &Values, scale: f64, out: &mut Vec<f64>) -> Option<()> {
|
||||
for id in closing {
|
||||
let (j, (_, fb)) = (tree.joints.get(id)?, tree.frames.get(id)?);
|
||||
let tb = tree.world(j.b.occurrence, vals)?;
|
||||
let want = tree.step(*id, true, &tree.world(j.a.occurrence, vals)?, vals)?;
|
||||
for p in [Vec3::ZERO, Vec3::X * scale, Vec3::Y * scale] {
|
||||
let at = apply_point(fb, p);
|
||||
let d = apply_point(&want, at) - apply_point(&tb, at);
|
||||
out.extend([d.x, d.y, d.z]);
|
||||
}
|
||||
}
|
||||
Some(())
|
||||
}
|
||||
|
||||
/// Occurrences whose degrees of freedom are worked out through their loops, at most; the rest
|
||||
/// keep the tree's count.
|
||||
const MAX_DOF_OCCURRENCES: usize = 256;
|
||||
|
||||
/// The degrees of freedom of the occurrences loops constrain: how many independent ways each can
|
||||
/// still move with every loop kept closed, the rank of its placement over the joint values once
|
||||
/// the loops' closure is taken out (rank [J_loops; J_placement] - rank J_loops). An occurrence's
|
||||
/// values are those on its tree path and the unknowns of the loops on that path. The tree's
|
||||
/// count stands where a loop group is over the solver's caps or the work runs out.
|
||||
fn loop_dof(tree: &Tree, closing: &[u64], asm: &Assembly, vals: &Values, dof: &mut BTreeMap<u64, usize>) {
|
||||
if closing.len() > MAX_LOOPS {
|
||||
return;
|
||||
}
|
||||
let linked: BTreeSet<(u64, usize)> = asm.links.iter().map(|l| (l.b, l.ib)).collect();
|
||||
let groups = loop_groups(tree, closing, asm, &linked);
|
||||
// The joints loops depend on: those around them, those whose values they read, and those a
|
||||
// motion link carries any of these values into.
|
||||
let mut relevant: BTreeSet<u64> = groups.iter().flat_map(|(_, a, t)| a.iter().copied().chain(t.iter().map(|(j, _)| *j))).collect();
|
||||
for _ in 0..MAX_LINK_PASSES {
|
||||
let more: Vec<u64> = asm.links.iter().filter(|l| relevant.contains(&l.a) && !relevant.contains(&l.b)).map(|l| l.b).collect();
|
||||
if more.is_empty() {
|
||||
break;
|
||||
}
|
||||
relevant.extend(more);
|
||||
}
|
||||
// The occurrences a loop may hold or move: placed by such a joint, or below one that is
|
||||
// (worked out down the tree, parents first). The others keep the tree's count.
|
||||
let mut candidates: BTreeSet<u64> = BTreeSet::new();
|
||||
for occ in &tree.order {
|
||||
if let Some(&(from, joint, _)) = tree.parent.get(occ)
|
||||
&& (relevant.contains(&joint) || candidates.contains(&from))
|
||||
{
|
||||
candidates.insert(*occ);
|
||||
}
|
||||
}
|
||||
let (mut spent, mut counted) = (0usize, 0usize);
|
||||
for occ in tree.order.iter().filter(|o| candidates.contains(o)) {
|
||||
let path: Vec<u64> = tree.path(*occ).1.iter().map(|(k, _)| *k).collect();
|
||||
let (mut values, mut have): (Vec<(u64, usize)>, BTreeSet<(u64, usize)>) = (Vec::new(), BTreeSet::new());
|
||||
let mut add = |values: &mut Vec<(u64, usize)>, v: (u64, usize)| {
|
||||
if have.insert(v) {
|
||||
values.push(v);
|
||||
}
|
||||
};
|
||||
for j in path.iter().filter_map(|k| tree.joints.get(k)) {
|
||||
for v in (0..j.kind.dofs().len()).map(|i| (j.id, i)).filter(|v| !linked.contains(v)) {
|
||||
add(&mut values, v);
|
||||
}
|
||||
if values.len() > MAX_LOOP_VALUES {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if values.len() > MAX_LOOP_VALUES {
|
||||
continue;
|
||||
}
|
||||
// A value on the path a motion link sets moves with the values its link chain starts from.
|
||||
let on_path: BTreeSet<u64> = path.iter().copied().collect();
|
||||
for v in link_chains(asm, tree, &on_path, &linked).1 {
|
||||
add(&mut values, v);
|
||||
}
|
||||
// The loops on the path, and every loop whose values those reach (a linked source may be
|
||||
// held by a loop of its own), each once.
|
||||
let (mut closers, mut around) = (Vec::new(), BTreeSet::new());
|
||||
let mut taken: BTreeSet<usize> = BTreeSet::new();
|
||||
loop {
|
||||
let more: Vec<usize> = groups
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(g, _)| !taken.contains(g))
|
||||
.filter(|(_, (_, a, t))| path.iter().any(|k| a.contains(k)) || t.iter().any(|v| values.contains(v)))
|
||||
.map(|(g, _)| g)
|
||||
.collect();
|
||||
if more.is_empty() {
|
||||
break;
|
||||
}
|
||||
for (c, a, _) in more.iter().filter_map(|g| groups.get(*g)) {
|
||||
closers.extend(c.iter().copied());
|
||||
around.extend(a.iter().copied());
|
||||
for v in unknowns(tree, a, asm, &linked, &Held::new()) {
|
||||
add(&mut values, v);
|
||||
}
|
||||
}
|
||||
taken.extend(more);
|
||||
}
|
||||
// No loop reaches this occurrence's values (not even through a motion link it drives):
|
||||
// the tree's count stands.
|
||||
if taken.is_empty() {
|
||||
continue;
|
||||
}
|
||||
counted += 1;
|
||||
if counted > MAX_DOF_OCCURRENCES || spent > MAX_LOOP_WORK {
|
||||
return;
|
||||
}
|
||||
if values.len() > MAX_LOOP_VALUES {
|
||||
continue;
|
||||
}
|
||||
let scale = loop_scale(tree, &around, vals);
|
||||
let mut work = vals.clone();
|
||||
// The loops' gaps, then three points of the occurrence `scale` apart.
|
||||
let mut f = |x: &[f64]| -> Option<Vec<f64>> {
|
||||
spent += 1;
|
||||
for ((id, i), v) in values.iter().zip(x) {
|
||||
if let Some(slot) = work.get_mut(id).and_then(|q| q.get_mut(*i)) {
|
||||
*slot = *v;
|
||||
}
|
||||
}
|
||||
apply_links(asm, &mut work);
|
||||
let mut out = Vec::with_capacity(closers.len() * 9 + 9);
|
||||
closure_gaps(tree, &closers, &work, scale, &mut out)?;
|
||||
let t = tree.world(*occ, &work)?;
|
||||
for p in [Vec3::ZERO, Vec3::X * scale, Vec3::Y * scale] {
|
||||
let q = apply_point(&t, p);
|
||||
out.extend([q.x, q.y, q.z]);
|
||||
}
|
||||
out.iter().all(|v| v.is_finite()).then_some(out)
|
||||
};
|
||||
let x0: Vec<f64> = values.iter().map(|(id, i)| vals.get(id).and_then(|q| q.get(*i)).copied().unwrap_or(0.0)).collect();
|
||||
let (n, gaps) = (values.len(), closers.len() * 9);
|
||||
let Some(jac) = jacobian(&mut f, &x0, gaps + 9) else { continue };
|
||||
let rank = |rows: usize| {
|
||||
let mut a = linalg::Mat::zeros(rows, n);
|
||||
let mut big = 0.0f64;
|
||||
for r in 0..rows {
|
||||
for c in 0..n {
|
||||
let v = jac.get(r * n + c).copied().unwrap_or(0.0);
|
||||
big = big.max(v.abs());
|
||||
a.set(r, c, v);
|
||||
}
|
||||
}
|
||||
linalg::rref(&mut a, 1e-6 * big.max(1.0)).len()
|
||||
};
|
||||
let free = rank(gaps + 9).saturating_sub(rank(gaps));
|
||||
dof.insert(*occ, free.min(6));
|
||||
}
|
||||
}
|
||||
|
||||
/// Close one group of loops: move the values of the joints `around` them (all but the `held`
|
||||
/// values, motion-link targets and rigid joints, within their limits; a linked value moves
|
||||
/// through the start of its link chain) until every loop-closing joint meets. A loop open at the
|
||||
@@ -619,16 +810,7 @@ fn close_group(
|
||||
spent: &std::cell::Cell<usize>,
|
||||
vals: &mut Values,
|
||||
) -> Option<BTreeMap<(u64, usize), f64>> {
|
||||
let starts = link_chains(asm, tree, around, linked).1;
|
||||
let mut vars: Vec<(u64, usize)> = Vec::new();
|
||||
for j in around.iter().filter_map(|id| tree.joints.get(id)) {
|
||||
vars.extend((0..j.kind.dofs().len()).map(|i| (j.id, i)).filter(|v| !linked.contains(v) && !held.contains_key(v)));
|
||||
}
|
||||
for v in starts {
|
||||
if !held.contains_key(&v) && !vars.contains(&v) {
|
||||
vars.push(v);
|
||||
}
|
||||
}
|
||||
let vars = unknowns(tree, around, asm, linked, held);
|
||||
if vars.len() > MAX_LOOP_VALUES {
|
||||
return Some(BTreeMap::new());
|
||||
}
|
||||
@@ -639,16 +821,7 @@ fn close_group(
|
||||
None => (f64::NEG_INFINITY, f64::INFINITY),
|
||||
})
|
||||
.collect();
|
||||
// Turns weigh like moves through points a loop's size apart.
|
||||
let (mut lo, mut hi) = (Vec3::new(f64::MAX, f64::MAX, f64::MAX), Vec3::new(f64::MIN, f64::MIN, f64::MIN));
|
||||
for id in around {
|
||||
let (Some(j), Some((fa, _))) = (tree.joints.get(id), tree.frames.get(id)) else { continue };
|
||||
let Some(t) = tree.world(j.a.occurrence, vals) else { continue };
|
||||
let p = apply_point(&mat_mul(&t, fa), Vec3::ZERO);
|
||||
lo = Vec3::new(lo.x.min(p.x), lo.y.min(p.y), lo.z.min(p.z));
|
||||
hi = Vec3::new(hi.x.max(p.x), hi.y.max(p.y), hi.z.max(p.z));
|
||||
}
|
||||
let scale = if lo.x <= hi.x { (hi - lo).len().clamp(1.0, 1e6) } else { 1.0 };
|
||||
let scale = loop_scale(tree, around, vals);
|
||||
// Closer than the conflict check needs: 1e-7 mm, and turns of 1e-10 rad across the loop.
|
||||
let tol = (1e-10 * scale).min(1e-7);
|
||||
// The values with the held ones `t` of the way along (and the unknowns at `x`).
|
||||
@@ -699,16 +872,7 @@ fn close_group(
|
||||
}
|
||||
place(&mut work, x, t);
|
||||
let mut out = Vec::with_capacity(closing.len() * 9);
|
||||
for id in closing {
|
||||
let (j, (_, fb)) = (tree.joints.get(id)?, tree.frames.get(id)?);
|
||||
let tb = tree.world(j.b.occurrence, &work)?;
|
||||
let want = tree.step(*id, true, &tree.world(j.a.occurrence, &work)?, &work)?;
|
||||
for p in [Vec3::ZERO, Vec3::X * scale, Vec3::Y * scale] {
|
||||
let at = apply_point(fb, p);
|
||||
let d = apply_point(&want, at) - apply_point(&tb, at);
|
||||
out.extend([d.x, d.y, d.z]);
|
||||
}
|
||||
}
|
||||
closure_gaps(tree, closing, &work, scale, &mut out)?;
|
||||
out.iter().all(|v| v.is_finite()).then_some(out)
|
||||
};
|
||||
// Already closed, with no held value moving. (A moving one is followed all the way: a
|
||||
@@ -764,8 +928,28 @@ fn largest(r: &[f64]) -> f64 {
|
||||
|
||||
/// Levenberg–Marquardt: move `x` (kept within `bounds`) until every value of `f` is within
|
||||
/// `tol` of zero. False when it can't get there.
|
||||
fn least_squares(f: &mut dyn FnMut(&[f64]) -> Option<Vec<f64>>, x: &mut Vec<f64>, bounds: &[(f64, f64)], tol: f64) -> bool {
|
||||
/// The Jacobian of `f` (`m` values) at `x` by central differences, row-major.
|
||||
fn jacobian(f: &mut dyn FnMut(&[f64]) -> Option<Vec<f64>>, x: &[f64], m: usize) -> Option<Vec<f64>> {
|
||||
const H: f64 = 1e-6;
|
||||
let n = x.len();
|
||||
let mut jac = vec![0.0; m * n];
|
||||
for k in 0..n {
|
||||
let (mut xp, mut xm) = (x.to_vec(), x.to_vec());
|
||||
if let (Some(p), Some(q)) = (xp.get_mut(k), xm.get_mut(k)) {
|
||||
*p += H;
|
||||
*q -= H;
|
||||
}
|
||||
let (rp, rm) = (f(&xp)?, f(&xm)?);
|
||||
for (i, (a, b)) in rp.iter().zip(&rm).enumerate().take(m) {
|
||||
if let Some(slot) = jac.get_mut(i * n + k) {
|
||||
*slot = (a - b) / (2.0 * H);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(jac)
|
||||
}
|
||||
|
||||
fn least_squares(f: &mut dyn FnMut(&[f64]) -> Option<Vec<f64>>, x: &mut Vec<f64>, bounds: &[(f64, f64)], tol: f64) -> bool {
|
||||
let n = x.len();
|
||||
let norm = |r: &[f64]| r.iter().map(|v| v * v).sum::<f64>();
|
||||
let Some(mut r) = f(x) else { return false };
|
||||
@@ -779,20 +963,7 @@ fn least_squares(f: &mut dyn FnMut(&[f64]) -> Option<Vec<f64>>, x: &mut Vec<f64>
|
||||
}
|
||||
// The Jacobian by central differences, then the normal equations JᵀJ δ = −Jᵀr.
|
||||
let m = r.len();
|
||||
let mut jac = vec![0.0; m * n];
|
||||
for k in 0..n {
|
||||
let (mut xp, mut xm) = (x.clone(), x.clone());
|
||||
if let (Some(p), Some(q)) = (xp.get_mut(k), xm.get_mut(k)) {
|
||||
*p += H;
|
||||
*q -= H;
|
||||
}
|
||||
let (Some(rp), Some(rm)) = (f(&xp), f(&xm)) else { return false };
|
||||
for (i, (a, b)) in rp.iter().zip(&rm).enumerate() {
|
||||
if let Some(slot) = jac.get_mut(i * n + k) {
|
||||
*slot = (a - b) / (2.0 * H);
|
||||
}
|
||||
}
|
||||
}
|
||||
let Some(jac) = jacobian(f, x, m) else { return false };
|
||||
let mut jtj = linalg::Mat::zeros(n, n);
|
||||
let mut g = vec![0.0; n];
|
||||
for (i, ri) in r.iter().enumerate() {
|
||||
@@ -940,6 +1111,9 @@ pub fn solve_holding(doc: &Document, st: &ModelState, held: &Held) -> Solution {
|
||||
sol.conflicts.push((j.id, j.name.clone(), "the loop can't stay closed on the way to these values".into()));
|
||||
}
|
||||
}
|
||||
if !closing.is_empty() {
|
||||
loop_dof(&tree, &closing, asm, &vals, &mut dof);
|
||||
}
|
||||
for o in &doc.occurrences {
|
||||
dof.entry(o.id).or_insert(6);
|
||||
}
|
||||
|
||||
@@ -467,10 +467,9 @@ fn values_left_out_of_a_drive_follow_the_loop() {
|
||||
assert!(near(place(&s, "Plate", Vec3::ZERO), Vec3::new(10.0, 10.0, 0.0)), "{:?}", place(&s, "Plate", Vec3::ZERO));
|
||||
}
|
||||
|
||||
/// A slider-crank: crank 15 about the origin, rod 40, a piston sliding along X. The piston
|
||||
/// sits at a·cos θ + √(b² − (a·sin θ)²).
|
||||
#[test]
|
||||
fn slider_crank_moves_the_piston() {
|
||||
/// A slider-crank: crank 15 about the origin, rod 40, a piston sliding along X. Returns the crank
|
||||
/// pin and the wrist pin.
|
||||
fn slider_crank() -> (Session, Vec3, Vec3) {
|
||||
let mut s = Session::default();
|
||||
let (o, p0) = (Vec3::ZERO, Vec3::new(0.0, 15.0, 0.0));
|
||||
let s0 = Vec3::new((40.0f64 * 40.0 - 15.0 * 15.0).sqrt(), 0.0, 0.0);
|
||||
@@ -483,6 +482,13 @@ fn slider_crank_moves_the_piston() {
|
||||
pin(&mut s, "revolute", "Crank pin", "Crank", "Rod", p0, Vec3::Z);
|
||||
pin(&mut s, "revolute", "Wrist pin", "Rod", "Piston", s0, Vec3::Z);
|
||||
pin(&mut s, "slider", "Bore", "Ground", "Piston", s0, Vec3::X);
|
||||
(s, p0, s0)
|
||||
}
|
||||
|
||||
/// The piston sits at a·cos θ + √(b² − (a·sin θ)²).
|
||||
#[test]
|
||||
fn slider_crank_moves_the_piston() {
|
||||
let (mut s, p0, s0) = slider_crank();
|
||||
for v in [40.0, 135.0, 270.0] {
|
||||
let r = run(&mut s, "joint.drive", json!({"joint": "Crank", "value": v}));
|
||||
assert_eq!(r["conflicts"].as_array().map(Vec::len), Some(0), "{v}°: {r}");
|
||||
@@ -721,6 +727,71 @@ fn a_clamped_link_is_followed_step_by_step() {
|
||||
assert_eq!(r["conflicts"].as_array().map(Vec::len), Some(0), "{r}");
|
||||
}
|
||||
|
||||
/// Each occurrence's degrees of freedom as `joint.list` reports them.
|
||||
fn dof_of(s: &mut Session, component: &str) -> u64 {
|
||||
let o = occ(s, component);
|
||||
let l = run(s, "joint.list", json!({}));
|
||||
l["dof"].as_array().into_iter().flatten().find(|d| d["occurrence"] == o).and_then(|d| d["dof"].as_u64()).unwrap_or(99)
|
||||
}
|
||||
|
||||
/// Loops take degrees of freedom away: a four-bar's moving parts have one between them (its
|
||||
/// mobility), a part hinged to the coupler one more, a slider-crank one, a parallelogram one, and a
|
||||
/// four-bar locked rigid none.
|
||||
#[test]
|
||||
fn loops_report_their_mobility() {
|
||||
let (mut s, p0, q0) = four_bar();
|
||||
for (part, want) in [("Ground", 0), ("Crank", 1), ("Coupler", 1), ("Rocker", 1)] {
|
||||
assert_eq!(dof_of(&mut s, part), want, "four-bar {part}");
|
||||
}
|
||||
// Still one after a drive moved it.
|
||||
run(&mut s, "joint.drive", json!({"joint": "Crank", "value": 70}));
|
||||
assert_eq!(dof_of(&mut s, "Coupler"), 1);
|
||||
// A flag hinged to the coupler half-way along: one more.
|
||||
let mid = (p0 + q0) * 0.5;
|
||||
part(&mut s, "Flag", [mid.x, mid.y, 5.0]);
|
||||
run(&mut s, "joint.as_built", json!({"type": "revolute", "a": "Coupler", "b": "Flag", "name": "Flag hinge"}));
|
||||
assert_eq!(dof_of(&mut s, "Flag"), 2);
|
||||
// Still two when a rotor elsewhere turns the flag's hinge through a motion link.
|
||||
part(&mut s, "Rotor", [100.0, 0.0, -5.0]);
|
||||
pin(&mut s, "revolute", "Rotor", "Ground", "Rotor", Vec3::new(100.0, 0.0, 0.0), Vec3::Z);
|
||||
run(&mut s, "joint.motion_link", json!({"a": "Rotor", "b": "Flag hinge", "ratio": 1}));
|
||||
assert_eq!(dof_of(&mut s, "Flag"), 2);
|
||||
// A rotor locked by a loop of its own leaves the flag only the mechanism's one.
|
||||
run(&mut s, "joint.as_built", json!({"type": "rigid", "a": "Ground", "b": "Rotor", "name": "Rotor lock"}));
|
||||
assert_eq!(dof_of(&mut s, "Flag"), 1);
|
||||
// Locked: nothing moves.
|
||||
let (mut s, _, _) = four_bar();
|
||||
run(&mut s, "joint.as_built", json!({"type": "rigid", "a": "Ground", "b": "Rocker", "name": "Lock"}));
|
||||
for part in ["Crank", "Coupler", "Rocker"] {
|
||||
assert_eq!(dof_of(&mut s, part), 0, "locked {part}");
|
||||
}
|
||||
// A rotor whose turn a motion link carries into the locked crank can't turn either.
|
||||
part(&mut s, "Rotor", [100.0, 0.0, -5.0]);
|
||||
pin(&mut s, "revolute", "Rotor", "Ground", "Rotor", Vec3::new(100.0, 0.0, 0.0), Vec3::Z);
|
||||
run(&mut s, "joint.motion_link", json!({"a": "Rotor", "b": "Crank", "ratio": 1}));
|
||||
assert_eq!(dof_of(&mut s, "Rotor"), 0);
|
||||
let (mut s, _, _) = slider_crank();
|
||||
for part in ["Crank", "Rod", "Piston"] {
|
||||
assert_eq!(dof_of(&mut s, part), 1, "slider-crank {part}");
|
||||
}
|
||||
// A parallelogram (crank and rocker 20, ground and coupler 45), away from its folded poses.
|
||||
let mut s = Session::default();
|
||||
let (o2, o4) = (Vec3::ZERO, Vec3::new(45.0, 0.0, 0.0));
|
||||
let (p, q) = (Vec3::new(12.0, 16.0, 0.0), Vec3::new(57.0, 16.0, 0.0));
|
||||
for (name, c) in [("Ground", o2), ("Crank", p), ("Coupler", (p + q) * 0.5), ("Rocker", q)] {
|
||||
part(&mut s, name, [c.x, c.y, -5.0]);
|
||||
}
|
||||
let g = occ(&s, "Ground");
|
||||
run(&mut s, "occurrence.ground", json!({"occurrence": g, "grounded": true}));
|
||||
pin(&mut s, "revolute", "Crank", "Ground", "Crank", o2, Vec3::Z);
|
||||
pin(&mut s, "revolute", "Crank pin", "Crank", "Coupler", p, Vec3::Z);
|
||||
pin(&mut s, "revolute", "Rocker pin", "Coupler", "Rocker", q, Vec3::Z);
|
||||
pin(&mut s, "revolute", "Rocker", "Ground", "Rocker", o4, Vec3::Z);
|
||||
for part in ["Crank", "Coupler", "Rocker"] {
|
||||
assert_eq!(dof_of(&mut s, part), 1, "parallelogram {part}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A design file path in a fresh temporary folder (removed by the caller).
|
||||
fn temp_design(tag: &str, ext: &str) -> (std::path::PathBuf, String) {
|
||||
let dir = std::env::temp_dir().join(format!("solidcraft-joint-visibility-{tag}-{}", std::process::id()));
|
||||
|
||||
@@ -34,7 +34,9 @@ mechanism stays in the assembly mode it started in (an open four-bar does not fl
|
||||
crossed one). If the motion would take a loop past the point where it can still close, for
|
||||
example driving a rocker beyond its swing, the motion stops at the last position where the loop
|
||||
closes, and the drive reports `stopped_by_loop`. A loop that cannot close at all is listed
|
||||
under `conflicts` and nothing in it moves. The Drive Joints dialog says when a drive stops short and
|
||||
under `conflicts` and nothing in it moves. `joint.list` reports how many degrees of freedom each
|
||||
part still has with its loops closed: the moving parts of a four-bar have one between them, and a
|
||||
loop locked by a rigid joint has none. The Drive Joints dialog says when a drive stops short and
|
||||
why. While it previews, a row under the values gives the value the joint reached and the reason
|
||||
(the loop that can't close further, or the two parts a contact set keeps apart), and after OK the
|
||||
status line repeats it. The value box keeps what you typed.
|
||||
|
||||
Reference in new issue
Block a user