mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-06 20:02:16 +00:00
Joints: a replaced motion link keeps the driven joint where it is; links count in dof (#2898)
Thanks @SS-EN.
This commit is contained in:
@@ -846,7 +846,10 @@ pub struct Solution {
|
||||
pub conflicts: Vec<(u64, String, String)>,
|
||||
/// Joints that could not be applied (bad origin…): (joint id, name, why).
|
||||
pub errors: Vec<(u64, String, String)>,
|
||||
/// Remaining degrees of freedom per occurrence (6 = free, 0 = fixed).
|
||||
/// Remaining degrees of freedom per occurrence (6 = free, 0 = fixed): how many independent
|
||||
/// values still move it, from the fixed occurrences, with loops kept closed and a value a
|
||||
/// motion link sets moving with the values its link chain starts from (a driven gear has its
|
||||
/// driving gear's one, a screw's travel its turn's).
|
||||
pub dof: BTreeMap<u64, usize>,
|
||||
/// Occurrences fixed in place (grounded, or the root's).
|
||||
pub fixed: Vec<u64>,
|
||||
@@ -1043,6 +1046,46 @@ fn link_chains(asm: &Assembly, tree: &Tree, around: &BTreeSet<u64>, linked: &BTr
|
||||
(feeds, starts)
|
||||
}
|
||||
|
||||
/// The values that move each value a motion link sets: the values its link chain starts from,
|
||||
/// which no link sets (none for a chain that comes back round on itself, which only a hand-edited
|
||||
/// design has). A driven gear's turn is its driving gear's: one degree of freedom, not two.
|
||||
fn link_sources(asm: &Assembly) -> BTreeMap<(u64, usize), BTreeSet<(u64, usize)>> {
|
||||
let mut into: BTreeMap<(u64, usize), Vec<(u64, usize)>> = BTreeMap::new();
|
||||
for l in &asm.links {
|
||||
into.entry((l.b, l.ib)).or_default().push((l.a, l.ia));
|
||||
}
|
||||
let mut sources = BTreeMap::new();
|
||||
if into.is_empty() {
|
||||
return sources;
|
||||
}
|
||||
// A link from a value its joint doesn't have (a hand-edited design) moves nothing.
|
||||
let real: BTreeSet<(u64, usize)> = asm.joints.iter().flat_map(|j| (0..j.kind.dofs().len()).map(|i| (j.id, i))).collect();
|
||||
for &v in into.keys() {
|
||||
// Back along the links, as far as a link chain carries a value (`MAX_LINK_PASSES`).
|
||||
let (mut from, mut seen) = (BTreeSet::new(), BTreeSet::from([v]));
|
||||
let mut frontier = vec![v];
|
||||
for _ in 0..MAX_LINK_PASSES {
|
||||
let mut next = Vec::new();
|
||||
for w in frontier.iter().filter_map(|w| into.get(w)).flatten() {
|
||||
if !seen.insert(*w) {
|
||||
continue;
|
||||
}
|
||||
if into.contains_key(w) {
|
||||
next.push(*w);
|
||||
} else if real.contains(w) {
|
||||
from.insert(*w);
|
||||
}
|
||||
}
|
||||
if next.is_empty() {
|
||||
break;
|
||||
}
|
||||
frontier = next;
|
||||
}
|
||||
sources.insert(v, from);
|
||||
}
|
||||
sources
|
||||
}
|
||||
|
||||
/// The held values among `touches` that move: (joint, index, from, to), `from` within the
|
||||
/// joint's limits. A suppressed joint still sets values through its motion links.
|
||||
fn held_moves(asm: &Assembly, held: &Held, touches: &BTreeSet<(u64, usize)>, vals: &Values) -> Vec<(u64, usize, f64, f64)> {
|
||||
@@ -1725,6 +1768,14 @@ fn build<'a>(doc: &'a Document, st: &ModelState, vals: &Values, sol: &mut Soluti
|
||||
Tree { joints: asm.joints.iter().map(|j| (j.id, j)).collect(), frames, fixed: placed.clone(), parent: BTreeMap::new(), order: Vec::new() };
|
||||
let mut used: BTreeSet<u64> = BTreeSet::new();
|
||||
let mut queue: VecDeque<u64> = placed.keys().copied().collect();
|
||||
// An occurrence's degrees of freedom are the distinct values that move it: those of the
|
||||
// joints on its path, a value a motion link sets standing for the values its link chain
|
||||
// starts from (a driven gear turns with its driving gear's one value). Kept per occurrence as
|
||||
// a count of the values no link reads or sets, which no other path value can repeat, and the
|
||||
// set of the ones that may be repeated (links' sources).
|
||||
let sources = link_sources(asm);
|
||||
let shared: BTreeSet<(u64, usize)> = sources.values().flatten().copied().collect();
|
||||
let mut moved_by: BTreeMap<u64, (usize, BTreeSet<(u64, usize)>)> = BTreeMap::new();
|
||||
// The joints at each occurrence, in the joints' order (a large assembly is not walked joint by
|
||||
// joint for every occurrence).
|
||||
let mut at: BTreeMap<u64, Vec<&Joint>> = BTreeMap::new();
|
||||
@@ -1755,7 +1806,18 @@ fn build<'a>(doc: &'a Document, st: &ModelState, vals: &Values, sol: &mut Soluti
|
||||
placed.insert(other, m);
|
||||
tree.parent.insert(other, (occ, j.id, from_a));
|
||||
tree.order.push(other);
|
||||
dof.insert(other, dof.get(&occ).copied().unwrap_or(0) + j.kind.dofs().len());
|
||||
let (mut own, mut common) = moved_by.get(&occ).cloned().unwrap_or_default();
|
||||
for v in (0..j.kind.dofs().len()).map(|i| (j.id, i)) {
|
||||
match sources.get(&v) {
|
||||
Some(from) => common.extend(from.iter().copied()),
|
||||
None if shared.contains(&v) => {
|
||||
common.insert(v);
|
||||
}
|
||||
None => own += 1,
|
||||
}
|
||||
}
|
||||
dof.insert(other, own + common.len());
|
||||
moved_by.insert(other, (own, common));
|
||||
queue.push_back(other);
|
||||
}
|
||||
}
|
||||
@@ -1892,6 +1954,26 @@ mod tests {
|
||||
Assembly { joints, links, ..Assembly::default() }
|
||||
}
|
||||
|
||||
/// A linked value moves with the free value its chain starts from, however the chain is
|
||||
/// stored; a chain that comes back round on itself, or a link from a value its joint lacks
|
||||
/// (hand-edited designs), moves it with nothing.
|
||||
#[test]
|
||||
fn linked_values_move_with_their_chains_start() {
|
||||
let mut asm = chain(5, 0.0);
|
||||
let sources = link_sources(&asm);
|
||||
for b in 2..=5 {
|
||||
assert_eq!(sources.get(&(b, 0)), Some(&BTreeSet::from([(1, 0)])), "J{b}: {sources:?}");
|
||||
}
|
||||
assert!(!sources.contains_key(&(1, 0)));
|
||||
let link = |a: u64, ia: usize, b: u64| MotionLink { a, ia, b, ib: 0, ratio: 1.0, offset: 0.0, coupling: None, lift: Vec::new() };
|
||||
asm.links.push(link(5, 0, 1));
|
||||
assert!(link_sources(&asm).values().all(BTreeSet::is_empty), "{:?}", link_sources(&asm));
|
||||
let mut asm = chain(2, 0.0);
|
||||
asm.links = vec![link(1, 7, 2)];
|
||||
assert_eq!(link_sources(&asm).get(&(2, 0)), Some(&BTreeSet::new()));
|
||||
assert!(link_sources(&Assembly::default()).is_empty());
|
||||
}
|
||||
|
||||
/// Links carry a value down a chain stored from its end, each linked value within its limits
|
||||
/// as it is set, and applying them again changes nothing.
|
||||
#[test]
|
||||
|
||||
@@ -53,21 +53,23 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
"a, b: joints that turn (revolute), b driven by a; ra, rb: the gears' pitch radii (or tooth counts): mm, an expression, or \
|
||||
{circle: [x,y,z], occurrence?}: the round face there, its radius (read again whenever the design changes, so the ratio follows \
|
||||
edits); internal?: bool (one is a ring gear: both turn the same way; by default b turns the other way); reverse?: bool; \
|
||||
link?: index (replace that motion link) → link, ratio (b's degrees per a's); b keeps where it is",
|
||||
link?: index (replace that motion link) → link, ratio (b's degrees per a's); b keeps where it is, also when the link replaces \
|
||||
another (at the current pose)",
|
||||
),
|
||||
CommandSpec::new("joint.rack_pinion", "Rack and Pinion", rack_pinion).at("SOLID", "ASSEMBLE").icon("rack_pinion").params(
|
||||
"a, b: a joint that turns (revolute: the pinion) and one that slides (slider: the rack), in either order, b driven by a; \
|
||||
radius: the pinion's pitch radius (mm, an expression, or {circle: [x,y,z], occurrence?}): the rack travels radius × the turn in \
|
||||
radians; reverse?: bool; link?: index (replace that motion link) → link, ratio (mm per degree, or degrees per mm); b keeps \
|
||||
where it is",
|
||||
where it is, also when the link replaces another (at the current pose)",
|
||||
),
|
||||
CommandSpec::new("joint.screw", "Screw", screw).at("SOLID", "ASSEMBLE").icon("screw").params(
|
||||
"joint: a cylindrical joint (its turn drives its travel) | a, b: a joint that turns and one that slides, b driven by a; pitch: \
|
||||
travel per turn (mm or an expression); reverse?: bool (a left-hand thread); link?: index (replace that motion link) → link, \
|
||||
ratio (mm per degree, or degrees per mm); b keeps where it is",
|
||||
ratio (mm per degree, or degrees per mm); b keeps where it is, also when the link replaces another (at the current \
|
||||
pose)",
|
||||
),
|
||||
CommandSpec::new("joint.cam", "Cam", cam_cmd).at("SOLID", "ASSEMBLE").icon("cam").params(
|
||||
"a: the cam's joint (revolute); b: the follower's joint, a slider (a translating follower) or a revolute (an oscillating one), b driven by a; profile: {sketch: id|name, index?: the profile (default: the largest)} | {edges: [[x,y,z]…] (points on edges of the cam's bodies forming one closed loop, world), occurrence?}: the cam's outline, in a plane square to its axis, read again whenever the design changes; follower?: knife|roller|flat (default knife; flat: a face square to a slider's line); radius?: the roller's radius (mm, an expression or {circle: [x,y,z], occurrence?}: the roller's round face, which is its tip too); tip?: a joint origin where the follower touches: its knife's point or its roller's centre, such as {circle: [x,y,z]} on the roller (needed for an oscillating follower; a translating one's line runs through it, by default along its slider's axis); flip?: bool (ride the cam from the other side than the one the follower is on now); link?: index (replace that motion link) → link, lift: {min, max, rise, unit} (b's travel over a turn of the cam), reverse (the side it rides from), follower_offset (mm: how far a translating follower's line passes from the cam's axis); b keeps where it is",
|
||||
"a: the cam's joint (revolute); b: the follower's joint, a slider (a translating follower) or a revolute (an oscillating one), b driven by a; profile: {sketch: id|name, index?: the profile (default: the largest)} | {edges: [[x,y,z]…] (points on edges of the cam's bodies forming one closed loop, world), occurrence?}: the cam's outline, in a plane square to its axis, read again whenever the design changes; follower?: knife|roller|flat (default knife; flat: a face square to a slider's line); radius?: the roller's radius (mm, an expression or {circle: [x,y,z], occurrence?}: the roller's round face, which is its tip too); tip?: a joint origin where the follower touches: its knife's point or its roller's centre, such as {circle: [x,y,z]} on the roller (needed for an oscillating follower; a translating one's line runs through it, by default along its slider's axis); flip?: bool (ride the cam from the other side than the one the follower is on now); link?: index (replace that motion link) → link, lift: {min, max, rise, unit} (b's travel over a turn of the cam), reverse (the side it rides from), follower_offset (mm: how far a translating follower's line passes from the cam's axis); b keeps where it is, also when the link replaces another (at the current pose)",
|
||||
),
|
||||
CommandSpec::new("joint.limits", "Joint Limits", limits).icon("joint").params("joint; index?: value index (default 0); min?, max? (omit both to clear)"),
|
||||
CommandSpec::new("joint.edit", "Edit Joint", edit)
|
||||
@@ -1092,7 +1094,8 @@ fn cam_profile(s: &Session, p: &Value, cmd: &str) -> Result<CamProfile> {
|
||||
}
|
||||
|
||||
/// Add a coupling's motion link, or put it in place of link `link`: its ratio worked out from its
|
||||
/// sizes, a new link's offset leaving b where it is (the teeth mesh as they are modelled).
|
||||
/// sizes, its offset leaving b where it is (the teeth mesh as they are now), for a new link and
|
||||
/// one put in place of another alike.
|
||||
fn couple(s: &mut Session, p: &Value, cmd: &str, mut l: MotionLink) -> Result<Value> {
|
||||
let links = &s.doc.assembly.links;
|
||||
let replace = match p.get("link") {
|
||||
@@ -1124,11 +1127,10 @@ fn couple(s: &mut Session, p: &Value, cmd: &str, mut l: MotionLink) -> Result<Va
|
||||
joints::coupling_values(&s.doc, &s.model.state(), &l).unwrap_or(Ok((l.ratio, Vec::new()))).map_err(|e| bad(cmd, e.to_string()))?;
|
||||
let vals = joints::joint_values(&s.doc.assembly);
|
||||
let value = |id: u64, i: usize| vals.get(&id).and_then(|q| q.get(i)).copied().unwrap_or(0.0);
|
||||
// A link put in place of another keeps its offset, as a radius edited in the design does.
|
||||
l.offset = match replace.and_then(|i| links.get(i)) {
|
||||
Some(old) => old.offset,
|
||||
None => value(l.b, l.ib) - MotionLink { offset: 0.0, ..l.clone() }.follow(value(l.a, l.ia)),
|
||||
};
|
||||
// New or put in place of another (turned the other way, a ring gear now), b keeps where it
|
||||
// is: the gears stay in mesh at the pose they are in. (A radius edited in the design keeps
|
||||
// the offset instead, so the gears turn from where they meshed when they were coupled.)
|
||||
l.offset = value(l.b, l.ib) - MotionLink { offset: 0.0, ..l.clone() }.follow(value(l.a, l.ia));
|
||||
if !l.offset.is_finite() {
|
||||
return Err(bad(cmd, "the coupling gives the driven joint no value where it is"));
|
||||
}
|
||||
|
||||
@@ -19,10 +19,15 @@ fn close_to(got: f64, want: f64) -> bool {
|
||||
/// A round disc (a gear's pitch circle) of radius `r` about a vertical axis through (x, 0), 5 mm
|
||||
/// thick, in a component of its own, on a revolute joint to the grounded base.
|
||||
fn disc(s: &mut Session, name: &str, x: f64, r: &str) {
|
||||
loose_disc(s, name, x, r);
|
||||
pin(s, "revolute", name, "Base", name, Vec3::new(x, 0.0, 0.0), Vec3::Z);
|
||||
}
|
||||
|
||||
/// The disc alone, on no joint.
|
||||
fn loose_disc(s: &mut Session, name: &str, x: f64, r: &str) {
|
||||
run(s, "component.activate", json!({"component": "root"}));
|
||||
run(s, "component.create", json!({"name": name}));
|
||||
run(s, "solid.cylinder", json!({"base": [x, 0, 0], "axis": [0, 0, 1], "radius": r, "height": 5}));
|
||||
pin(s, "revolute", name, "Base", name, Vec3::new(x, 0.0, 0.0), Vec3::Z);
|
||||
}
|
||||
|
||||
fn base(s: &mut Session) {
|
||||
@@ -80,11 +85,13 @@ fn a_gear_train_of_three_turns_by_its_radii() {
|
||||
run(&mut s, "motion.study", json!({"name": "Spin", "steps": 10, "keys": [{"joint": "G1", "points": [[0, 0], [10, 30]]}]}));
|
||||
run(&mut s, "motion.play", json!({"study": "Spin", "step": 10}));
|
||||
assert!(close_to(shown(&mut s, "G3"), 24.0), "{}", shown(&mut s, "G3"));
|
||||
// An internal (ring) gear turns its mate the same way; redefining a link keeps its offset.
|
||||
// An internal (ring) gear turns its mate the same way; the redefined link leaves G3 where it
|
||||
// is (G2 at -18°), and G1 turning on 30° more turns G2 on -18° and G3 on -24°.
|
||||
let r = run(&mut s, "joint.gear", json!({"a": "G2", "b": "G3", "ra": 20, "rb": 15, "internal": true, "link": 1}));
|
||||
assert_eq!(r["link"], 1, "{r}");
|
||||
assert!(close_to(shown(&mut s, "G3"), 24.0), "{}", run(&mut s, "joint.list", json!({})));
|
||||
run(&mut s, "joint.drive", json!({"joint": "G1", "value": 60}));
|
||||
assert!(close_to(shown(&mut s, "G2"), -36.0) && close_to(shown(&mut s, "G3"), -48.0), "{}", run(&mut s, "joint.list", json!({})));
|
||||
assert!(close_to(shown(&mut s, "G2"), -36.0) && close_to(shown(&mut s, "G3"), 0.0), "{}", run(&mut s, "joint.list", json!({})));
|
||||
// Undo takes the redefinition back.
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
@@ -162,10 +169,10 @@ fn a_screw_advances_its_pitch_per_turn() {
|
||||
let travel = r["values"][1]["value"].as_f64().unwrap_or(f64::NAN);
|
||||
assert!(close_to(travel, 3.0), "{r}");
|
||||
assert!(near(place(&s, "Nut", Vec3::new(-1.0, -1.0, 0.0)), Vec3::new(-1.0, -1.0, 3.0)));
|
||||
// A left-hand thread goes the other way.
|
||||
// A left-hand thread goes the other way, from where the nut is.
|
||||
run(&mut s, "joint.screw", json!({"joint": "Thread", "pitch": 1.5, "reverse": true, "link": 0}));
|
||||
let r = run(&mut s, "joint.drive", json!({"joint": "Thread", "values": [360]}));
|
||||
assert!(close_to(r["values"][1]["value"].as_f64().unwrap_or(f64::NAN), -1.5), "{r}");
|
||||
let r = run(&mut s, "joint.drive", json!({"joint": "Thread", "values": [1080]}));
|
||||
assert!(close_to(r["values"][1]["value"].as_f64().unwrap_or(f64::NAN), 1.5), "{r}");
|
||||
// A revolute joint and a slider, the pitch a parameter: changing it changes the lead.
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "parameters.add", json!({"name": "lead", "expression": "2 mm"}));
|
||||
@@ -253,3 +260,135 @@ fn couplings_refuse_what_they_cannot_couple() {
|
||||
}
|
||||
assert_eq!(s.doc.assembly, before);
|
||||
}
|
||||
|
||||
/// Gears of radii 10 and 20 on the grounded base, turned to 30° and 40° before they are coupled.
|
||||
fn meshed_at_30_and_40() -> Session {
|
||||
let mut s = Session::default();
|
||||
base(&mut s);
|
||||
disc(&mut s, "GA", 0.0, "10 mm");
|
||||
disc(&mut s, "GB", 30.0, "20 mm");
|
||||
run(&mut s, "joint.drive", json!({"joint": "GA", "value": 30}));
|
||||
run(&mut s, "joint.drive", json!({"joint": "GB", "value": 40}));
|
||||
let r = run(&mut s, "joint.gear", json!({"a": "GA", "b": "GB", "ra": 10, "rb": 20}));
|
||||
assert!(close_to(r["ratio"].as_f64().unwrap_or(f64::NAN), -0.5), "{r}");
|
||||
s
|
||||
}
|
||||
|
||||
/// The first motion link's offset in degrees, and whether its gear is reversed.
|
||||
fn gear_link(s: &Session) -> (f64, Option<bool>) {
|
||||
let l = s.doc.assembly.links.first();
|
||||
let reverse = l.and_then(|l| match &l.coupling {
|
||||
Some(Coupling::Gear { reverse, .. }) => Some(*reverse),
|
||||
_ => None,
|
||||
});
|
||||
(l.map(|l| l.offset.to_degrees()).unwrap_or(f64::NAN), reverse)
|
||||
}
|
||||
|
||||
/// A gear link redefined in place (turned the other way with `reverse`) leaves the driven gear
|
||||
/// where it is, as a new link does, so the gears stay in mesh at the pose they are in: GB stays
|
||||
/// at 40° rather than jumping to 70° (the old offset kept, the flip pivoting about GA at 0°).
|
||||
/// Undo, redo and saving keep each definition with its pose.
|
||||
#[test]
|
||||
fn a_redefined_gear_stays_in_mesh_where_it_is() {
|
||||
let mut s = meshed_at_30_and_40();
|
||||
// b = -0.5·a + 55: GB keeps its 40° at GA's 30°.
|
||||
let (offset, reverse) = gear_link(&s);
|
||||
assert!(close_to(offset, 55.0) && reverse == Some(false), "{offset} {reverse:?}");
|
||||
assert!(close_to(shown(&mut s, "GB"), 40.0), "{}", shown(&mut s, "GB"));
|
||||
let r = run(&mut s, "joint.gear", json!({"a": "GA", "b": "GB", "ra": 10, "rb": 20, "reverse": true, "link": 0}));
|
||||
assert!(r["link"] == 0 && close_to(r["ratio"].as_f64().unwrap_or(f64::NAN), 0.5), "{r}");
|
||||
// b = 0.5·a + 25.
|
||||
let (offset, reverse) = gear_link(&s);
|
||||
assert!(close_to(offset, 25.0) && reverse == Some(true), "{offset} {reverse:?}");
|
||||
assert!(close_to(shown(&mut s, "GA"), 30.0) && close_to(shown(&mut s, "GB"), 40.0), "{}", run(&mut s, "joint.list", json!({})));
|
||||
assert!(near(rim(&s, "GB", 30.0, 20.0), turned(30.0, 20.0, 40.0)), "{:?}", rim(&s, "GB", 30.0, 20.0));
|
||||
// It turns from there, now the same way as GA.
|
||||
run(&mut s, "joint.drive", json!({"joint": "GA", "value": 0}));
|
||||
assert!(close_to(shown(&mut s, "GB"), 25.0), "{}", shown(&mut s, "GB"));
|
||||
assert!(near(rim(&s, "GB", 30.0, 20.0), turned(30.0, 20.0, 25.0)), "{:?}", rim(&s, "GB", 30.0, 20.0));
|
||||
// Undo goes back through the drive and the redefinition, redo forward again.
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
let (offset, reverse) = gear_link(&s);
|
||||
assert!(close_to(offset, 55.0) && reverse == Some(false), "{offset} {reverse:?}");
|
||||
assert!(close_to(shown(&mut s, "GA"), 30.0) && close_to(shown(&mut s, "GB"), 40.0), "{}", run(&mut s, "joint.list", json!({})));
|
||||
run(&mut s, "edit.redo", json!({}));
|
||||
let (offset, reverse) = gear_link(&s);
|
||||
assert!(close_to(offset, 25.0) && reverse == Some(true), "{offset} {reverse:?}");
|
||||
assert!(close_to(shown(&mut s, "GB"), 40.0), "{}", shown(&mut s, "GB"));
|
||||
run(&mut s, "edit.redo", json!({}));
|
||||
assert!(close_to(shown(&mut s, "GA"), 0.0) && close_to(shown(&mut s, "GB"), 25.0), "{}", run(&mut s, "joint.list", json!({})));
|
||||
// Saved and opened again: the same link, the same pose.
|
||||
let back = solidcraft_doc::format::read(&s.doc.to_json()).unwrap_or_else(|e| panic!("{e}"));
|
||||
assert_eq!(back.assembly.links, s.doc.assembly.links);
|
||||
let vals = solidcraft_doc::joints::joint_values(&back.assembly);
|
||||
let gb = back.assembly.joints.iter().find(|j| j.name == "GB").map(|j| j.id).unwrap_or(0);
|
||||
assert!(vals.get(&gb).and_then(|q| q.first()).is_some_and(|v| close_to(v.to_degrees(), 25.0)), "{vals:?}");
|
||||
}
|
||||
|
||||
/// The other couplings redefined in place keep their driven joint where it is too.
|
||||
#[test]
|
||||
fn a_redefined_rack_keeps_the_rack_where_it_is() {
|
||||
let mut s = rack();
|
||||
run(&mut s, "joint.rack_pinion", json!({"a": "Pinion", "b": "Rail", "radius": 8}));
|
||||
run(&mut s, "joint.drive", json!({"joint": "Pinion", "value": 45}));
|
||||
let travel = 8.0 * std::f64::consts::FRAC_PI_4;
|
||||
run(&mut s, "joint.rack_pinion", json!({"a": "Pinion", "b": "Rail", "radius": 4, "reverse": true, "link": 0}));
|
||||
assert!(close_to(shown(&mut s, "Rail"), travel), "{}", shown(&mut s, "Rail"));
|
||||
run(&mut s, "joint.drive", json!({"joint": "Pinion", "value": 0}));
|
||||
assert!(close_to(shown(&mut s, "Rail"), travel + 4.0 * std::f64::consts::FRAC_PI_4), "{}", shown(&mut s, "Rail"));
|
||||
}
|
||||
|
||||
/// A joint a motion link drives adds no freedom of its own: gears on a grounded base have one
|
||||
/// between them, each turning (and draggable) with it; a part hinged on the driven gear has one
|
||||
/// more; a cylindrical joint whose turn drives its travel (a screw) has one, not two.
|
||||
#[test]
|
||||
fn a_motion_link_takes_its_driven_joints_freedom_on_a_grounded_base() {
|
||||
let mut s = Session::default();
|
||||
base(&mut s);
|
||||
disc(&mut s, "GA", 0.0, "10 mm");
|
||||
disc(&mut s, "GB", 30.0, "20 mm");
|
||||
run(&mut s, "joint.gear", json!({"a": "GA", "b": "GB", "ra": 10, "rb": 20}));
|
||||
for (part, want) in [("Base", 0), ("GA", 1), ("GB", 1)] {
|
||||
assert_eq!(dof_of(&mut s, part), want, "{part}");
|
||||
}
|
||||
part(&mut s, "Arm", [45.0, -1.0, 5.0]);
|
||||
pin(&mut s, "revolute", "Arm", "GB", "Arm", Vec3::new(46.0, 0.0, 5.0), Vec3::Z);
|
||||
assert_eq!(dof_of(&mut s, "Arm"), 2);
|
||||
// The arm's hinge driven by GA too: still the one value.
|
||||
run(&mut s, "joint.motion_link", json!({"a": "GA", "b": "Arm", "ratio": 2}));
|
||||
assert_eq!(dof_of(&mut s, "Arm"), 1);
|
||||
let mut s = Session::default();
|
||||
base(&mut s);
|
||||
part(&mut s, "Nut", [-1.0, -1.0, 0.0]);
|
||||
pin(&mut s, "cylindrical", "Thread", "Base", "Nut", Vec3::ZERO, Vec3::Z);
|
||||
assert_eq!(dof_of(&mut s, "Nut"), 2);
|
||||
run(&mut s, "joint.screw", json!({"joint": "Thread", "pitch": 1.5}));
|
||||
assert_eq!(dof_of(&mut s, "Nut"), 1);
|
||||
}
|
||||
|
||||
/// With nothing grounded, the first joint's A side stays put. A gear there (its joint to the
|
||||
/// housing made from the gear) leaves the housing one turn about it, and the driven gear on the
|
||||
/// housing turns with that same value once the gears are coupled: one, not two. With the
|
||||
/// housing first, each gear has one.
|
||||
#[test]
|
||||
fn a_motion_link_takes_its_driven_joints_freedom_with_nothing_grounded() {
|
||||
for gear_first in [true, false] {
|
||||
let mut s = Session::default();
|
||||
part(&mut s, "Housing", [-10.0, -10.0, -20.0]);
|
||||
loose_disc(&mut s, "GA", 0.0, "10 mm");
|
||||
loose_disc(&mut s, "GB", 30.0, "20 mm");
|
||||
let (a, b) = if gear_first { ("GA", "Housing") } else { ("Housing", "GA") };
|
||||
pin(&mut s, "revolute", "JA", a, b, Vec3::ZERO, Vec3::Z);
|
||||
pin(&mut s, "revolute", "JB", "Housing", "GB", Vec3::new(30.0, 0.0, 0.0), Vec3::Z);
|
||||
let (fixed, free) = if gear_first { ("GA", "Housing") } else { ("Housing", "GA") };
|
||||
assert_eq!(dof_of(&mut s, fixed), 0, "gear first: {gear_first}");
|
||||
assert_eq!(dof_of(&mut s, free), 1, "gear first: {gear_first}");
|
||||
// Before the link GB turns on JB, and with the housing about GA when GA stays put.
|
||||
assert_eq!(dof_of(&mut s, "GB"), if gear_first { 2 } else { 1 }, "gear first: {gear_first}");
|
||||
run(&mut s, "joint.gear", json!({"a": "JA", "b": "JB", "ra": 10, "rb": 20}));
|
||||
for (part, want) in [(fixed, 0), (free, 1), ("GB", 1)] {
|
||||
assert_eq!(dof_of(&mut s, part), want, "{part}, gear first: {gear_first}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,7 +108,9 @@ ratio comes from the parts rather than a typed number:
|
||||
A radius or pitch may be an expression of the design's parameters, and a picked face is found
|
||||
again whenever the design changes. Change the parameter or the gear's diameter, and the ratio
|
||||
follows. The new link leaves the driven joint where it is, so teeth modelled in mesh stay in mesh.
|
||||
**Reverse** turns the driven joint the other way, for joints whose axes point opposite ways. These
|
||||
**Reverse** turns the driven joint the other way, for joints whose axes point opposite ways.
|
||||
Redefining a link (`link: i`, such as turning it the other way with **Reverse**) also leaves the
|
||||
driven joint where it is, so the gears stay in mesh at the pose they are in. These
|
||||
couplings are motion links, so they work in closed loops, with contact sets and in motion studies:
|
||||
a rack can push the piston of a slider-crank, or a pinion can steer through a rack and its tie
|
||||
rods. Drive the joint that drives the coupling (joint 1, the pinion, or the screw's turn), not the
|
||||
@@ -158,7 +160,8 @@ example driving a rocker beyond its swing, the motion stops at the last position
|
||||
closes, and the drive reports `stopped_by_loop`. A loop that cannot close at all is listed
|
||||
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
|
||||
loop locked by a rigid joint has none. A joint a motion link drives adds no freedom of its own:
|
||||
two meshed gears on a fixed housing have one between them, each turning with the driving gear. 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. The dialog opens on the joint
|
||||
|
||||
Reference in New Issue
Block a user