engine: pick-driven points, cells' methods per cell, rigid walls as interactions

- geometry.bodies lists each body's vertices; geometry.datum.point by
  vertex takes the design's vertex names (labelled Vertex, not Mesh
  vertex) (#346).
- geometry.centre: the point for what is picked: a vertex, nodes, a
  hole's centre (cylindrical faces or circular edges), else a centroid
  (#351).
- mesh.cells' form: the cells first, and a method drop-down per cell
  (schema kind RefChoices) instead of JSON (#344).
- interaction.rigid_wall(.delete): rigid walls of an explicit step from
  the Interactions tab, no longer coming soon (#350).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
jyvikoandClaude Opus 5.5 authored and Irakli Kourtis committed 2026-10-15 09:27:39 -04:00
1 parent e8e0b92d87
commit 1372094f8a
10 files changed
+564 -27

No files matched your search

+13
View File
@@ -148,6 +148,19 @@ fn scalar(f: &Field, v: &Value, units: UnitSystem) -> String {
Kind::Vector { labels, .. } => vector(labels, v, unit.as_deref()),
Kind::Ref(_) | Kind::Text | Kind::Path { .. } => v.as_str().map_or_else(|| crate::context::value_text(v), str::to_string),
Kind::RefList(_) => list(v, |x| x.as_str().map(str::to_string)),
// `Bracket.3: Tet, Bracket.7: Swept`.
Kind::RefChoices(_, options) => v.as_object().map_or_else(
|| crate::context::value_text(v),
|o| {
o.iter()
.map(|(k, x)| {
let s = x.as_str().unwrap_or_default();
format!("{k}: {}", options.iter().find(|(o, _)| *o == s).map_or(s, |(_, l)| *l))
})
.collect::<Vec<_>>()
.join(", ")
},
),
Kind::Either(options) => match v {
Value::String(s) => options.iter().find(|o| o.key == s).map_or_else(|| s.clone(), |o| o.label.to_string()),
Value::Object(o) => o
+62 -3
View File
@@ -50,6 +50,8 @@ pub struct Outcome {
pub create: Option<(Collection, String)>,
/// The paths of the fields edited this frame.
pub touched: Vec<String>,
/// The body whose row in a list of bodies is under the pointer (the viewport highlights it).
pub hover_body: Option<String>,
}
struct Form<'a> {
@@ -298,6 +300,7 @@ impl Form<'_> {
Kind::Vector { labels, nullable, .. } => self.vector(ui, id, value, f, labels, nullable),
Kind::Ref(c) => self.reference(ui, id, path, value, c, f.hint, f.required),
Kind::RefList(c) => self.reference_list(ui, path, value, c),
Kind::RefChoices(c, options) => self.reference_choices(ui, id, value, c, options),
Kind::Either(options) => {
let key = match value {
Some(Value::String(s)) => Some(s.as_str()),
@@ -431,18 +434,74 @@ impl Form<'_> {
let chosen: Vec<String> =
value.and_then(Value::as_array).map(|a| a.iter().filter_map(Value::as_str).map(str::to_string).collect()).unwrap_or_default();
let names = self.names(c).to_vec();
let shown = names.get(..names.len().min(MAX_ROWS)).unwrap_or_default();
let mut changed = None;
let list = |picked: Vec<String>| if picked.is_empty() { Value::Null } else { json!(picked) };
ui.vertical(|ui| {
if names.is_empty() {
ui.label(RichText::new(format!("No {} yet", plural(c.label()))).color(self.style.dim));
self.create_button(ui, c, path);
}
for n in names.iter().take(MAX_ROWS) {
// All, none and the others, for long lists.
if shown.len() > 3 {
ui.horizontal(|ui| {
if ui.small_button("All").clicked() {
changed = Some(list(shown.to_vec()));
}
if ui.small_button("None").clicked() {
changed = Some(Value::Null);
}
if ui.small_button("Invert").on_hover_text("Tick the unticked ones and untick the others").clicked() {
changed = Some(list(shown.iter().filter(|n| !chosen.contains(n)).cloned().collect()));
}
});
}
for n in shown {
let mut on = chosen.contains(n);
if ui.checkbox(&mut on, n).changed() {
let r = ui.checkbox(&mut on, n);
if r.hovered() && c == Collection::Body {
self.out.hover_body = Some(n.clone());
}
if r.changed() {
let mut next: Vec<String> = names.iter().filter(|m| if *m == n { on } else { chosen.contains(m) }).cloned().collect();
next.dedup();
changed = Some(if next.is_empty() { Value::Null } else { json!(next) });
changed = Some(list(next));
}
}
});
changed
}
/// A drop-down per item of `c` (its row hovered highlights a body), "(default)" leaving it
/// out of the object.
fn reference_choices(&mut self, ui: &mut Ui, id: Id, value: Option<&Value>, c: Collection, options: &[(&str, &str)]) -> Option<Value> {
let current = value.and_then(Value::as_object).cloned().unwrap_or_default();
let names = self.names(c).to_vec();
let mut labels: Vec<&str> = options.iter().map(|(_, l)| *l).collect();
labels.push("(default)");
let mut changed = None;
ui.vertical(|ui| {
if names.is_empty() {
ui.label(RichText::new(format!("No {} yet", plural(c.label()))).color(self.style.dim));
}
for (i, n) in names.iter().take(MAX_ROWS).enumerate() {
let chosen = current.get(n).and_then(Value::as_str);
let selected = options.iter().find(|(v, _)| Some(*v) == chosen).map_or("(default)", |(_, l)| *l);
let row = ui.horizontal(|ui| {
let width = (ui.available_width() * 0.5).max(FIELD_MIN);
ui.add_sized([width, ui.spacing().interact_size.y], egui::Label::new(n.as_str()).truncate());
fields::combo(ui, id.with(i), selected, &labels, fill(ui))
});
if row.response.contains_pointer() && c == Collection::Body {
self.out.hover_body = Some(n.clone());
}
if let Some(k) = row.inner {
let mut next = current.clone();
match options.get(k) {
Some((v, _)) => next.insert(n.clone(), json!(v)),
None => next.remove(n),
};
changed = Some(if next.is_empty() { Value::Null } else { Value::Object(next) });
}
}
});