Drive Joints says where and why a drive stopped short (#1169)
Drive Joints says where and why a drive stopped short.
This commit is contained in:
1 parent
5744b1f327
commit
01b1b70586
12 files changed
+165
-9
No files matched your search
@@ -404,6 +404,8 @@
|
||||
"ui.updateToCurrentView": "Auf aktuelle Ansicht aktualisieren",
|
||||
"ui.zoom": "Zoomen",
|
||||
"ui.command_done": "{command} fertig",
|
||||
"drive.stopped_by_loop": "Angehalten bei {value}: die Schleife über {joint} lässt sich nicht weiter schließen",
|
||||
"drive.stopped_by_contact": "Angehalten bei {value}: {a} würde {b} durchdringen",
|
||||
"cmd.appearance_assign": "Erscheinungsbild",
|
||||
"cmd.appearance_library": "Erscheinungsbild-Bibliothek",
|
||||
"cmd.appearance_list": "Erscheinungsbilder auflisten",
|
||||
|
||||
@@ -957,6 +957,8 @@
|
||||
"ui.updateToCurrentView": "Update to Current View",
|
||||
"ui.zoom": "Zoom",
|
||||
"ui.command_done": "{command} done",
|
||||
"drive.stopped_by_loop": "Stopped at {value}: the loop through {joint} can't close any further",
|
||||
"drive.stopped_by_contact": "Stopped at {value}: {a} would pass through {b}",
|
||||
"timeline.group": "New Feature Folder",
|
||||
"timeline.ungroup": "Remove Feature Folders",
|
||||
"timeline.rename_group": "Rename Feature Folder",
|
||||
|
||||
@@ -957,6 +957,8 @@
|
||||
"ui.updateToCurrentView": "現在のビューに更新",
|
||||
"ui.zoom": "ズーム",
|
||||
"ui.command_done": "{command} 完了",
|
||||
"drive.stopped_by_loop": "{value} で停止: {joint} を通るループはこれ以上閉じられません",
|
||||
"drive.stopped_by_contact": "{value} で停止: {a} が {b} を通り抜けてしまいます",
|
||||
"timeline.group": "フィーチャーフォルダを新規作成",
|
||||
"timeline.ungroup": "フィーチャーフォルダを削除",
|
||||
"timeline.rename_group": "フィーチャーフォルダの名前を変更",
|
||||
|
||||
@@ -404,6 +404,8 @@
|
||||
"ui.updateToCurrentView": "현재 보기로 업데이트",
|
||||
"ui.zoom": "확대/축소",
|
||||
"ui.command_done": "{command} 완료",
|
||||
"drive.stopped_by_loop": "{value}에서 멈춤: {joint}을(를) 지나는 루프를 더 이상 닫을 수 없습니다",
|
||||
"drive.stopped_by_contact": "{value}에서 멈춤: {a}이(가) {b}을(를) 통과하게 됩니다",
|
||||
"cmd.appearance_assign": "외관",
|
||||
"cmd.appearance_library": "외관 라이브러리",
|
||||
"cmd.appearance_list": "외관 목록",
|
||||
|
||||
@@ -949,6 +949,8 @@
|
||||
"ui.updateToCurrentView": "Обновить до текущего вида",
|
||||
"ui.zoom": "Масштаб",
|
||||
"ui.command_done": "{command} — готово",
|
||||
"drive.stopped_by_loop": "Остановлено на {value}: контур через {joint} дальше не замыкается",
|
||||
"drive.stopped_by_contact": "Остановлено на {value}: {a} прошёл бы сквозь {b}",
|
||||
"timeline.group": "Новая папка операций",
|
||||
"timeline.ungroup": "Убрать папки операций",
|
||||
"timeline.rename_group": "Переименовать папку операций",
|
||||
|
||||
@@ -955,6 +955,8 @@
|
||||
"ui.updateToCurrentView": "更新为当前视图",
|
||||
"ui.zoom": "缩放",
|
||||
"ui.command_done": "{command} 完成",
|
||||
"drive.stopped_by_loop": "停在 {value}:经过 {joint} 的环路无法再闭合",
|
||||
"drive.stopped_by_contact": "停在 {value}:{a} 会穿过 {b}",
|
||||
"timeline.group": "新建特征文件夹",
|
||||
"timeline.ungroup": "移除特征文件夹",
|
||||
"timeline.rename_group": "重命名特征文件夹",
|
||||
|
||||
@@ -3030,7 +3030,7 @@ fn sels(d: &Dialog, i: usize) -> &[Sel] {
|
||||
d.inputs.get(i).map(|x| x.items.as_slice()).unwrap_or(&[])
|
||||
}
|
||||
|
||||
fn run_dialog(app: &mut SolveApp, d: &Dialog) -> Result<(), String> {
|
||||
pub(crate) fn run_dialog(app: &mut SolveApp, d: &Dialog) -> Result<(), String> {
|
||||
let commands = apply_commands(app, d)?;
|
||||
// The live preview may be computing these very commands: OK takes its result.
|
||||
crate::preview::settle(app, &commands);
|
||||
@@ -3039,12 +3039,22 @@ fn run_dialog(app: &mut SolveApp, d: &Dialog) -> Result<(), String> {
|
||||
roll_marker(app, marker)?;
|
||||
}
|
||||
for (cmd, params) in commands {
|
||||
if let Err(error) = app.run(&cmd, params) {
|
||||
if let Some((id, _)) = d.editing {
|
||||
let position = app.session.doc.feature_index(id);
|
||||
let _ = roll_marker(app, position);
|
||||
match app.run(&cmd, params) {
|
||||
Ok(reply) => {
|
||||
// A drive that stopped short says where, and why.
|
||||
if cmd == "joint.drive"
|
||||
&& let Some(why) = crate::dialogs_assembly::drive_stop(&app.session, &reply)
|
||||
{
|
||||
app.set_status(why, false);
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
if let Some((id, _)) = d.editing {
|
||||
let position = app.session.doc.feature_index(id);
|
||||
let _ = roll_marker(app, position);
|
||||
}
|
||||
return Err(error);
|
||||
}
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -548,6 +548,13 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Asm, inputs: &mut [Se
|
||||
);
|
||||
ui.end_row();
|
||||
}
|
||||
// The preview drove the joint: where it stopped short, and why (a closed loop or a
|
||||
// contact set), so the values asked for are not taken for where it is.
|
||||
if let Some(why) = app.preview.stopped.as_ref().and_then(|r| drive_stop(&app.session, r)) {
|
||||
row_label(ui, "");
|
||||
ui.label(RichText::new(why).color(t.warning));
|
||||
ui.end_row();
|
||||
}
|
||||
}
|
||||
Asm::MotionLink { a, b, ia, ib, ratio, offset } => {
|
||||
let s = &app.session;
|
||||
@@ -834,6 +841,11 @@ pub fn world_preview(s: &Session, cmds: &[(String, Value)], colors: Colors) -> O
|
||||
Ok(preview) => preview,
|
||||
Err(error) => return Some(Err(error.to_string())),
|
||||
};
|
||||
let stopped = cmds
|
||||
.iter()
|
||||
.zip(&preview.replies)
|
||||
.find(|((c, _), r)| c == "joint.drive" && (r["stopped_by_loop"].is_object() || r["stopped_by_contact"].is_object()))
|
||||
.map(|(_, r)| r.clone());
|
||||
let (before, after) = (preview.before, preview.after);
|
||||
// A moved component looks like itself (no new surface), with its old place as the ghost.
|
||||
let colors = Colors { added: colors.body, ..colors };
|
||||
@@ -841,12 +853,36 @@ pub fn world_preview(s: &Session, cmds: &[(String, Value)], colors: Colors) -> O
|
||||
Some(match built {
|
||||
Ok(mut b) => {
|
||||
b.bounds = after.bodies.iter().fold(Aabb3::EMPTY, |acc, x| acc.union(&x.mesh().bounds()));
|
||||
b.stopped = stopped;
|
||||
Ok(b)
|
||||
}
|
||||
Err(_) => Err("the preview failed".into()),
|
||||
})
|
||||
}
|
||||
|
||||
/// What a drive that stopped short of the asked values says: where the joint got to and why
|
||||
/// (`None`: it went all the way). `reply` is `joint.drive`'s result.
|
||||
pub fn drive_stop(s: &Session, reply: &Value) -> Option<String> {
|
||||
let id = reply.get("joint").and_then(Value::as_u64)?;
|
||||
let j = s.doc.assembly.joints.iter().find(|j| j.id == id)?;
|
||||
let at: Vec<String> = reply
|
||||
.get("values")
|
||||
.and_then(Value::as_array)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.enumerate()
|
||||
.filter_map(|(i, v)| v.as_f64().map(|v| (i, v)))
|
||||
.map(|(i, v)| if j.kind.is_angle(i) { num((v.to_degrees() * 10.0).round() / 10.0, "deg") } else { num((v * 100.0).round() / 100.0, "mm") })
|
||||
.collect();
|
||||
let at = at.join(", ");
|
||||
let name = |v: &Value| v.as_str().unwrap_or_default().to_string();
|
||||
if let Some(l) = reply.get("stopped_by_loop").filter(|v| v.is_object()) {
|
||||
return Some(crate::i18n::format("drive.stopped_by_loop", &[("value", &at), ("joint", &name(&l["joint"]))]));
|
||||
}
|
||||
let c = reply.get("stopped_by_contact").filter(|v| v.is_object())?;
|
||||
Some(crate::i18n::format("drive.stopped_by_contact", &[("value", &at), ("a", &name(&c["a"])), ("b", &name(&c["b"]))]))
|
||||
}
|
||||
|
||||
// ---- overlay ----
|
||||
|
||||
/// Snap glyphs under the cursor and at the picked origins; the overlap of an interference.
|
||||
|
||||
@@ -364,3 +364,73 @@ fn edit_feature_on_a_moved_component_refills_in_the_world() {
|
||||
assert_eq!(body, "Body2");
|
||||
assert!(point.dist(Vec3::new(40.0, 0.0, 25.0)) < 1e-6, "{point:?}");
|
||||
}
|
||||
|
||||
/// A crank-rocker four-bar (crank 20, coupler 50, rocker 40, ground pivots 45 apart), built as
|
||||
/// it stands with the crank at 90°: joints Crank, Crank pin, Rocker pin, Rocker.
|
||||
fn four_bar() -> SolveApp {
|
||||
let mut s = Session::default();
|
||||
let (p0, o4) = ([0.0, 20.0], [45.0, 0.0]);
|
||||
// The rocker pin: where the coupler (50 from the crank pin) meets the rocker (40 from O4).
|
||||
let (dx, dy) = (o4[0] - p0[0], o4[1] - p0[1]);
|
||||
let l = f64::hypot(dx, dy);
|
||||
let a = (2500.0 - 1600.0 + l * l) / (2.0 * l);
|
||||
let h = (2500.0 - a * a).sqrt();
|
||||
let q0 = [p0[0] + dx * a / l - dy * h / l, p0[1] + dy * a / l + dx * h / l];
|
||||
for (name, at) in [("Ground", [0.0, 0.0]), ("Crank", p0), ("Coupler", q0), ("Rocker", o4)] {
|
||||
s.execute("component.activate", &json!({"component": "root"})).unwrap();
|
||||
s.execute("component.create", &json!({ "name": name })).unwrap();
|
||||
s.execute("solid.box", &json!({"length": 2, "width": 2, "height": 2, "corner": [at[0], at[1], -5]})).unwrap();
|
||||
}
|
||||
s.execute("component.activate", &json!({"component": "root"})).unwrap();
|
||||
s.execute("occurrence.ground", &json!({"occurrence": "Ground:1", "grounded": true})).unwrap();
|
||||
for (name, a, b, p) in [
|
||||
("Crank", "Ground", "Crank", [0.0, 0.0]),
|
||||
("Crank pin", "Crank", "Coupler", p0),
|
||||
("Rocker pin", "Coupler", "Rocker", q0),
|
||||
("Rocker", "Ground", "Rocker", o4),
|
||||
] {
|
||||
let at = |o: &str| json!({"occurrence": o, "point": [p[0], p[1], 0], "z": [0, 0, 1]});
|
||||
s.execute("joint.create", &json!({"type": "revolute", "a": at(a), "b": at(b), "flip": true, "name": name})).unwrap();
|
||||
}
|
||||
SolveApp::new(s, Services::default())
|
||||
}
|
||||
|
||||
/// Drive Joints says where a drive stopped short and why: the preview names the loop that can't
|
||||
/// close further, OK says it on the status line, and a drive within reach says nothing.
|
||||
#[test]
|
||||
fn drive_says_where_a_loop_stopped_it() {
|
||||
let mut app = four_bar();
|
||||
let colors = crate::preview::Colors { body: [1, 1, 1, 255], added: [2, 2, 2, 255], removed: [3, 3, 3, 100], edge: [0, 0, 0, 255] };
|
||||
let rocker = app.session.doc.assembly.joints.iter().position(|j| j.name == "Rocker").unwrap();
|
||||
let mut d = drive_joint(&app, app.session.doc.assembly.joints[rocker].id).unwrap();
|
||||
let ask = |d: &mut Dialog, v: &str| {
|
||||
if let Kind::Assembly(Asm::Drive { values, .. }) = &mut d.kind {
|
||||
values[0] = v.into();
|
||||
}
|
||||
};
|
||||
// Within its swing: no stop.
|
||||
ask(&mut d, "20 deg");
|
||||
let b = world_preview(&app.session, &apply_commands(&app, &d).unwrap(), colors).unwrap().unwrap();
|
||||
assert!(b.stopped.is_none());
|
||||
// Past its toggle position (about +50.4° from where it stands).
|
||||
ask(&mut d, "100 deg");
|
||||
let b = world_preview(&app.session, &apply_commands(&app, &d).unwrap(), colors).unwrap().unwrap();
|
||||
let why = b.stopped.as_ref().and_then(|r| drive_stop(&app.session, r)).unwrap();
|
||||
assert!(why.starts_with("Stopped at 50.") && why.contains("can't close any further"), "{why}");
|
||||
// OK drives it there and says so.
|
||||
crate::dialogs::run_dialog(&mut app, &d).unwrap();
|
||||
let (status, _, error) = app.status.clone().unwrap();
|
||||
assert!(status == why && !error, "{status}");
|
||||
}
|
||||
|
||||
/// A contact set stops a drive too: the message names the parts that would pass through each other.
|
||||
#[test]
|
||||
fn drive_says_which_parts_a_contact_set_stopped() {
|
||||
let mut app = two_boxes();
|
||||
let at = |o: &str| json!({"occurrence": o, "point": [30, 10, 5], "z": [1, 0, 0]});
|
||||
app.run("joint.create", json!({"type": "slider", "a": at("A"), "b": at("B"), "flip": true, "name": "Rail"})).unwrap();
|
||||
app.run("contact.create", json!({"occurrences": ["A:1", "B:1"]})).unwrap();
|
||||
let r = app.run("joint.drive", json!({"joint": "Rail", "value": -20})).unwrap();
|
||||
let why = drive_stop(&app.session, &r).unwrap();
|
||||
assert!(why.starts_with("Stopped at -1") && why.contains("A:1") && why.contains("B:1") && why.contains("would pass through"), "{why}");
|
||||
}
|
||||
@@ -53,6 +53,9 @@ pub struct Built {
|
||||
pub applied: Option<(f64, f64)>,
|
||||
/// The radius spline of each edge a fillet tapered (`fit: taper`), with a point on the edge.
|
||||
pub tapered: Vec<(Vec3, solidcraft_engine::kernel::Taper)>,
|
||||
/// The reply of a previewed drive that stopped short of the asked values (the dialog says
|
||||
/// where and why, in the UI's language).
|
||||
pub stopped: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
type Job = Result<Built, String>;
|
||||
@@ -90,6 +93,8 @@ pub struct PreviewState {
|
||||
started: f64,
|
||||
/// The smallest and largest radius the previewed fillet used when it reduced the one asked for.
|
||||
pub applied: Option<(f64, f64)>,
|
||||
/// The reply of a previewed drive that stopped short (see [`Built::stopped`]).
|
||||
pub stopped: Option<serde_json::Value>,
|
||||
/// The radius splines the previewed fillet tapered its edges with.
|
||||
pub tapered: Vec<(Vec3, solidcraft_engine::kernel::Taper)>,
|
||||
}
|
||||
@@ -101,6 +106,7 @@ impl PreviewState {
|
||||
self.shown_key = None;
|
||||
self.error = None;
|
||||
self.applied = None;
|
||||
self.stopped = None;
|
||||
self.tapered.clear();
|
||||
self.sketches.clear();
|
||||
self.busy = false;
|
||||
@@ -130,6 +136,7 @@ impl PreviewState {
|
||||
self.active = true;
|
||||
self.error = b.error;
|
||||
self.applied = b.applied;
|
||||
self.stopped = b.stopped;
|
||||
self.tapered = b.tapered;
|
||||
self.set_scene(b.scene);
|
||||
}
|
||||
@@ -137,6 +144,7 @@ impl PreviewState {
|
||||
self.sketches.clear();
|
||||
self.error = Some(e);
|
||||
self.applied = None;
|
||||
self.stopped = None;
|
||||
self.tapered.clear();
|
||||
}
|
||||
}
|
||||
@@ -338,6 +346,7 @@ fn compute_staged(s: &Session, cmds: &[(String, Value)], colors: Colors, partial
|
||||
bounds: b.bounds,
|
||||
applied: None,
|
||||
tapered: Vec::new(),
|
||||
stopped: None,
|
||||
});
|
||||
b.error = s.preview(cmds).err().map(|e| e.to_string());
|
||||
Ok(b)
|
||||
@@ -387,7 +396,16 @@ fn build_tool(before: &ModelState, after: &ModelState, c: Colors, op: &str) -> B
|
||||
}
|
||||
}
|
||||
}
|
||||
Built { scene: sc, sketches: Vec::new(), replaced: Vec::new(), error: None, bounds: Aabb3::EMPTY, applied: None, tapered: Vec::new() }
|
||||
Built {
|
||||
scene: sc,
|
||||
sketches: Vec::new(),
|
||||
replaced: Vec::new(),
|
||||
error: None,
|
||||
bounds: Aabb3::EMPTY,
|
||||
applied: None,
|
||||
tapered: Vec::new(),
|
||||
stopped: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The same commands making a new body instead of joining, cutting or intersecting, and the
|
||||
@@ -524,7 +542,7 @@ pub fn build(before: &ModelState, after: &ModelState, c: Colors) -> Built {
|
||||
if indices.is_empty() && points.is_empty() { None } else { Some(SketchPreview { sketch: sketch.clone(), curves: indices, points }) }
|
||||
})
|
||||
.collect();
|
||||
Built { scene: sc, sketches, replaced, error: None, bounds: Aabb3::EMPTY, applied: None, tapered: Vec::new() }
|
||||
Built { scene: sc, sketches, replaced, error: None, bounds: Aabb3::EMPTY, applied: None, tapered: Vec::new(), stopped: None }
|
||||
}
|
||||
|
||||
/// Which of the `nf` faces of `m` are new surface, i.e. not on the old mesh? Each is decided by
|
||||
@@ -719,6 +737,7 @@ mod tests {
|
||||
bounds: Aabb3::EMPTY,
|
||||
applied: None,
|
||||
tapered: Vec::new(),
|
||||
stopped: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,10 @@ 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.
|
||||
under `conflicts` and nothing in it moves. 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.
|
||||
|
||||
**Insert Design** (`component.insert_design {path, translate?}`) adds a component linked to
|
||||
another saved design, so each part can live in its own document. The part's solid bodies come
|
||||
|
||||
@@ -16,6 +16,12 @@ to leave the bodies just short of penetration, within the tessellated contact to
|
||||
Motion links and limits participate in the trajectory checks. Moving a body between
|
||||
components preserves its placement for contact checks through its body offset.
|
||||
|
||||
A drive can also stop where a closed loop of joints (a four-bar, a slider-crank) can no longer
|
||||
close; it then returns `stopped_by_loop` instead, with the joint whose loop opens and the
|
||||
fraction reached. When both apply, the motion stops at whichever comes first, and only that
|
||||
one is reported. The Drive Joints dialog shows the reached value and the reason while it
|
||||
previews, and the status line repeats it after OK.
|
||||
|
||||
The trajectory uses bounded samples of cached tessellated geometry. Spacing accounts for
|
||||
effective joint travel, angular motion, distance from joint pivots and whole-body widths
|
||||
along sampled face directions. This handles tilted thin plates while keeping curved bodies
|
||||
|
||||
Reference in new issue
Block a user