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:
1 parent
3b9a975299
commit
8601611d0c
7 files changed
+277
-33
No files matched your search
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
Reference in new issue
Block a user