diff --git a/crates/ui-egui/src/dim_view.rs b/crates/ui-egui/src/dim_view.rs index 4c88a7eb..ca2ebd09 100644 --- a/crates/ui-egui/src/dim_view.rs +++ b/crates/ui-egui/src/dim_view.rs @@ -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, + selection: Option, } #[derive(Clone, Copy, Debug)] @@ -132,7 +133,9 @@ pub fn begin_dimension(app: &mut SolveApp, entities: Vec, 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, pointer: Option 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)); diff --git a/crates/ui-egui/src/dimension_create_ui_tests.rs b/crates/ui-egui/src/dimension_create_ui_tests.rs index fc013b22..6ee556f7 100644 --- a/crates/ui-egui/src/dimension_create_ui_tests.rs +++ b/crates/ui-egui/src/dimension_create_ui_tests.rs @@ -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")] { diff --git a/crates/ui-egui/src/viewport.rs b/crates/ui-egui/src/viewport.rs index 7017fee4..b0ebc8b5 100644 --- a/crates/ui-egui/src/viewport.rs +++ b/crates/ui-egui/src/viewport.rs @@ -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)); diff --git a/docs/book/src/guide/parameters.md b/docs/book/src/guide/parameters.md index c3edf3ea..369ca633 100644 --- a/docs/book/src/guide/parameters.md +++ b/docs/book/src/guide/parameters.md @@ -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.