MCP demo v3 (enclosure_ui_mcp.py --story v3, default): one standoff placed from the parameters (width/2 - inset, depth/2 - inset) and patterned 2 x 2 in the Rectangular Pattern dialog with spacings width - 2*inset and depth - 2*inset, then width 80 -> 100 and wall 2 -> 3 typed into the Parameters dialog; record_v2.sh is now record_ui.sh STORY. A pattern's Direction can be a straight model edge (an edge along an origin axis points the axis's way), scenario pattern_direction_edge. Dialog rows publish their widgets as dialog:<label> handles (combo items dialog:<label>:<item>, OK), the Parameters dialog its expressions (param:<name>), title and close button

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
bherbruckandClaude Opus 5.5 committed 2026-10-09 15:09:29 +00:00
1 parent 3b9a975299
commit 8601611d0c
7 files changed
+277 -33

No files matched your search

+50 -8
View File
@@ -363,7 +363,11 @@ impl Dialog {
}
"solid.pattern.rectangular" => Dialog::new(
Kind::PatternRect { count: "3".into(), spacing: "20 mm".into(), count2: "1".into(), spacing2: "20 mm".into() },
vec![SelInput::new("Objects", BODIES, true), SelInput::new("Direction", AXES, false), SelInput::new("Direction 2", AXES, false)],
vec![
SelInput::new("Objects", BODIES, true),
SelInput::new("Direction", AXES | EDGES, false),
SelInput::new("Direction 2", AXES | EDGES, false),
],
),
"solid.pattern.circular" => Dialog::new(
Kind::PatternCirc { count: "6".into(), angle: "360 deg".into() },
@@ -713,16 +717,30 @@ fn plane_value(s: &Session, sel: Option<&Sel>) -> Option<Value> {
/// A value field; true when Enter was pressed in it.
pub(crate) fn field(ui: &mut egui::Ui, s: &mut String) -> bool {
let r = ui.text_edit_singleline(s);
publish_row(&r);
crate::params_dialog::complete(ui, &r, s);
r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter))
}
pub(crate) fn combo(ui: &mut egui::Ui, id: &str, labels: &[&str], sel: &mut usize) {
egui::ComboBox::from_id_salt(id).selected_text(labels.get(*sel).copied().unwrap_or("")).width(FIELD_W).show_ui(ui, |ui| {
let row = ROW.with(|r| r.borrow().clone());
let r = egui::ComboBox::from_id_salt(id).selected_text(labels.get(*sel).copied().unwrap_or("")).width(FIELD_W).show_ui(ui, |ui| {
for (i, l) in labels.iter().enumerate() {
ui.selectable_value(sel, i, *l);
let item = ui.selectable_value(sel, i, *l);
crate::scenario::publish_handle(&format!("dialog:{row}:{l}"), item.rect.center());
}
});
publish_row(&r.response);
}
thread_local! {
/// The label of the dialog row being laid out (its widgets publish `dialog:<label>` handles).
static ROW: std::cell::RefCell<String> = const { std::cell::RefCell::new(String::new()) };
}
/// A dialog row's widget, for scenarios and demos: the handle `dialog:<row label>`.
fn publish_row(r: &egui::Response) {
ROW.with(|row| crate::scenario::publish_handle(&format!("dialog:{}", row.borrow()), r.rect.center()));
}
fn title(k: &Kind) -> &'static str {
@@ -805,6 +823,7 @@ const LABEL_W: f32 = 92.0;
/// A row label in the dialog's label column.
pub(crate) fn row_label(ui: &mut egui::Ui, text: &str) {
ROW.with(|r| *r.borrow_mut() = text.to_string());
ui.allocate_ui_with_layout(vec2(LABEL_W, 22.0), egui::Layout::left_to_right(egui::Align::Center), |ui| {
ui.set_width(LABEL_W);
ui.add(egui::Label::new(text).truncate()).on_hover_text(text);
@@ -926,7 +945,11 @@ fn placement_row(d: &mut Dialog, ui: &mut egui::Ui) {
let (kind, mut inputs) = match ty {
0 => (
Kind::PatternRect { count: "3".into(), spacing: "20 mm".into(), count2: "1".into(), spacing2: "20 mm".into() },
vec![SelInput::new("Objects", BODIES, true), SelInput::new("Direction", AXES, false), SelInput::new("Direction 2", AXES, false)],
vec![
SelInput::new("Objects", BODIES, true),
SelInput::new("Direction", AXES | EDGES, false),
SelInput::new("Direction 2", AXES | EDGES, false),
],
),
1 => (
Kind::PatternCirc { count: "6".into(), angle: "360 deg".into() },
@@ -988,7 +1011,9 @@ fn input_rows(d: &mut Dialog, ui: &mut egui::Ui) {
.stroke(stroke)
.min_size(vec2(0.0, 22.0))
.sense(egui::Sense::CLICK);
if ui.add(b).on_hover_text(format!("{} (click to pick into this input)", hint(inp))).clicked() {
let chip = ui.add(b);
publish_row(&chip);
if chip.on_hover_text(format!("{} (click to pick into this input)", hint(inp))).clicked() {
activate = Some(i);
}
if !inp.items.is_empty() && ui.small_button("×").on_hover_text("Clear the selection").clicked() {
@@ -1571,7 +1596,9 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
let b = egui::Button::new(RichText::new(label).color(if valid { Color32::WHITE } else { t.text_dim }))
.fill(if valid { t.accent } else { Color32::TRANSPARENT })
.min_size(vec2(64.0, 24.0));
if ui.add_enabled(valid, b).clicked() {
let r = ui.add_enabled(valid, b);
crate::scenario::publish_handle(&format!("dialog:{label}"), r.rect.center());
if r.clicked() {
ok = true;
}
}
@@ -1898,9 +1925,9 @@ fn dialog_commands(app: &SolveApp, d: &Dialog) -> Result<Vec<(String, Value)>, S
need(0, "objects")?;
let kept = d.extra.get("dir1").and_then(|v| serde_json::from_value::<[f64; 3]>(v.clone()).ok()).map(|a| Vec3::new(a[0], a[1], a[2]));
let d1 = match (sels(d, 1).first(), kept) {
(Some(x), _) => axis_of(app, x).ok_or("the direction must be an axis or a sketch line")?.1,
(Some(x), _) => axis_of(app, x).ok_or("the direction must be an axis, a sketch line or a straight edge")?.1,
(None, Some(k)) => k,
(None, None) => return Err("select a direction (an axis or a sketch line) first".into()),
(None, None) => return Err("select a direction (an axis, a sketch line or a straight edge) first".into()),
};
let mut p = json!({"features": pattern_features(s, sels(d, 0)), "dir1": pt(d1), "count1": count, "spacing1": spacing});
if let Some((_, d2)) = sels(d, 2).first().and_then(|x| axis_of(app, x)) {
@@ -2276,6 +2303,21 @@ pub fn axis_of(app: &SolveApp, sel: &Sel) -> Option<(Vec3, Vec3)> {
let (pa, pb) = (ss.plane.to_world(ss.sketch.point(a)?), ss.plane.to_world(ss.sketch.point(b)?));
Some((pa, (pb - pa).normalized()?))
}
// A straight model edge, along it.
Sel::Edge { body, index, .. } => {
let b = app.session.world_state().body(body).cloned()?;
let m = b.mesh();
let e = m.edges.get(*index)?;
let (a, z) = (*e.first()?, *e.last()?);
let dir = (z - a).normalized()?;
let tol = 1e-6 * a.dist(z).max(1.0);
if !e.iter().all(|p| (*p - a).cross(dir).len() < tol) {
return None;
}
// An edge along an origin axis points the way the axis does (as picking the axis).
let axis = [Vec3::X, Vec3::Y, Vec3::Z].into_iter().find(|x| dir.dot(*x).abs() > 1.0 - 1e-9);
Some((a, axis.unwrap_or(dir)))
}
_ => None,
}
}
+6
View File
@@ -188,6 +188,11 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
}
});
});
// The title bar's close button, for scenarios and demos.
if let Some(r) = ctx.memory(|m| m.area_rect(egui::Id::new("sc_params_dialog"))) {
crate::scenario::publish_handle("params:close", egui::pos2(r.right() - 16.0, r.top() + 15.0));
crate::scenario::publish_handle("params:title", egui::pos2(r.center().x, r.top() + 15.0));
}
if !open {
app.params = None;
return;
@@ -271,6 +276,7 @@ fn table(app: &mut SolveApp, ui: &mut egui::Ui, d: &mut ParamsDialog, rows: &[&R
te = te.text_color(t.error);
}
let resp = ui.add_sized([170.0, 20.0], te);
crate::scenario::publish_handle(&format!("param:{}", r.name), resp.rect.center());
let completed = complete(ui, &resp, &mut e);
if resp.changed() || completed {
d.editing.insert(r.name.clone(), e.clone());
@@ -0,0 +1,79 @@
[
{
"note": "A Rectangular Pattern's direction can be a straight model edge (as in Fusion), not only an origin axis or a sketch line."
},
{
"call": "engine.execute",
"params": {
"command": "solid.box",
"params": {
"length": 20,
"width": 10,
"height": 10
}
}
},
{
"call": "ui.view",
"params": {
"view": "home"
}
},
{
"start": "solid.pattern.rectangular"
},
{
"click": {
"world": [
10,
5,
10
]
}
},
{
"click": {
"handle": "dialog:Direction"
}
},
{
"click": {
"world": [
10,
0,
10
]
}
},
{
"click": {
"handle": "dialog:OK"
}
},
{
"expect": {
"dialog": null,
"bodies": 3
}
},
{
"note": "Along the picked edge (an edge along X points +X, as the axis would): copies 20 mm apart."
},
{
"expect": {
"bbox": {
"body": "Body3",
"min": [
40,
0,
0
],
"max": [
60,
10,
10
]
}
}
}
]