diff --git a/crates/engine/src/cmd/contact_sweep.rs b/crates/engine/src/cmd/contact_sweep.rs index 887a728b..3ab185d8 100644 --- a/crates/engine/src/cmd/contact_sweep.rs +++ b/crates/engine/src/cmd/contact_sweep.rs @@ -228,10 +228,11 @@ impl ContactGeometry { Ok(self.hits(s, work, MAX_WORK, false)?.into_iter().next().map(|(a, b, depth)| (a.1, b.1, depth))) } - /// The pairs that pass through each other in `s`, with how deep (every pair, or the first - /// found when `all` is false). `work` adds up the cost, refused past `budget`. + /// The pairs of set members whose bodies pass through each other in `s`, with how deep the + /// first two bodies found go (every pair, or the first found when `all` is false). `work` + /// adds up the cost, refused past `budget`. pub(super) fn hits(&self, s: &Session, work: &mut usize, budget: usize, all: bool) -> Result> { - let mut out = Vec::new(); + let mut out: Vec<(Member, Member, f64)> = Vec::new(); let meshes: Vec = self .bodies .iter() @@ -242,6 +243,10 @@ impl ContactGeometry { }) .collect::>()?; for (a, b, oa, ob) in &self.pairs { + // Members already found colliding need no more of their bodies tested. + if out.iter().any(|(x, y, _)| (x.0, y.0) == (oa.0, ob.0) || (x.0, y.0) == (ob.0, oa.0)) { + continue; + } let (Some(ma), Some(mb), Some(ba), Some(bb)) = (meshes.get(*a), meshes.get(*b), self.bodies.get(*a), self.bodies.get(*b)) else { return Err(limit("invalid contact geometry pair")); }; diff --git a/crates/engine/src/cmd/motion_analysis.rs b/crates/engine/src/cmd/motion_analysis.rs index e9412361..99e32aa3 100644 --- a/crates/engine/src/cmd/motion_analysis.rs +++ b/crates/engine/src/cmd/motion_analysis.rs @@ -31,7 +31,8 @@ pub const INTERFERENCE_DOC: &str = "study: id|name; occurrences?: [ids or names] default every occurrence), with against?: [ids or names] (then each of `occurrences` is checked only against these, such \ as an arm against the parts it must miss), or contact_sets?: true (the pairs of the design's contact sets); samples? \ (2…1000, default the study's steps + 1) → collisions: [{a, b, first_step, last_step, steps: [[from, to]…] (the step ranges where they overlap), \ - depth_mm (the deepest), deepest_step, volume_mm3 (their overlap there; null when it could not be worked out)}], clear, \ + depth_mm (how far into each other, the most at deepest_step), volume_mm3 (their overlap there; null when it was not \ + cheap to work out)}], clear, \ pairs (checked), samples (taken), truncated (the work budget ran out; checked_to: the last step checked), stopped_at, \ stopped_by_loop. The study plays as motion.play plays it but contact sets do not stop it, on a copy (the design is not \ changed). Parts are tested at the samples only: one that passes wholly through another between two samples is missed"; diff --git a/crates/ui-egui/src/dialogs_motion_plot.rs b/crates/ui-egui/src/dialogs_motion_plot.rs index ea1960f7..2b9b76ec 100644 --- a/crates/ui-egui/src/dialogs_motion_plot.rs +++ b/crates/ui-egui/src/dialogs_motion_plot.rs @@ -163,6 +163,9 @@ fn chart(ui: &mut egui::Ui, xs: &[f64], ys: &[f64], unit: &str, total: u32, now: p.text(r.right_top() + vec2(-4.0, 2.0), Align2::RIGHT_TOP, format!("{} {unit}", short(v)), font, t.text); } } + // Where steps 0 and `total` fall, for a scenario or an agent to click between. + crate::scenario::publish_handle("motion:chart_start", pos2(x(0.0), plot.center().y)); + crate::scenario::publish_handle("motion:chart_end", pos2(x(total), plot.center().y)); let cx = x(now); p.line_segment([pos2(cx, r.top()), pos2(cx, r.bottom())], Stroke::new(1.5, t.warning)); let pos = resp.interact_pointer_pos().filter(|_| resp.clicked() || resp.dragged())?; @@ -238,7 +241,8 @@ pub fn rows( combo(ui, "ms_quantity", &QUANTITIES, &mut a.quantity); ui.end_row(); row_label(ui, "Duration"); - ui.add(egui::TextEdit::singleline(&mut a.duration).hint_text(crate::i18n::tr("motion.duration_hint"))); + let r = ui.add(egui::TextEdit::singleline(&mut a.duration).hint_text(crate::i18n::tr("motion.duration_hint"))); + crate::scenario::publish_handle("dialog:Duration", r.rect.center()); ui.end_row(); let params = study.clone().and_then(|st| plot_params(s, what, &st, &a.duration, inputs).map(|p| (st, p))); let key = key_of(¶ms); @@ -295,7 +299,9 @@ pub fn rows( if sets && parts.is_empty() { combo(ui, "ms_among", &["All components", "Contact sets"], &mut a.among); } - if ui.add_enabled(check.is_some(), egui::Button::new(crate::i18n::tr_label("Check collisions"))).clicked() + let button = ui.add_enabled(check.is_some(), egui::Button::new(crate::i18n::tr_label("Check collisions"))); + crate::scenario::publish_handle("motion:check", button.rect.center()); + if button.clicked() && let Some((st, mut p)) = check { let mut sc = app.session.scratch(); @@ -313,7 +319,7 @@ pub fn rows( if hits.is_empty() { ui.label(RichText::new(crate::i18n::tr("motion.no_collisions")).color(t.text_dim)); } - for c in &hits { + for (i, c) in hits.iter().enumerate() { let steps: Vec = c["steps"] .as_array() .into_iter() @@ -326,7 +332,9 @@ pub fn rows( let name = |k: &str| c[k].as_str().unwrap_or_default().to_string(); let text = crate::i18n::format("motion.collision", &[("a", &name("a")), ("b", &name("b")), ("steps", &steps.join(", "))]); // A click shows where they first collide. - if ui.link(RichText::new(text).color(t.error)).clicked() + let link = ui.link(RichText::new(text).color(t.error)); + crate::scenario::publish_handle(&format!("motion:collision:{i}"), link.rect.center()); + if link.clicked() && let Some(first) = c["first_step"].as_f64() { (*step, *playing) = (first, false); diff --git a/docs/book/src/guide/assemblies.md b/docs/book/src/guide/assemblies.md index 2b928534..f686e2f8 100644 --- a/docs/book/src/guide/assemblies.md +++ b/docs/book/src/guide/assemblies.md @@ -189,9 +189,10 @@ so they are as smooth as the samples are dense (up to 1000, one per step by defa unwrapped, so a joint that turns on past half a turn keeps counting. `motion.interference {study, occurrences?, against?, contact_sets?, samples?}` answers each colliding pair with its `first_step`, `last_step`, the `steps` ranges where they overlap, how -deep (`depth_mm`, the deepest at `deepest_step`) and their shared `volume_mm3` there. It plays -the study as `motion.play` does, but contact sets don't stop it, and parts are tested at the -samples only: a part that passes right through another between two samples is missed. A check +far into each other (`depth_mm`, the most at `deepest_step`) and their shared `volume_mm3` +there (null when it is not cheap to work out). It plays the study as `motion.play` does, but +contact sets don't stop it, and parts are tested at the samples only: a part that passes right +through another between two samples is missed. A check takes at most 256 bodies; on a large design, list the `occurrences` to check (an arm, with `against` the parts it must miss). When the work runs out it says `truncated` and the step it `checked_to`. diff --git a/docs/control-protocol.md b/docs/control-protocol.md index cc85bf88..13236705 100644 --- a/docs/control-protocol.md +++ b/docs/control-protocol.md @@ -139,7 +139,7 @@ labels; `prefs:measure_axis_distances` identifies the checkbox. | `ui.sketchToScreen` | `points`: `[[x, y]…]` in active-sketch coordinates | their screen points (null when behind the camera) | | `ui.dialogInput` | `index` | what clicking an input's Select chip does: picks go to that input | | `ui.at` | `world: [x,y,z]` \| `sketch: [x,y]` \| `plane: "XY"` \| `axis: "Z"` \| `dimension: "d1"` (its value text) \| `handle: "arrow"` (a manipulator handle, see `scenario::publish_handle`) | the screen point (a scenario step's target; see `crates/ui-egui/src/scenario.rs`) | -| `ui.handles` | `prefix?` | the handles drawn last frame, `[{name, x, y}]`: toolbar buttons and drop-down items `toolbar:` (the button alone: `button:`; a drop-down row: `dropdown:`, highlighted `dropdown_hot:`, its pin `dropdown_pin:`; Customize Toolbar's check boxes `customize:`), panel labels `panel::`, browser rows `row: