sketch(feat): add dimension references and parameter tools (#1548)
Show each dimension's parameter name while creating or editing it, and let users insert another driving dimension at the expression cursor or selection. Use the available editor width and keep Apply aligned with the right edge. Add a tools dropdown with Measure, named user parameter expressions, and access to the existing parameter table for adding and editing parameters. Preserve the unfinished dimension expression while either window is open. Cover reference insertion, dependent updates, parameter selection, dialog round trips, applying and cancellation with UI integration tests. Update the parameter guide. Signed-off-by: Karl Von Richter<[email protected]>
This commit is contained in:
1 parent
a11446fd17
commit
11a196e6f8
4 files changed
+128
-6
No files matched your search
@@ -49,6 +49,7 @@ struct Edit {
|
||||
/// Type-first creation: the value took the keyboard as soon as the picks determined the
|
||||
/// dimension, and its text follows the pointer until a click pins it or Enter places it.
|
||||
live: Option<Live>,
|
||||
selection: Option<egui::text::CCursorRange>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
@@ -132,7 +133,9 @@ pub fn begin_dimension(app: &mut SolveApp, entities: Vec<String>, at: Vec2) -> R
|
||||
app.set_status("The sketch already determines that dimension, so it was added as a driven (reference) dimension", false);
|
||||
return Ok(());
|
||||
}
|
||||
EDIT.with(|edit| *edit.borrow_mut() = Some(Edit { param, expr, at: None, fresh: FRESH, wait: 10, draft: Some(draft), live: None }));
|
||||
EDIT.with(|edit| {
|
||||
*edit.borrow_mut() = Some(Edit { param, expr, at: None, fresh: FRESH, wait: 10, draft: Some(draft), live: None, selection: None })
|
||||
});
|
||||
PLACEMENT.with(|preview| *preview.borrow_mut() = None);
|
||||
Ok(())
|
||||
}
|
||||
@@ -147,7 +150,7 @@ pub fn begin_live(app: &mut SolveApp, entities: Vec<String>, pointer: Option<Pos
|
||||
return Ok(false);
|
||||
}
|
||||
let live = Some(Live { origin: pointer, moved: false, typed: false });
|
||||
EDIT.with(|edit| *edit.borrow_mut() = Some(Edit { param, expr, at: None, fresh: FRESH, wait: 10, draft: Some(draft), live }));
|
||||
EDIT.with(|edit| *edit.borrow_mut() = Some(Edit { param, expr, at: None, fresh: FRESH, wait: 10, draft: Some(draft), live, selection: None }));
|
||||
PLACEMENT.with(|preview| *preview.borrow_mut() = None);
|
||||
Ok(true)
|
||||
}
|
||||
@@ -351,7 +354,9 @@ pub fn double_click(app: &mut SolveApp, pos: Pos2) -> bool {
|
||||
return true;
|
||||
};
|
||||
let expr = app.session.doc.param(¶m).map(|p| p.expr.clone()).unwrap_or_default();
|
||||
EDIT.with(|e| *e.borrow_mut() = Some(Edit { param, expr, at: Some(d.text.center()), fresh: FRESH, wait: 0, draft: None, live: None }));
|
||||
EDIT.with(|e| {
|
||||
*e.borrow_mut() = Some(Edit { param, expr, at: Some(d.text.center()), fresh: FRESH, wait: 0, draft: None, live: None, selection: None })
|
||||
});
|
||||
true
|
||||
}
|
||||
|
||||
@@ -577,7 +582,52 @@ fn pos2_mid_top(r: Rect) -> Pos2 {
|
||||
/// its value is selected, so typing replaces it).
|
||||
pub fn edit_param(app: &SolveApp, param: &str) {
|
||||
let expr = app.session.doc.param(param).map(|p| p.expr.clone()).unwrap_or_default();
|
||||
EDIT.with(|e| *e.borrow_mut() = Some(Edit { param: param.to_string(), expr, at: None, fresh: FRESH, wait: 10, draft: None, live: None }));
|
||||
EDIT.with(|e| {
|
||||
*e.borrow_mut() = Some(Edit { param: param.to_string(), expr, at: None, fresh: FRESH, wait: 10, draft: None, live: None, selection: None })
|
||||
});
|
||||
}
|
||||
|
||||
/// Insert a reference at the expression's selection, using character offsets (not bytes).
|
||||
fn insert_reference(ctx: &egui::Context, edit: &mut Edit, name: &str) {
|
||||
let id = egui::Id::new("sc_dim_edit");
|
||||
let mut state = egui::TextEdit::load_state(ctx, id).unwrap_or_default();
|
||||
let len = edit.expr.chars().count();
|
||||
let (start, end) = edit
|
||||
.selection
|
||||
.or_else(|| state.cursor.char_range())
|
||||
.map(|r| {
|
||||
let a = r.primary.index.0.min(len);
|
||||
let b = r.secondary.index.0.min(len);
|
||||
(a.min(b), a.max(b))
|
||||
})
|
||||
.unwrap_or((len, len));
|
||||
let before: String = edit.expr.chars().take(start).collect();
|
||||
let after: String = edit.expr.chars().skip(end).collect();
|
||||
edit.expr = format!("{before}{name}{after}");
|
||||
state.cursor.set_char_range(Some(egui::text::CCursorRange::one(egui::text::CCursor::new(start + name.chars().count()))));
|
||||
edit.selection = state.cursor.char_range();
|
||||
state.store(ctx, id);
|
||||
ctx.memory_mut(|m| m.request_focus(id));
|
||||
edit.fresh = FRESH - 1;
|
||||
if let Some(live) = edit.live.as_mut() {
|
||||
live.typed = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Consume dimension clicks while editing; only a double-click inserts a reference.
|
||||
pub fn pick_reference(ctx: &egui::Context, pos: Pos2, double: bool) -> bool {
|
||||
EDIT.with(|editor| {
|
||||
let mut editor = editor.borrow_mut();
|
||||
let Some(edit) = editor.as_mut() else { return false };
|
||||
let Some(id) = hit(pos) else { return false };
|
||||
let param = DRAWN.with(|d| d.borrow().iter().find(|d| d.id == id).and_then(|d| d.param.clone()));
|
||||
if double && let Some(param) = param.filter(|p| p != &edit.param) {
|
||||
insert_reference(ctx, edit, ¶m);
|
||||
}
|
||||
// Self references and driven dimensions cannot supply a reference.
|
||||
edit.fresh = FRESH - 1;
|
||||
true
|
||||
})
|
||||
}
|
||||
|
||||
fn edit_box(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
@@ -612,8 +662,16 @@ fn edit_box(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
crate::frame::area(egui::Id::new("sc_dim_edit_area")).fixed_pos(anchor).order(egui::Order::Foreground).show(ctx, |ui| {
|
||||
egui::Frame::popup(ui.style()).inner_margin(egui::Margin::symmetric(3, 1)).show(ui, |ui| {
|
||||
ui.set_max_width(popup_width);
|
||||
let r = ui.add(egui::TextEdit::singleline(&mut e.expr).id(id).desired_width(popup_width.min(90.0)));
|
||||
ui.label(egui::RichText::new(format!("Dimension: {}", e.param)).strong())
|
||||
.on_hover_text("Double-click another driving dimension to insert its name");
|
||||
let r = ui.add(egui::TextEdit::singleline(&mut e.expr).id(id).desired_width(popup_width));
|
||||
if r.has_focus() {
|
||||
e.selection = egui::TextEdit::load_state(ctx, id).and_then(|s| s.cursor.char_range());
|
||||
}
|
||||
let completed = crate::params_dialog::complete(ui, &r, &mut e.expr);
|
||||
if completed {
|
||||
e.selection = egui::TextEdit::load_state(ctx, id).and_then(|s| s.cursor.char_range());
|
||||
}
|
||||
if let Some(live) = e.live.as_mut() {
|
||||
live.typed |= r.changed() || completed;
|
||||
}
|
||||
@@ -625,6 +683,7 @@ fn edit_box(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
let n = e.expr.chars().count();
|
||||
let mut s = egui::text_edit::TextEditState::load(ui.ctx(), id).unwrap_or_default();
|
||||
s.cursor.set_char_range(Some(egui::text_selection::CCursorRange::two(egui::text::CCursor::new(0), egui::text::CCursor::new(n))));
|
||||
e.selection = s.cursor.char_range();
|
||||
s.store(ui.ctx(), id);
|
||||
}
|
||||
let enter = ui.input(|i| i.key_pressed(egui::Key::Enter));
|
||||
|
||||
@@ -32,6 +32,57 @@ fn type_value(h: &mut Harness, text: &str) {
|
||||
h.step(&json!({"text":text})).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dimension_editor_names_and_reference_picks_drive_new_and_existing_dimensions() {
|
||||
let mut h = line([20., 0.]);
|
||||
let source =
|
||||
h.app.run("sketch.dimension", json!({"entities":["l1"],"value":20,"text_at":[10,8]})).unwrap()["param"].as_str().unwrap().to_string();
|
||||
h.app.run("sketch.line", json!({"points":[[0,-10],[12,-10]],"infer":false})).unwrap();
|
||||
h.settle();
|
||||
place(&mut h, &[[6., -10.]], [6., -16.]);
|
||||
let target = h.app.session.doc.next_d_name();
|
||||
assert!(h.widget(&format!("Dimension: {target}")).is_some(), "new dimension shows its actual parameter name");
|
||||
// Let the preceding placement clicks expire before starting a distinct double-click.
|
||||
h.frames(40);
|
||||
h.step(&json!({"click":{"dimension":source},"double":true})).unwrap();
|
||||
assert!(crate::dim_view::editing(), "reference click keeps the draft open");
|
||||
h.step(&json!({"text":" / 2"})).unwrap();
|
||||
h.step(&json!({"key":"Enter"})).unwrap();
|
||||
assert!(!crate::dim_view::editing(), "{}", h.call("ui.inspect", json!({})));
|
||||
assert_eq!(h.app.session.doc.param(&target).unwrap().expr, format!("{source} / 2"));
|
||||
h.app.tool = None;
|
||||
h.settle();
|
||||
crate::dim_view::edit_param(&h.app, &target);
|
||||
h.frames(5);
|
||||
assert!(h.widget(&format!("Dimension: {target}")).is_some());
|
||||
type_value(&mut h, "2 * ");
|
||||
h.frames(40);
|
||||
h.step(&json!({"click":{"dimension":source},"double":true})).unwrap();
|
||||
h.step(&json!({"key":"Enter"})).unwrap();
|
||||
assert_eq!(h.app.session.doc.param(&target).unwrap().expr, format!("2 * {source}"));
|
||||
h.app.run("parameters.change", json!({"name":source,"expression":"15 mm"})).unwrap();
|
||||
assert!((h.app.session.doc.eval(&target, solidcraft_engine::doc::expr::Kind::Length).unwrap() - 30.).abs() < 1e-6);
|
||||
crate::dim_view::cancel();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_clicking_another_dimension_does_not_change_the_expression() {
|
||||
let mut h = line([20., 0.]);
|
||||
h.app.run("sketch.dimension", json!({"entities":["l1"],"value":20,"text_at":[10,8]})).unwrap();
|
||||
h.app.run("sketch.line", json!({"points":[[0,-10],[12,-10]],"infer":false})).unwrap();
|
||||
h.app.run("sketch.dimension", json!({"entities":["l2"],"value":12,"text_at":[6,-16]})).unwrap();
|
||||
h.settle();
|
||||
crate::dim_view::edit_param(&h.app, "d2");
|
||||
h.frames(5);
|
||||
type_value(&mut h, "8");
|
||||
h.step(&json!({"click":{"dimension":"d1"}})).unwrap();
|
||||
assert!(crate::dim_view::editing());
|
||||
h.step(&json!({"key":"Enter"})).unwrap();
|
||||
assert_eq!(h.app.session.doc.param("d2").unwrap().expr, "8");
|
||||
crate::dim_view::cancel();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dimension_parameters_use_typing_and_ctrl_space_completion() {
|
||||
for (prefix, expected) in [("wid", "width"), ("", "width"), ("d", "d1")] {
|
||||
|
||||
@@ -2461,7 +2461,11 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
crate::tools::on_click(app, &proj, a);
|
||||
app.viewport.draw_drag = app.tool.as_ref().is_some_and(|t| t.pts.len() == 1);
|
||||
}
|
||||
if resp.clicked() && !held {
|
||||
let reference_pick = (resp.clicked() || resp.double_clicked())
|
||||
&& !held
|
||||
&& app.dialog.is_none()
|
||||
&& crate::dim_view::pick_reference(ui.ctx(), p, resp.double_clicked());
|
||||
if resp.clicked() && !held && !reference_pick {
|
||||
if app.tool.is_some() {
|
||||
crate::tools::on_click(app, &proj, p);
|
||||
} else if app.dialog.as_ref().is_some_and(|d| d.wants_picks()) {
|
||||
@@ -2508,6 +2512,7 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
// Double-click a dimension: edit its value in place.
|
||||
let dim_edit = resp.double_clicked()
|
||||
&& !held
|
||||
&& !crate::dim_view::editing()
|
||||
&& app.tool.is_none()
|
||||
&& app.dialog.is_none()
|
||||
&& (crate::sketch_modify_ui::double_click_pattern(app, p) || crate::dim_view::double_click(app, p));
|
||||
|
||||
@@ -5,6 +5,13 @@ are ones you add yourself, with a unit, an expression and an optional comment. C
|
||||
**Change Parameters** (`parameters.change`). The timeline rebuilds from the first feature that
|
||||
uses them.
|
||||
|
||||
When creating or editing a sketch dimension, its value editor shows its model parameter
|
||||
name (`d1`, `d2`, …). Double-click another driving dimension to insert its name at the cursor,
|
||||
replacing selected text; add arithmetic such as `/ 2` to build a dependent dimension.
|
||||
Single clicks on dimensions leave the expression unchanged. Press **Enter** to apply the
|
||||
expression or **Escape** to cancel the edit.
|
||||
Add and edit user parameters through the existing **Change Parameters** command.
|
||||
|
||||
In the SOLID toolbar's CONFIGURE panel, click **Parameters** to open the user and model
|
||||
parameter editor. The separate **Configurations** button opens the configuration table;
|
||||
**Configure** remains in the dropdown for configuring columns.
|
||||
|
||||
Reference in new issue
Block a user