Joint drives say when they move nothing; drive values in degrees; quiet replies (#1304)
joint.drive answers moved, and a reason (with unconnected) when the joint can move nothing; motion.play answers moved and unmoved; joint.list answers floating chains. joint.drive values come back in joint.list's shape (deg/mm). quiet: true trims joint answers and motion.play poses. A single value keeps a joint's other values. Drive Joints shows the floating case (drive.unconnected, all locales).
This commit is contained in:
1 parent
fd19efc6fc
commit
7195e323bf
15 files changed
+326
-79
No files matched your search
@@ -95,7 +95,7 @@ fn slider_limits_clamp() {
|
||||
// B now sits on A.
|
||||
assert!(near(place(&s, "B", Vec3::new(25.0, 5.0, 0.0)), Vec3::new(5.0, 5.0, 10.0)), "{:?}", place(&s, "B", Vec3::new(25.0, 5.0, 0.0)));
|
||||
let r = run(&mut s, "joint.drive", json!({"joint": "Rail", "value": 25}));
|
||||
assert_eq!(r["values"], json!([10.0]), "{r}");
|
||||
assert_eq!((&r["values"][0]["value"], &r["values"][0]["unit"]), (&json!(10.0), &json!("mm")), "{r}");
|
||||
assert!(near(place(&s, "B", Vec3::new(25.0, 5.0, 0.0)), Vec3::new(5.0, 5.0, 20.0)));
|
||||
run(&mut s, "joint.drive", json!({"joint": "Rail", "value": -5}));
|
||||
assert!(near(place(&s, "B", Vec3::new(25.0, 5.0, 0.0)), Vec3::new(5.0, 5.0, 10.0)));
|
||||
@@ -361,8 +361,8 @@ fn four_bar_follows_its_crank() {
|
||||
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}");
|
||||
assert!(r["stopped_by_loop"].is_null(), "{v}°: {r}");
|
||||
// The driven joint keeps exactly the value asked for.
|
||||
assert!((r["values"][0].as_f64().unwrap_or(f64::NAN) - v.to_radians()).abs() < 1e-12, "{r}");
|
||||
// The driven joint keeps exactly the value asked for, answered in degrees as asked.
|
||||
assert!((r["values"][0]["value"].as_f64().unwrap_or(f64::NAN) - v).abs() < 1e-9 && r["values"][0]["unit"] == "deg", "{r}");
|
||||
q = rocker_pin(v - 30.0, v, q);
|
||||
let th = (90.0 + v).to_radians();
|
||||
let p = Vec3::new(20.0 * th.cos(), 20.0 * th.sin(), 0.0);
|
||||
@@ -399,7 +399,7 @@ fn four_bar_rocker_stops_where_the_loop_cannot_close() {
|
||||
let r = run(&mut s, "joint.drive", json!({"joint": "Rocker", "value": 100}));
|
||||
assert!(r["stopped_by_loop"].is_object(), "{r}");
|
||||
assert_eq!(r["conflicts"].as_array().map(Vec::len), Some(0), "{r}");
|
||||
let reached = r["values"][0].as_f64().unwrap_or(f64::NAN).to_degrees();
|
||||
let reached = r["values"][0]["value"].as_f64().unwrap_or(f64::NAN);
|
||||
// |Q|² = 45² + 40² + 2·45·40·cos φ = 30².
|
||||
let limit = ((900.0f64 - 3625.0) / 3600.0).acos().to_degrees() - start;
|
||||
assert!(reached > limit - 5.0 && reached <= limit, "stopped at {reached}°, the limit is {limit}°");
|
||||
@@ -413,7 +413,7 @@ fn four_bar_rocker_stops_where_the_loop_cannot_close() {
|
||||
let (mut s, _, _) = four_bar();
|
||||
let r = run(&mut s, "joint.drive", json!({"joint": "Rocker", "value": 360}));
|
||||
assert!(r["stopped_by_loop"].is_object(), "{r}");
|
||||
assert!(r["values"][0].as_f64().unwrap_or(f64::NAN).to_degrees() <= limit, "{r}");
|
||||
assert!(r["values"][0]["value"].as_f64().unwrap_or(f64::NAN) <= limit, "{r}");
|
||||
}
|
||||
|
||||
/// A loop value a motion link sets follows through the start of its link chain: a dial outside
|
||||
@@ -550,7 +550,7 @@ fn a_locked_loop_does_not_move() {
|
||||
run(&mut s, "joint.as_built", json!({"type": "rigid", "a": "Ground", "b": "Rocker", "name": "Lock"}));
|
||||
let r = run(&mut s, "joint.drive", json!({"joint": "Crank", "value": 360}));
|
||||
assert!(r["stopped_by_loop"].is_object(), "{r}");
|
||||
assert!(r["values"][0].as_f64().unwrap_or(f64::NAN).abs() < 1e-3, "{r}");
|
||||
assert!(r["values"][0]["value"].as_f64().unwrap_or(f64::NAN).to_radians().abs() < 1e-3, "{r}");
|
||||
}
|
||||
|
||||
/// A link that turns the rocker 100 times as far as its dial: a small turn of the dial is a full
|
||||
@@ -576,7 +576,7 @@ fn a_full_turn_stops_from_a_suppressed_dial_or_a_missing_value() {
|
||||
run(&mut s, "joint.motion_link", json!({"a": "Dial", "b": "Rocker", "ratio": 0.5}));
|
||||
run(&mut s, "joint.edit", json!({"joint": "Dial", "suppressed": true}));
|
||||
let r = run(&mut s, "joint.drive", json!({"joint": "Dial", "value": 720}));
|
||||
assert!(r["stopped_by_loop"].is_object(), "{r}");
|
||||
assert!(r["stopped_by_loop"].is_object() && r["moved"] == true && r.get("reason").is_none(), "{r}");
|
||||
// Undriven, the suppressed dial still follows the rocker when the crank turns.
|
||||
run(&mut s, "joint.drive", json!({"joint": "Dial", "value": 0}));
|
||||
let r = run(&mut s, "joint.drive", json!({"joint": "Crank", "value": 60}));
|
||||
@@ -955,3 +955,86 @@ fn a_link_chain_stored_backwards_drives_a_loop() {
|
||||
assert!(near(place(&s, "Rocker", q0), rocker_pin(0.0, v, q0)), "{v}: {:?}", place(&s, "Rocker", q0));
|
||||
}
|
||||
}
|
||||
|
||||
/// A turret on a pedestal joined to nothing grounded (Rover v7's turret): the solver places
|
||||
/// neither, so driving the turret moves nothing. The drive, a motion study and joint.list say
|
||||
/// so, naming the pedestal, instead of answering as if it worked.
|
||||
#[test]
|
||||
fn a_turret_on_a_floating_pedestal_says_it_moves_nothing() {
|
||||
let mut s = Session::default();
|
||||
for (name, corner) in [("Chassis", [0.0, 0.0, 0.0]), ("Arm", [10.0, 0.0, 0.0]), ("Pedestal", [50.0, 0.0, 0.0]), ("Turret", [50.0, 0.0, 2.0])] {
|
||||
part(&mut s, name, corner);
|
||||
}
|
||||
let g = occ(&s, "Chassis");
|
||||
run(&mut s, "occurrence.ground", json!({"occurrence": g, "grounded": true}));
|
||||
pin(&mut s, "revolute", "Shoulder", "Chassis", "Arm", Vec3::new(10.0, 1.0, 1.0), Vec3::Y);
|
||||
pin(&mut s, "revolute", "Azimuth", "Pedestal", "Turret", Vec3::new(51.0, 1.0, 2.0), Vec3::Z);
|
||||
let corner = Vec3::new(52.0, 0.0, 2.0);
|
||||
let start = place(&s, "Turret", corner);
|
||||
let r = run(&mut s, "joint.drive", json!({"joint": "Azimuth", "value": 30, "quiet": true}));
|
||||
assert_eq!((&r["moved"], &r["unconnected"]), (&json!(false), &json!("Pedestal:1")), "{r}");
|
||||
assert!(r["reason"].as_str().is_some_and(|w| w.starts_with("Pedestal:1 is not connected to anything grounded")), "{r}");
|
||||
assert!(r.get("dof").is_none() && r.get("fixed").is_none(), "quiet: {r}");
|
||||
// The value is set and answered in degrees, as asked; the turret stays where it was.
|
||||
assert!((r["values"][0]["value"].as_f64().unwrap_or(f64::NAN) - 30.0).abs() < 1e-9 && r["values"][0]["unit"] == "deg", "{r}");
|
||||
assert!(near(place(&s, "Turret", corner), start));
|
||||
let l = run(&mut s, "joint.list", json!({}));
|
||||
assert_eq!(l["floating"], json!([{"occurrences": ["Pedestal:1", "Turret:1"], "joints": ["Azimuth"]}]), "{l}");
|
||||
// A study keying both joints moves the arm and says the turret can't move.
|
||||
let keys = json!([{"joint": "Azimuth", "points": [[0, 0], [10, 90]]}, {"joint": "Shoulder", "points": [[0, 0], [10, 45]]}]);
|
||||
run(&mut s, "motion.study", json!({"name": "Scan", "steps": 10, "keys": keys}));
|
||||
let r = run(&mut s, "motion.play", json!({"study": "Scan", "step": 10, "quiet": true}));
|
||||
assert_eq!(r["moved"], true, "{r}");
|
||||
let unmoved = r["unmoved"].as_array().cloned().unwrap_or_default();
|
||||
assert!(unmoved.len() == 1 && unmoved[0]["name"] == "Azimuth" && unmoved[0]["reason"].as_str().is_some_and(|w| w.contains("Pedestal:1")), "{r}");
|
||||
// Quiet: only the arm, which moved, has its pose in the answer.
|
||||
assert_eq!(r["positions"].as_array().map(|p| p.iter().map(|x| x["occurrence"].clone()).collect::<Vec<_>>()), Some(vec![json!("Arm:1")]), "{r}");
|
||||
// Fixed to the chassis, the pedestal is placed and the turret turns.
|
||||
run(&mut s, "joint.as_built", json!({"type": "rigid", "a": "Chassis", "b": "Pedestal", "name": "Mount"}));
|
||||
let r = run(&mut s, "joint.drive", json!({"joint": "Azimuth", "value": 60}));
|
||||
assert!(r["moved"] == true && r.get("reason").is_none() && r.get("unconnected").is_none() && r["dof"].is_array(), "{r}");
|
||||
assert!(!near(place(&s, "Turret", corner), start));
|
||||
assert_eq!(run(&mut s, "joint.list", json!({}))["floating"], json!([]));
|
||||
}
|
||||
|
||||
/// `quiet: true` leaves the per-occurrence lists out of joint answers, and `moved` says whether
|
||||
/// a drive moved anything: driving a joint to where it is moves nothing, with no reason given.
|
||||
#[test]
|
||||
fn quiet_joint_answers_and_a_drive_to_where_it_is() {
|
||||
let mut s = two_cubes();
|
||||
let r =
|
||||
run(&mut s, "joint.create", json!({"type": "slider", "a": {"face": [5, 5, 10]}, "b": {"face": [25, 5, 0]}, "name": "Rail", "quiet": true}));
|
||||
assert!(r.get("dof").is_none() && r.get("fixed").is_none() && r["conflicts"].is_array(), "{r}");
|
||||
let r = run(&mut s, "joint.drive", json!({"joint": "Rail", "value": 5, "quiet": true}));
|
||||
assert!(r.get("dof").is_none() && r["moved"] == true, "{r}");
|
||||
let r = run(&mut s, "joint.drive", json!({"joint": "Rail", "value": 5}));
|
||||
assert!(r["dof"].is_array() && r["moved"] == false && r.get("reason").is_none(), "{r}");
|
||||
// A suppressed joint with no motion link moves nothing, and says why.
|
||||
run(&mut s, "joint.edit", json!({"joint": "Rail", "suppressed": true}));
|
||||
let r = run(&mut s, "joint.drive", json!({"joint": "Rail", "value": 8}));
|
||||
assert!(r["moved"] == false && r["reason"] == "Rail is suppressed" && r.get("unconnected").is_none(), "{r}");
|
||||
}
|
||||
|
||||
/// An as-built joint takes a name and limits like any other joint.
|
||||
#[test]
|
||||
fn as_built_takes_a_name_and_limits() {
|
||||
let mut s = two_cubes();
|
||||
let r = run(&mut s, "joint.as_built", json!({"type": "slider", "a": "A", "b": "B", "name": "Rail", "limits": [[0, 5]]}));
|
||||
assert_eq!(r["name"], "Rail", "{r}");
|
||||
let r = run(&mut s, "joint.drive", json!({"joint": "Rail", "value": 20}));
|
||||
assert_eq!((&r["values"][0]["value"], &r["values"][0]["limits"]), (&json!(5.0), &json!([0.0, 5.0])), "{r}");
|
||||
assert!(near(place(&s, "B", Vec3::new(20.0, 0.0, 0.0)), Vec3::new(20.0, 0.0, 5.0)), "{:?}", place(&s, "B", Vec3::new(20.0, 0.0, 0.0)));
|
||||
}
|
||||
|
||||
/// `value` drives a joint's first value only: the others keep theirs, as with a shorter `values`.
|
||||
#[test]
|
||||
fn a_single_value_keeps_the_joints_other_values() {
|
||||
let mut s = two_cubes();
|
||||
run(&mut s, "joint.as_built", json!({"type": "cylindrical", "a": "A", "b": "B", "name": "Spindle"}));
|
||||
run(&mut s, "joint.drive", json!({"joint": "Spindle", "values": [30, 5]}));
|
||||
let at = |r: &Value| [0, 1].map(|i| r["values"][i]["value"].as_f64().unwrap_or(f64::NAN));
|
||||
let r = run(&mut s, "joint.drive", json!({"joint": "Spindle", "value": 45}));
|
||||
assert!((at(&r)[0] - 45.0).abs() < 1e-9 && (at(&r)[1] - 5.0).abs() < 1e-9, "{r}");
|
||||
let r = run(&mut s, "joint.drive", json!({"joint": "Spindle", "values": [60]}));
|
||||
assert!((at(&r)[0] - 60.0).abs() < 1e-9 && (at(&r)[1] - 5.0).abs() < 1e-9, "{r}");
|
||||
}
|
||||
Reference in new issue
Block a user