Drive Joints opens on the picked joint; ui.start takes params (#1297)
Drive Joints opens on the joint picked in the browser, else the first joint that moves, and won't start on a rigid joint. ui.start takes params: joint.drive opens on a joint (id or name) at given values, driving nothing until OK; a command or param it doesn't take is refused.
This commit is contained in:
1 parent
0ee5e97db5
commit
849426e866
5 files changed
+145
-3
No files matched your search
@@ -336,7 +336,25 @@ fn dispatch(app: &mut SolveApp, ctx: &egui::Context, method: &str, p: &Value) ->
|
||||
"ui.camera" => wrap(camera(app, p)),
|
||||
"ui.start" => {
|
||||
let Some(id) = s("command") else { return err("missing `command`") };
|
||||
// `params`: what the dialog opens on. Refused: nothing starts, or the dialog closes.
|
||||
let params = p.get("params").filter(|v| !v.is_null());
|
||||
if params.is_some() && id != "joint.drive" {
|
||||
return err(format!("`{id}` takes no ui.start params yet (joint.drive takes joint, values)"));
|
||||
}
|
||||
app.start(id);
|
||||
if let Some(params) = params {
|
||||
if app.tree.capture_prompt.is_some() {
|
||||
return err("Drive Joints waits for the Capture Position question (ui.capture)");
|
||||
}
|
||||
let Some(crate::dialogs::Kind::Assembly(k)) = app.dialog.as_mut().map(|d| &mut d.kind) else {
|
||||
let why = app.status.as_ref().filter(|s| s.2).map(|s| s.0.clone());
|
||||
return err(why.unwrap_or_else(|| "Drive Joints did not open".into()));
|
||||
};
|
||||
if let Err(e) = crate::dialogs_assembly::preset(&app.session, k, params) {
|
||||
crate::dialogs::cancel(app);
|
||||
return err(e);
|
||||
}
|
||||
}
|
||||
ok(json!({"tool": app.tool.as_ref().map(|t| t.cmd), "dialog": app.dialog.as_ref().map(|d| format!("{d:?}"))}))
|
||||
}
|
||||
"ui.move" | "ui.click" => {
|
||||
@@ -1251,4 +1269,48 @@ mod tests {
|
||||
let r = empty.call("ui.camera", json!({"yaw": 1, "fit": true}));
|
||||
assert!(r["ok"] == false && empty.app.cam == before, "{r}");
|
||||
}
|
||||
|
||||
/// `ui.start` params set what a dialog opens on: Drive Joints on a joint (by name or id) at
|
||||
/// the given values, driving nothing yet. A param it doesn't take is refused and closes it.
|
||||
#[test]
|
||||
fn ui_start_params_open_drive_joints_where_asked() {
|
||||
let mut s = Session::default();
|
||||
for (name, x) in [("A", 0), ("B", 30), ("C", 60)] {
|
||||
s.execute("component.activate", &json!({"component": "root"})).unwrap();
|
||||
s.execute("component.create", &json!({ "name": name })).unwrap();
|
||||
s.execute("solid.box", &json!({"length": 20, "width": 20, "height": 10, "corner": [x, 0, 0]})).unwrap();
|
||||
}
|
||||
s.execute("component.activate", &json!({"component": "root"})).unwrap();
|
||||
s.execute("occurrence.ground", &json!({"occurrence": "A:1", "grounded": true})).unwrap();
|
||||
let at = |o: &str, x: i32| json!({"occurrence": o, "point": [x, 10, 5], "z": [1, 0, 0]});
|
||||
s.execute("joint.create", &json!({"type": "revolute", "a": at("A", 25), "b": at("B", 25), "flip": true, "name": "Hinge"})).unwrap();
|
||||
s.execute("joint.create", &json!({"type": "slider", "a": at("B", 55), "b": at("C", 55), "flip": true, "name": "Rail"})).unwrap();
|
||||
let mut h = crate::scenario::Harness::new();
|
||||
h.app = SolveApp::new(s, Services::default());
|
||||
h.app.viewport.no_pixels = true;
|
||||
let before = h.app.session.doc.clone();
|
||||
let drive = |h: &crate::scenario::Harness| match h.app.dialog.as_ref().map(|d| &d.kind) {
|
||||
Some(crate::dialogs::Kind::Assembly(crate::dialogs_assembly::Asm::Drive { joint, values, .. })) => {
|
||||
(h.app.session.doc.assembly.joints[*joint].name.clone(), values.clone())
|
||||
}
|
||||
other => panic!("{other:?}"),
|
||||
};
|
||||
let r = h.call("ui.start", json!({"command": "joint.drive", "params": {"joint": "Rail", "values": [12.5]}}));
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
assert_eq!(drive(&h), ("Rail".to_string(), vec!["12.5 mm".to_string()]));
|
||||
let hinge = h.app.session.doc.assembly.joints[0].id;
|
||||
h.call("ui.start", json!({"command": "joint.drive", "params": {"joint": hinge, "values": ["45 deg"]}}));
|
||||
assert_eq!(drive(&h), ("Hinge".to_string(), vec!["45 deg".to_string()]));
|
||||
assert_eq!(h.app.session.doc, before, "nothing is driven until OK");
|
||||
for bad in [json!({"joint": "Nope"}), json!({"joint": "Rail", "value": 3}), json!({"values": [1, 2]}), json!({"values": [f64::MAX]})] {
|
||||
let r = h.call("ui.start", json!({"command": "joint.drive", "params": bad}));
|
||||
assert_eq!(r["ok"], false, "{bad}: {r}");
|
||||
assert!(h.app.dialog.is_none(), "{bad}");
|
||||
}
|
||||
for command in ["solid.extrude", "sketch.create", "inspect.zebra"] {
|
||||
let r = h.call("ui.start", json!({"command": command, "params": {"distance": 5}}));
|
||||
assert!(r["ok"] == false && r["error"].as_str().unwrap().contains("takes no ui.start params"), "{r}");
|
||||
assert!(h.app.dialog.is_none() && h.app.tool.is_none() && h.app.session.analysis.is_none(), "{command} started nothing");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -152,7 +152,14 @@ pub fn start(app: &SolveApp, id: &str) -> Option<(Kind, Vec<SelInput>)> {
|
||||
"joint.origin" => (Asm::JointOrigin { name: String::new(), hover: None }, vec![SelInput::new("Snap", SNAPS, false)]),
|
||||
"joint.rigid_group" => (Asm::RigidGroup, vec![SelInput::new("Components", BODIES, true)]),
|
||||
"joint.drive" => {
|
||||
let joint = movable(s).first().copied().unwrap_or(0);
|
||||
// The joint picked in the browser, else the first that moves.
|
||||
let list = movable(s);
|
||||
let picked = app
|
||||
.tree
|
||||
.picked_joints
|
||||
.iter()
|
||||
.find_map(|id| list.iter().copied().find(|i| s.doc.assembly.joints.get(*i).is_some_and(|j| j.id == *id)));
|
||||
let joint = picked.or(list.first().copied()).unwrap_or(0);
|
||||
(Asm::Drive { joint, values: current_values(s, joint), focus: true }, vec![])
|
||||
}
|
||||
"joint.motion_link" => {
|
||||
@@ -167,6 +174,48 @@ pub fn start(app: &SolveApp, id: &str) -> Option<(Kind, Vec<SelInput>)> {
|
||||
Some((Kind::Assembly(asm), inputs))
|
||||
}
|
||||
|
||||
/// What an open dialog starts on when the control channel's `ui.start` passes `params`. Drive
|
||||
/// Joints takes `joint` (id or name) and `values` (numbers: degrees or millimetres; text: an
|
||||
/// expression), as `joint.drive` does. Nothing is driven until OK.
|
||||
pub fn preset(s: &Session, k: &mut Asm, p: &Value) -> Result<(), String> {
|
||||
let Asm::Drive { joint, values, .. } = k else { return Err(format!("{} takes no params yet", k.title())) };
|
||||
let o = p.as_object().ok_or("`params` must be an object")?;
|
||||
if let Some(key) = o.keys().find(|k| !matches!(k.as_str(), "joint" | "values")) {
|
||||
return Err(format!("unknown param `{key}` (Drive Joints takes joint, values)"));
|
||||
}
|
||||
if let Some(v) = o.get("joint") {
|
||||
let key = match v {
|
||||
Value::Number(n) => n.to_string(),
|
||||
Value::String(x) => x.clone(),
|
||||
_ => return Err("`joint` must be an id or a name".into()),
|
||||
};
|
||||
let i = s.doc.assembly.joints.iter().position(|j| j.id.to_string() == key || j.name == key).ok_or_else(|| format!("no joint `{key}`"))?;
|
||||
if !movable(s).contains(&i) {
|
||||
return Err(format!("joint `{key}` is rigid: it has nothing to drive"));
|
||||
}
|
||||
*joint = i;
|
||||
*values = current_values(s, i);
|
||||
}
|
||||
if let Some(v) = o.get("values") {
|
||||
let j = s.doc.assembly.joints.get(*joint).ok_or("no joint that moves yet")?;
|
||||
let list = v.as_array().ok_or("`values` must be a list")?;
|
||||
if list.len() > values.len() {
|
||||
return Err(format!("`{}` takes {} value(s)", j.name, values.len()));
|
||||
}
|
||||
for (i, (slot, v)) in values.iter_mut().zip(list).enumerate() {
|
||||
*slot = match v {
|
||||
Value::Number(n) => {
|
||||
let x = n.as_f64().filter(|x| x.is_finite() && x.abs() < 1e9).ok_or("values must be finite numbers")?;
|
||||
num(x, if j.kind.is_angle(i) { "deg" } else { "mm" })
|
||||
}
|
||||
Value::String(t) if t.len() <= 1000 => t.clone(),
|
||||
_ => return Err("values must be numbers or expressions".into()),
|
||||
};
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Indices (into the design's joints) of joints that move (not rigid).
|
||||
fn movable(s: &Session) -> Vec<usize> {
|
||||
s.doc.assembly.joints.iter().enumerate().filter(|(_, j)| j.kind != JointKind::Rigid).map(|(i, _)| i).collect()
|
||||
|
||||
@@ -460,3 +460,33 @@ fn drive_says_when_its_joint_is_joined_to_nothing_grounded() {
|
||||
crate::dialogs::run_dialog(&mut app, &d).unwrap();
|
||||
assert_eq!(app.status.clone().map(|(status, _, error)| (status, error)), Some((why, false)));
|
||||
}
|
||||
|
||||
/// Drive Joints opens on the joint picked in the browser; with none picked, on the first joint
|
||||
/// that moves.
|
||||
#[test]
|
||||
fn drive_opens_on_the_picked_joint() {
|
||||
let mut app = four_bar();
|
||||
let drive_on = |app: &mut SolveApp| {
|
||||
app.dialog = None;
|
||||
app.start("joint.drive");
|
||||
match &app.dialog.as_ref().unwrap().kind {
|
||||
Kind::Assembly(Asm::Drive { joint, .. }) => app.session.doc.assembly.joints[*joint].name.clone(),
|
||||
k => panic!("{k:?}"),
|
||||
}
|
||||
};
|
||||
assert_eq!(drive_on(&mut app), "Crank");
|
||||
let rocker = app.session.doc.assembly.joints.iter().find(|j| j.name == "Rocker").unwrap().id;
|
||||
pick_joint(&mut app, rocker, false);
|
||||
assert_eq!(drive_on(&mut app), "Rocker");
|
||||
}
|
||||
|
||||
/// A rigid joint has nothing to drive: Drive Joints won't start on it.
|
||||
#[test]
|
||||
fn drive_preset_refuses_a_rigid_joint() {
|
||||
let mut app = two_boxes();
|
||||
let at = |o: &str| json!({"occurrence": o, "point": [25, 10, 5], "z": [1, 0, 0]});
|
||||
app.run("joint.create", json!({"type": "rigid", "a": at("A"), "b": at("B"), "name": "Glue"})).unwrap();
|
||||
let mut k = Asm::Drive { joint: 0, values: Vec::new(), focus: true };
|
||||
let e = preset(&app.session, &mut k, &json!({"joint": "Glue"})).unwrap_err();
|
||||
assert!(e.contains("rigid"), "{e}");
|
||||
}
|
||||
@@ -39,7 +39,8 @@ part still has with its loops closed: the moving parts of a four-bar have one be
|
||||
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.
|
||||
status line repeats it. The value box keeps what you typed. The dialog opens on the joint
|
||||
picked in the browser, or on the first joint that moves when none is picked.
|
||||
|
||||
The solver places what it reaches from the root and from grounded components. A chain of joints
|
||||
that is joined to nothing grounded is never placed, so driving one of its joints moves nothing.
|
||||
|
||||
@@ -105,7 +105,7 @@ unit regardless of the design, pass an expression with one: `"value": "50 mm"` o
|
||||
| `ui.set` | any `UiState` field (`tab`, `perspective`, `showGrid`, `dark`, …), and `hiddenBodies`, `hiddenOrigin`, `hiddenSketches`, `shownSketches`, `hiddenJoints` (whole lists; `browser.visibility` changes single items) | `dark: false` switches to the light theme |
|
||||
| `ui.view` | `view`: `front`, `back`, `top`, `bottom`, `left`, `right`, `iso`, `home`, `fit`; `animate` (bool, default false: snap) | |
|
||||
| `ui.camera` | `target?`: `[x, y, z]` (mm), `yaw?`, `pitch?`, `roll?` (radians), `distance?` (mm), `fov?`, `up?`; `fit?`: `true`, `false` or `[occurrence id or name…]` (`root`: the bodies outside every component) | the viewport's orbit camera: it looks at `target` from `distance` away; `yaw` 0 looks along +Y (from the front), `pitch` is the elevation (±π/2: straight down or up), `roll` turns the image. Fields given are set first (the others keep their values), then `fit` frames the listed occurrences (or everything), keeping the direction. The reply, passed back, sets the same view again, so a framing can be stored and replayed. `fov` and `up` follow the perspective setting (`ui.set`) and the up axis preference (`ui.prefs`) and can't change here; a refused field changes nothing |
|
||||
| `ui.start` | `command` | like a toolbar click: starts the sketch tool or dialog for the command |
|
||||
| `ui.start` | `command`, `params?` | like a toolbar click: starts the sketch tool or dialog for the command. `params` sets what the dialog opens on, and nothing runs until OK: `joint.drive` takes `joint` (id or name; without it, the joint picked in the browser, else the first joint that moves) and `values` (numbers in degrees or millimetres, or expressions). A command that takes no params is refused before it starts; a param the dialog doesn't take is refused and the dialog closes |
|
||||
| `ui.click` | `x`, `y` (screen points), `button?`: left/right/middle, `shift?`, `ctrl?`, `double?` | real pointer input |
|
||||
| `ui.move` | `x`, `y`, `shift?`, `alt?`, `ctrl?`, `cmd?` | pointer movement without a button; modifiers apply until the next synthetic move/click; a move with omitted modifiers releases them. With `nav: "touchpad"`, Shift pans and Alt/Option orbits. |
|
||||
| `ui.drag` | `x0`, `y0`, `x1`, `y1`, `button?`, `shift?`, `ctrl?`, `steps?`, `hold?`, `hold_end?` | press, move, release; a left drag on the model is a box selection (left to right: window, right to left: crossing); with OpenSCAD navigation, plain left-drag on empty space orbits, Shift+left-drag box-selects, and drags starting on sketch geometry edit it; `hold` keeps the button down that many frames first (a right press-and-hold opens the marking menu, releasing over a direction picks it), `hold_end` that many frames at the end (a sketch drag shows its snap) |
|
||||
|
||||
Reference in new issue
Block a user