Joints: a link ratio between an angle and a distance is per degree (#1433)
A motion link's ratio is read in b's units per a's (mm per degree, degrees per mm) when an angle and a distance are linked; stored values stay per radian, so saved designs are unchanged. Follow-up to #1415. Co-authored-by: SychPL <[email protected]>
This commit is contained in:
1 parent
1e984c8e46
commit
02fe495d38
5 files changed
+50
-8
No files matched your search
@@ -39,7 +39,7 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
CommandSpec::new("joint.motion_link", "Motion Link", motion_link)
|
||||
.at("SOLID", "ASSEMBLE")
|
||||
.icon("motion_link")
|
||||
.params("a, b: joints; ratio (b = ratio · a + offset); offset?: in b's units (deg for an angle, mm for a distance, or an expression); ia?, ib?: value indices (default 0)"),
|
||||
.params("a, b: joints; ratio (b = ratio · a + offset): b's units per a's (mm per deg for an angle driving a distance); offset?: in b's units (deg for an angle, mm for a distance, or an expression); ia?, ib?: value indices (default 0)"),
|
||||
CommandSpec::new("joint.limits", "Joint Limits", limits).params("joint; index?: value index (default 0); min?, max? (omit both to clear)"),
|
||||
CommandSpec::new("joint.edit", "Edit Joint", edit).params("joint; type?, flip?, offset?, angle?, suppressed?, name?"),
|
||||
CommandSpec::new("joint.delete", "Delete Joint", delete).params("joint: id|name (or origin: name, link: index)"),
|
||||
@@ -521,16 +521,27 @@ fn motion_link(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
return Err(bad(cmd, "the linked joints need moving values at those indices"));
|
||||
}
|
||||
// In b's units, like its values and limits (stored in radians for an angle).
|
||||
let angle = s.doc.assembly.joints.iter().find(|j| j.id == b).is_some_and(|j| j.kind.is_angle(ib));
|
||||
let offset = match p.get("offset") {
|
||||
Some(v) => value(s, v, angle, cmd)?,
|
||||
Some(v) => value(s, v, is_angle(s, b, ib), cmd)?,
|
||||
None => 0.0,
|
||||
};
|
||||
let ratio = ratio * ratio_scale(s, a, ia, b, ib);
|
||||
s.doc_mut().assembly.links.push(MotionLink { a, ia, b, ib, ratio, offset });
|
||||
let sol = resolve(s);
|
||||
Ok(sol_json(&sol, p))
|
||||
}
|
||||
|
||||
fn is_angle(s: &Session, joint: u64, i: usize) -> bool {
|
||||
s.doc.assembly.joints.iter().find(|j| j.id == joint).is_some_and(|j| j.kind.is_angle(i))
|
||||
}
|
||||
|
||||
/// A link's `ratio` is in b's units per a's (mm per degree, degrees per mm) and stored per
|
||||
/// radian: the given one times this is the stored one.
|
||||
fn ratio_scale(s: &Session, a: u64, ia: usize, b: u64, ib: usize) -> f64 {
|
||||
let unit = |angle: bool| if angle { 1f64.to_radians() } else { 1.0 };
|
||||
unit(is_angle(s, b, ib)) / unit(is_angle(s, a, ia))
|
||||
}
|
||||
|
||||
fn limits(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "joint.limits";
|
||||
let id = joint_id(s, p.get("joint"), cmd)?;
|
||||
@@ -652,9 +663,10 @@ fn list(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
.links
|
||||
.iter()
|
||||
.map(|l| {
|
||||
let angle = s.doc.assembly.joints.iter().find(|j| j.id == l.b).is_some_and(|j| j.kind.is_angle(l.ib));
|
||||
let angle = is_angle(s, l.b, l.ib);
|
||||
let offset = if angle { l.offset.to_degrees() } else { l.offset };
|
||||
json!({"a": l.a, "ia": l.ia, "b": l.b, "ib": l.ib, "ratio": l.ratio, "offset": offset, "unit": if angle { "deg" } else { "mm" }})
|
||||
let ratio = l.ratio / ratio_scale(s, l.a, l.ia, l.b, l.ib);
|
||||
json!({"a": l.a, "ia": l.ia, "b": l.b, "ib": l.ib, "ratio": ratio, "offset": offset, "unit": if angle { "deg" } else { "mm" }})
|
||||
})
|
||||
.collect();
|
||||
let origins: Vec<Value> = s.doc.assembly.origins.iter().map(|o| json!({"name": o.name, "occurrence": o.origin.occurrence})).collect();
|
||||
|
||||
@@ -1122,6 +1122,35 @@ fn a_link_offset_is_in_the_linked_values_units() {
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// A link between an angle and a distance takes its ratio in b's units per a's, like its offset:
|
||||
/// a pinion moves its rack 0.5 mm per degree, and a car turns its dial 2 degrees per mm.
|
||||
/// `joint.list` answers the ratios the same way; they are stored per radian, as before.
|
||||
#[test]
|
||||
fn a_link_ratio_between_an_angle_and_a_distance_is_per_degree() {
|
||||
let mut s = Session::default();
|
||||
for (name, x) in [("Ground", 0.0), ("Pinion", 80.0), ("Rack", 100.0), ("Car", 120.0), ("Dial", 140.0)] {
|
||||
part(&mut s, name, [x, 0.0, -5.0]);
|
||||
}
|
||||
let g = occ(&s, "Ground");
|
||||
run(&mut s, "occurrence.ground", json!({"occurrence": g, "grounded": true}));
|
||||
for (kind, name, x) in [("revolute", "Pinion", 80.0), ("slider", "Rack", 100.0), ("slider", "Car", 120.0), ("revolute", "Dial", 140.0)] {
|
||||
pin(&mut s, kind, name, "Ground", name, Vec3::new(x, 0.0, 0.0), Vec3::Z);
|
||||
}
|
||||
run(&mut s, "joint.motion_link", json!({"a": "Pinion", "b": "Rack", "ratio": 0.5}));
|
||||
run(&mut s, "joint.motion_link", json!({"a": "Car", "b": "Dial", "ratio": 2}));
|
||||
run(&mut s, "joint.drive", json!({"joint": "Pinion", "value": 20}));
|
||||
run(&mut s, "joint.drive", json!({"joint": "Car", "value": 3}));
|
||||
let l = run(&mut s, "joint.list", json!({}));
|
||||
let value = |name: &str| {
|
||||
l["joints"].as_array().into_iter().flatten().find(|j| j["name"] == name).and_then(|j| j["values"][0]["value"].as_f64()).unwrap_or(f64::NAN)
|
||||
};
|
||||
assert!((value("Rack") - 10.0).abs() < 1e-9 && (value("Dial") - 6.0).abs() < 1e-9, "{l}");
|
||||
let ratios: Vec<f64> = l["links"].as_array().into_iter().flatten().map(|k| k["ratio"].as_f64().unwrap_or(f64::NAN)).collect();
|
||||
assert!(ratios.len() == 2 && (ratios[0] - 0.5).abs() < 1e-9 && (ratios[1] - 2.0).abs() < 1e-9, "{l}");
|
||||
let stored: Vec<f64> = s.doc.assembly.links.iter().map(|k| k.ratio).collect();
|
||||
assert!((stored[0] - 0.5f64 / 1f64.to_radians()).abs() < 1e-9 && (stored[1] - 2f64.to_radians()).abs() < 1e-12, "{stored:?}");
|
||||
}
|
||||
|
||||
/// A new joint starts at its values inside its limits. Limits that leave out 0 (or the values
|
||||
/// given) start it at the nearer limit, which it keeps, and the answer says the part moved there.
|
||||
#[test]
|
||||
|
||||
@@ -878,7 +878,8 @@ pub fn commands(app: &SolveApp, k: &Asm, inputs: &[SelInput]) -> Result<Vec<(Str
|
||||
Asm::MotionLink { a, b, ia, ib, ratio, offset } => {
|
||||
let js = &s.doc.assembly.joints;
|
||||
let (Some(ja), Some(jb)) = (js.get(*a), js.get(*b)) else { return Err("needs two joints".into()) };
|
||||
// The offset as typed: the engine reads it in Joint 2's units (deg or mm).
|
||||
// The offset as typed: the engine reads it in Joint 2's units (deg or mm), and the
|
||||
// ratio in Joint 2's units per Joint 1's.
|
||||
let ratio = eval(ratio, ValueKind::Unitless)?;
|
||||
cmd("joint.motion_link", json!({"a": ja.id, "b": jb.id, "ia": ia, "ib": ib, "ratio": ratio, "offset": offset.trim()}))
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ Written against SolidCraft `main` after 0.11.1, which counts the degrees of free
|
||||
- **Every chain must reach something grounded.** A part that is neither grounded nor joined to a grounded part is never placed. A drive on a joint in that chain moves nothing and answers `moved: false`, with a `reason` and the part joined to nothing as `unconnected`. `joint.list` lists such chains under `floating`, and their occurrences have `dof: 6`. Fix it with `joint.rigid_group {"occurrences":["Frame:1", ...]}`, or with a joint to the grounded structure.
|
||||
|
||||
## Linked and closed motion
|
||||
- **Motion links:** `joint.motion_link {"a":..., "b":..., "ratio":r, "offset":o}` sets b = r·a + o, clamped to b's own limits. `offset` is in b's units: degrees for an angle, mm for a distance, or an expression. A mirrored part takes `ratio: -1`, and limits mirrored too. Chains (a drives b, b drives c) work in any order of creation. Drive only the head of a chain.
|
||||
- **Motion links:** `joint.motion_link {"a":..., "b":..., "ratio":r, "offset":o}` sets b = r·a + o, clamped to b's own limits. `ratio` is b's units per a's: a pinion that moves its rack 0.5 mm per degree takes `ratio: 0.5`. `offset` is in b's units: degrees for an angle, mm for a distance, or an expression. A mirrored part takes `ratio: -1`, and limits mirrored too. Chains (a drives b, b drives c) work in any order of creation. Drive only the head of a chain.
|
||||
- **Closed loops:** an actuator between a frame and a hinged panel takes three joints besides the hinge:
|
||||
- a revolute root pin (frame and barrel)
|
||||
- a slider (barrel and rod), with z along the actuator from root to tip on both origins, both at the root pin, and the stroke as its limits
|
||||
|
||||
@@ -61,7 +61,7 @@ A slider's value grows as b moves along +z of its origin. Give its two origins t
|
||||
- `joint.create` and `joint.as_built` start the joint at its `values` (default 0). A value outside the limits starts at the nearer limit, and the reply's `warnings` say so: the part turned as it was joined.
|
||||
- `joint.as_built` takes occurrences, not origins. `at` is the joint's frame (default: b's component origin).
|
||||
- `joint.rigid_group` joins every occurrence to the first one with a rigid as-built joint.
|
||||
- `joint.motion_link`: b = ratio·a + offset, clamped to b's limits. `offset` is in b's units: degrees for an angle, mm for a distance, or an expression such as `"10 deg"`. `joint.list` gives each link's `offset` with its `unit`. `ia`/`ib` pick a value of a multi-value joint (default 0). `joint.delete {"link": n}` removes link n (its index in `joint.list` `links`).
|
||||
- `joint.motion_link`: b = ratio·a + offset, clamped to b's limits. `ratio` is in b's units per a's: mm per degree when an angle drives a distance (a pinion of pitch radius r moves its rack r·π/180 mm per degree), degrees per mm the other way, and a plain factor between two angles or two distances. `offset` is in b's units: degrees for an angle, mm for a distance, or an expression such as `"10 deg"`. `joint.list` gives each link's `ratio` the same way and its `offset` with its `unit`. `ia`/`ib` pick a value of a multi-value joint (default 0). `joint.delete {"link": n}` removes link n (its index in `joint.list` `links`).
|
||||
- `joint.drive`: `values` drives the values it lists, in order (`Spindle` above is cylindrical: 30 degrees, then 5 mm), and `value` the first; values left out keep theirs, or follow in a closed loop. Values are clamped to the limits. The reply gives `values` as `joint.list` does (`{name, value, unit, limits}`), and `moved`; when the joint can move nothing, a `reason`.
|
||||
- `motion.study {"study":..., ...}` edits a study; `keys` given replace all keys.
|
||||
- `motion.play` sets the joints to the study's values at a step (fractions work) and changes the design. `motion.export` does not change it.
|
||||
|
||||
Reference in new issue
Block a user