From bd9a2d08f77b17d22b54263373c810c919cfc4d0 Mon Sep 17 00:00:00 2001 From: Felix Riestra Date: Thu, 10 Sep 2026 23:16:11 +0200 Subject: [PATCH] Add Properties-panel Constraints/Parameters sections, a constraint-pill value toggle, and redesigned constraint-ribbon icons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Properties panel: - New "Parameters" section (shown with no selection) listing every named parameter with inline name/formula editing, add, and delete, backed by new PropValue::ParamRow/ParamAddRow and command_driver handlers (resolve_named_parameter_edit, on_prop_param_commit/_delete/_add_new). A fresh parameter's formula now defaults to "1", not "0" — a 0-length seed value is a genuine solver singularity for a Distance constraint driven by that parameter, with no defined direction to grow back out of. - New "Constraints" section (shown for a single selected entity) listing every constraint touching it as a clickable PropValue::EntityLink that selects the linked entities in the viewport, danger-tinted when the constraint conflicts. Constraint pills: - New Options > General toggle, "Show values and parameter names on constraint markers" (on by default, persisted). Off, every pill shows just its bare glyph instead of glyph+driven-value/parameter-name — the glyph alone is enough to see a constraint is present, without the value text covering nearby geometry on a dense sketch. Constraint-ribbon icons: - Replaced all 18 unicode/letter glyph icons (including the plain "F", "L", "M", "S" letters) with proper SVGs under assets/icons/constrain/, matching the Modify section's two-color style (#B4B6B9 white / #6DB7ED blue, consistent stroke width). Tangent reuses the Draw section's white-arc-plus-blue-dots motif. Co-Authored-By: Claude Sonnet 5 --- assets/icons/constrain/angle.svg | 5 + assets/icons/constrain/center_point.svg | 4 + assets/icons/constrain/coincident.svg | 4 + assets/icons/constrain/colinear.svg | 4 + assets/icons/constrain/concentric.svg | 4 + assets/icons/constrain/distance.svg | 7 + assets/icons/constrain/equal.svg | 4 + assets/icons/constrain/equal_distance.svg | 8 + assets/icons/constrain/fixed.svg | 7 + assets/icons/constrain/horizontal.svg | 3 + assets/icons/constrain/midpoint.svg | 4 + assets/icons/constrain/normal.svg | 5 + assets/icons/constrain/parallel.svg | 4 + assets/icons/constrain/perpendicular.svg | 4 + assets/icons/constrain/point_on_curve.svg | 4 + assets/icons/constrain/symmetric.svg | 6 + assets/icons/constrain/tangent.svg | 5 + assets/icons/constrain/vertical.svg | 3 + src/app/command_driver.rs | 215 ++++++++++++++++++ src/app/mod.rs | 36 +++ src/app/properties.rs | 88 +++++++ src/app/settings.rs | 11 + src/app/update/file.rs | 2 + src/app/update/mod.rs | 27 +++ src/app/view/mod.rs | 10 +- src/app/view/modal.rs | 1 + src/modules/draw/constrain/coincident.rs | 2 +- src/modules/draw/constrain/equal_distance.rs | 2 +- src/modules/draw/constrain/point_on_entity.rs | 6 +- src/modules/draw/constrain/tools.rs | 22 +- src/modules/draw/constrain/value.rs | 4 +- src/scene/model/object.rs | 24 +- src/scene/selection.rs | 10 +- src/ui/properties.rs | 168 ++++++++++++++ src/ui/window/named_parameters.rs | 2 +- src/ui/window/options.rs | 18 ++ 36 files changed, 707 insertions(+), 26 deletions(-) create mode 100644 assets/icons/constrain/angle.svg create mode 100644 assets/icons/constrain/center_point.svg create mode 100644 assets/icons/constrain/coincident.svg create mode 100644 assets/icons/constrain/colinear.svg create mode 100644 assets/icons/constrain/concentric.svg create mode 100644 assets/icons/constrain/distance.svg create mode 100644 assets/icons/constrain/equal.svg create mode 100644 assets/icons/constrain/equal_distance.svg create mode 100644 assets/icons/constrain/fixed.svg create mode 100644 assets/icons/constrain/horizontal.svg create mode 100644 assets/icons/constrain/midpoint.svg create mode 100644 assets/icons/constrain/normal.svg create mode 100644 assets/icons/constrain/parallel.svg create mode 100644 assets/icons/constrain/perpendicular.svg create mode 100644 assets/icons/constrain/point_on_curve.svg create mode 100644 assets/icons/constrain/symmetric.svg create mode 100644 assets/icons/constrain/tangent.svg create mode 100644 assets/icons/constrain/vertical.svg diff --git a/assets/icons/constrain/angle.svg b/assets/icons/constrain/angle.svg new file mode 100644 index 000000000..e632dd2c8 --- /dev/null +++ b/assets/icons/constrain/angle.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/constrain/center_point.svg b/assets/icons/constrain/center_point.svg new file mode 100644 index 000000000..cb4cb7333 --- /dev/null +++ b/assets/icons/constrain/center_point.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/constrain/coincident.svg b/assets/icons/constrain/coincident.svg new file mode 100644 index 000000000..263234192 --- /dev/null +++ b/assets/icons/constrain/coincident.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/constrain/colinear.svg b/assets/icons/constrain/colinear.svg new file mode 100644 index 000000000..be8d671c9 --- /dev/null +++ b/assets/icons/constrain/colinear.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/constrain/concentric.svg b/assets/icons/constrain/concentric.svg new file mode 100644 index 000000000..d68acc43b --- /dev/null +++ b/assets/icons/constrain/concentric.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/constrain/distance.svg b/assets/icons/constrain/distance.svg new file mode 100644 index 000000000..278ee92ee --- /dev/null +++ b/assets/icons/constrain/distance.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icons/constrain/equal.svg b/assets/icons/constrain/equal.svg new file mode 100644 index 000000000..bee8e3fec --- /dev/null +++ b/assets/icons/constrain/equal.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/constrain/equal_distance.svg b/assets/icons/constrain/equal_distance.svg new file mode 100644 index 000000000..9b027b7fd --- /dev/null +++ b/assets/icons/constrain/equal_distance.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/icons/constrain/fixed.svg b/assets/icons/constrain/fixed.svg new file mode 100644 index 000000000..b0e75becb --- /dev/null +++ b/assets/icons/constrain/fixed.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icons/constrain/horizontal.svg b/assets/icons/constrain/horizontal.svg new file mode 100644 index 000000000..2259c70bf --- /dev/null +++ b/assets/icons/constrain/horizontal.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/constrain/midpoint.svg b/assets/icons/constrain/midpoint.svg new file mode 100644 index 000000000..23ac2c0f1 --- /dev/null +++ b/assets/icons/constrain/midpoint.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/constrain/normal.svg b/assets/icons/constrain/normal.svg new file mode 100644 index 000000000..f4ddef0cd --- /dev/null +++ b/assets/icons/constrain/normal.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/constrain/parallel.svg b/assets/icons/constrain/parallel.svg new file mode 100644 index 000000000..891fd41f1 --- /dev/null +++ b/assets/icons/constrain/parallel.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/constrain/perpendicular.svg b/assets/icons/constrain/perpendicular.svg new file mode 100644 index 000000000..520becabb --- /dev/null +++ b/assets/icons/constrain/perpendicular.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/constrain/point_on_curve.svg b/assets/icons/constrain/point_on_curve.svg new file mode 100644 index 000000000..745845a1e --- /dev/null +++ b/assets/icons/constrain/point_on_curve.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/constrain/symmetric.svg b/assets/icons/constrain/symmetric.svg new file mode 100644 index 000000000..6a73ba883 --- /dev/null +++ b/assets/icons/constrain/symmetric.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/constrain/tangent.svg b/assets/icons/constrain/tangent.svg new file mode 100644 index 000000000..7d0422129 --- /dev/null +++ b/assets/icons/constrain/tangent.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/constrain/vertical.svg b/assets/icons/constrain/vertical.svg new file mode 100644 index 000000000..81c68fe6e --- /dev/null +++ b/assets/icons/constrain/vertical.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index 68c44ca5c..218067ef5 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -1051,6 +1051,147 @@ impl OpenCADStudio { } } + /// Re-solves every entity touched by an enabled constraint driven by + /// `name` — the targeted, single-row counterpart to + /// `apply_named_parameter_editor_rows`'s "re-solve what it affects": an + /// inline Properties-panel edit changes one parameter, not the whole + /// table, so unlike that whole-table Apply this can cheaply scope the + /// re-solve to just the entities that actually reference it. + fn resolve_named_parameter_edit(&mut self, i: usize, name: &str) { + let touched: Vec = self.tabs[i] + .scene + .sketch_constraints + .iter() + .flat_map(|set| set.constraints.iter()) + .filter(|c| { + c.enabled + && matches!(&c.driving_param, Some(crate::scene::named_parameters::DrivingValue::Named(n)) if n == name) + }) + .flat_map(|c| c.refs.iter().map(|r| r.entity)) + .collect(); + self.tabs[i].dirty = true; + if touched.is_empty() { + return; + } + let pending = self.begin_undo(i, "Edit named parameter", touched.len(), true); + let changes: Vec<(Handle, crate::scene::ChangeKind)> = + touched.into_iter().map(|h| (h, crate::scene::ChangeKind::Modified)).collect(); + self.tabs[i].scene.bump_entities(&changes); + if let Some(pd) = pending { + self.commit_undo_delta(i, pd); + } + } + + /// Commits one field of one Parameters-section row (Properties panel) — + /// the per-row, commit-on-submit counterpart to + /// `apply_named_parameter_editor_rows`'s whole-table Apply. A Formula + /// edit is a plain redefine (`ParameterTable::set` on the existing + /// name); a Name edit is a remove-then-set (there's no rename primitive + /// — `ParameterTable` upserts by name), restoring the old name if the + /// new one fails to validate so nothing is silently lost. Either way + /// this only re-solves what actually depends on this one parameter — + /// no "duplicate name" scan across the whole table is needed the way + /// the buffered modal Apply needs one, since every row here is always + /// already a real, distinct table entry. + pub(super) fn on_prop_param_commit( + &mut self, + index: usize, + field: crate::ui::window::named_parameters::ParamField, + ) -> Task { + use crate::ui::properties::FieldKey; + use crate::ui::window::named_parameters::ParamField; + let i = self.active_tab; + let key = FieldKey::Param(index, field); + self.tabs[i].properties.active_field = None; + let Some(typed) = self.tabs[i].properties.edit_buf.remove(&key) else { + return Task::none(); + }; + let typed = typed.trim().to_string(); + let Some(current) = self.tabs[i].scene.named_parameters().iter().nth(index).cloned() else { + self.refresh_properties(); + return Task::none(); + }; + + let (result, resolve_name) = match field { + ParamField::Formula => { + if typed.is_empty() || typed == current.source { + self.refresh_properties(); + return Task::none(); + } + (self.tabs[i].scene.named_parameters_mut().set(¤t.name, &typed), current.name.clone()) + } + ParamField::Name => { + if typed.is_empty() || typed == current.name { + self.refresh_properties(); + return Task::none(); + } + if self.tabs[i].scene.named_parameters().contains(&typed) { + self.command_line.push_error(crate::tf!("Parameter '{}' already exists.", typed).as_ref()); + self.refresh_properties(); + return Task::none(); + } + self.tabs[i].scene.named_parameters_mut().remove(¤t.name); + let outcome = self.tabs[i].scene.named_parameters_mut().set(&typed, ¤t.source); + if outcome.is_err() { + let _ = self.tabs[i].scene.named_parameters_mut().set(¤t.name, ¤t.source); + } + (outcome, typed.clone()) + } + }; + + if let Err(e) = result { + self.command_line.push_error(crate::tf!("Parameter '{}': {}", current.name, e).as_ref()); + self.refresh_properties(); + return Task::none(); + } + self.resolve_named_parameter_edit(i, &resolve_name); + self.refresh_properties(); + Task::none() + } + + /// Removes Parameters-section row `index` immediately — no confirmation, + /// matching AutoCAD's own Parameters Manager delete button. Any + /// constraint that referenced it gets the same "undefined reference" + /// resolve-failure treatment a formula's own bad reference already gets + /// (`build_constraint`) — re-solving surfaces that rather than needing + /// special-case handling here. + pub(super) fn on_prop_param_delete(&mut self, index: usize) -> Task { + let i = self.active_tab; + let Some(name) = self.tabs[i].scene.named_parameters().iter().nth(index).map(|p| p.name.clone()) else { + return Task::none(); + }; + self.tabs[i].scene.named_parameters_mut().remove(&name); + self.resolve_named_parameter_edit(i, &name); + self.refresh_properties(); + Task::none() + } + + /// The Parameters section's "+ Add parameter" row: appends a fresh, + /// uniquely-named parameter (`param1`, `param2`, …) the user then + /// renames/redefines inline via `on_prop_param_commit`. Defaults to `1`, + /// not `0`: renaming this onto a name an existing Distance/Radius + /// constraint already references (unusual, but reachable — define + /// first, wire up the constraint's reference later) would otherwise + /// briefly collapse that geometry to a zero-length/zero-radius + /// degenerate state, which is a genuine numerical singularity for a + /// point-to-point distance constraint to grow back out of (no defined + /// direction to separate two exactly-coincident points from). `1` never + /// hits that. + pub(super) fn on_prop_param_add_new(&mut self) -> Task { + let i = self.active_tab; + let mut n = 1usize; + let name = loop { + let candidate = format!("param{n}"); + if !self.tabs[i].scene.named_parameters().contains(&candidate) { + break candidate; + } + n += 1; + }; + let _ = self.tabs[i].scene.named_parameters_mut().set(&name, "1"); + self.refresh_properties(); + Task::none() + } + fn apply_cmd_result_inner(&mut self, result: CmdResult) -> Task { let i = self.active_tab; let preserve_commit_layer = self.tabs[i] @@ -6370,6 +6511,80 @@ mod sketch_constraint_undo_tests { assert!((len - 3.0).abs() < 1e-6, "length should track the redefined parameter, got {len}"); } + /// End-to-end through the actual `Message` handlers a Properties-panel + /// click/keystroke would fire — not `apply_named_parameter_editor_rows` + /// directly — covering the whole Parameters-section lifecycle: add, + /// rename, redefine, and confirm a referencing constraint re-solves + /// after each commit (not just after a whole-table Apply, since this + /// path commits per field). + #[test] + fn properties_panel_parameter_row_add_rename_redefine_and_delete() { + use crate::ui::window::named_parameters::ParamField; + + let mut app = OpenCADStudio::new_for_test(); + let _ = app.automation_op(r#"{"op":"new"}"#); + let line = add_line(&mut app, 0.0, 0.0, 10.0, 0.0); + let _ = app.apply_cmd_result(CmdResult::AddSketchConstraint { + kind: ConstraintKind::Distance, + refs: vec![SketchRef::point(line, 0), SketchRef::point(line, 1)], + driving_param: Some(crate::scene::named_parameters::DrivingValue::Named("len".to_string())), + label: "Distance constraint", + }); + + // Add: the first row is auto-named "param1". + let _ = app.update(Message::PropParamAddNew); + assert!(app.tabs[app.active_tab].scene.named_parameters().contains("param1")); + let index = app.tabs[app.active_tab].scene.named_parameters().iter().position(|p| p.name == "param1").unwrap(); + + // Rename param1 -> len (commit-on-submit, not per keystroke: input + // alone must not touch the table yet). + let _ = app.update(Message::PropParamInput { index, field: ParamField::Name, value: "len".to_string() }); + assert!(app.tabs[app.active_tab].scene.named_parameters().contains("param1"), "typing alone must not commit"); + let _ = app.update(Message::PropParamCommit { index, field: ParamField::Name }); + assert!(!app.tabs[app.active_tab].scene.named_parameters().contains("param1")); + assert!(app.tabs[app.active_tab].scene.named_parameters().contains("len")); + + // Redefine its formula to 8 and confirm the Distance constraint + // (already referencing "len" by name, defined before this) re-solves. + let _ = app.update(Message::PropParamInput { index, field: ParamField::Formula, value: "8".to_string() }); + let _ = app.update(Message::PropParamCommit { index, field: ParamField::Formula }); + assert_eq!(app.tabs[app.active_tab].scene.named_parameters().resolve("len"), Ok(8.0)); + let (start, end) = match app.tabs[app.active_tab].scene.document.get_entity(line) { + Some(acadrust::EntityType::Line(l)) => (l.start, l.end), + other => panic!("expected a Line, got {other:?}"), + }; + let len = ((end.x - start.x).powi(2) + (end.y - start.y).powi(2)).sqrt(); + assert!((len - 8.0).abs() < 1e-6, "length should track the redefined parameter, got {len}"); + + // Delete: the row is gone from the table. + let _ = app.update(Message::PropParamDelete(index)); + assert!(!app.tabs[app.active_tab].scene.named_parameters().contains("len")); + } + + /// A Constraints-section row click selects every entity the constraint + /// references, replacing whatever was selected before. + #[test] + fn properties_panel_constraint_link_click_selects_its_entities() { + let mut app = OpenCADStudio::new_for_test(); + let _ = app.automation_op(r#"{"op":"new"}"#); + let a = add_line(&mut app, 0.0, 0.0, 10.0, 0.0); + let b = add_line(&mut app, 0.0, 5.0, 10.0, 5.0); + let unrelated = add_line(&mut app, 20.0, 20.0, 30.0, 20.0); + let _ = app.apply_cmd_result(CmdResult::AddSketchConstraint { + kind: ConstraintKind::Parallel, + refs: vec![SketchRef::whole(a), SketchRef::whole(b)], + driving_param: None, + label: "Parallel constraint", + }); + app.tabs[app.active_tab].scene.select_entity(unrelated, true); + + let _ = app.update(Message::PropConstraintLinkClick(vec![a, b])); + + let selected: std::collections::HashSet = + app.tabs[app.active_tab].scene.selected_entities().into_iter().map(|(h, _)| h).collect(); + assert_eq!(selected, std::collections::HashSet::from([a, b])); + } + /// A row that fails to validate (here, a formula that doesn't parse) /// must not silently vanish or corrupt the table: it's reported as an /// error and simply isn't added, while a valid row alongside it still diff --git a/src/app/mod.rs b/src/app/mod.rs index 73a73d6fd..f19b01c19 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -557,6 +557,13 @@ pub(super) struct OpenCADStudio { /// this only governs whether the graph is *created* in a scope that /// doesn't have one yet. pub write_dwg_native_constraints: bool, + /// When true (default), a sketch constraint's viewport pill shows its + /// glyph plus a driven value or named-parameter name (e.g. "⌀ 5.000" or + /// "⌀ hole_dia"). When false, every pill shows just the bare glyph — the + /// value/name text can cover important canvas detail on a dense sketch, + /// and the glyph alone is already enough to see *that* a constraint is + /// present (`docs/parametric_system_design.md` §6.3/§7). + pub show_constraint_values: bool, /// Minutes between autosaves to a `.sv$` recovery file (SAVETIME command); /// 0 disables autosave. pub savetime_min: i32, @@ -2046,6 +2053,9 @@ pub enum Message { /// Toggle writing AutoCAD-native constraint objects on save, from /// Options. See `write_dwg_native_constraints`'s doc comment. WriteDwgNativeConstraintsChanged(bool), + /// Toggle showing driven values/named-parameter names on constraint + /// pills, from Options. See `show_constraint_values`'s doc comment. + ShowConstraintValuesChanged(bool), /// Switch the interface language and redraw localized views. LanguageChanged(crate::i18n::Language), /// Drop every entity from the active drawing. @@ -2790,6 +2800,31 @@ pub enum Message { /// and re-solve every constraint that reads a named parameter; stays /// open. NamedParametersApply, + // ── Parameters / Constraints sections embedded in the Properties panel + // (AutoCAD-style: a live section instead of the modal above, per-row + // commit-on-submit instead of a whole-table Apply) ───────────────────── + /// Live text of one column of parameter row `index`, keyed by its + /// `ParameterTable::iter()` position — see `PropValue::ParamRow`. + PropParamInput { + index: usize, + field: crate::ui::window::named_parameters::ParamField, + value: String, + }, + /// Commit row `index`'s buffered edit for `field` to `Scene:: + /// named_parameters` (Enter / losing focus) and re-solve whatever it + /// drives. + PropParamCommit { + index: usize, + field: crate::ui::window::named_parameters::ParamField, + }, + /// Remove parameter row `index` immediately (no confirmation, matching + /// AutoCAD's own Parameters Manager delete button). + PropParamDelete(usize), + /// Append a fresh, uniquely-named parameter to `Scene::named_parameters`. + PropParamAddNew, + /// A Constraints-section row was clicked: select every entity in the + /// list (replacing the current selection) in the viewport. + PropConstraintLinkClick(Vec), // ── About window ──────────────────────────────────────────────────── AboutOpen, /// Close whatever in-canvas modal dialog is open (Plan B). @@ -3479,6 +3514,7 @@ impl OpenCADStudio { backup_on_save: true, file_assoc_enabled: true, write_dwg_native_constraints: false, + show_constraint_values: true, savetime_min: 10, default_bg_color: None, default_paper_bg_color: None, diff --git a/src/app/properties.rs b/src/app/properties.rs index 8fd1faec1..47501b87a 100644 --- a/src/app/properties.rs +++ b/src/app/properties.rs @@ -406,6 +406,38 @@ impl OpenCADStudio { read_only(t!("Visual Style").as_ref(), tab.visual_style.clone()), ], }, + // Document-wide named-parameter table (AutoCAD + // Parameters Manager equivalent), embedded here + // instead of a separate modal — see `PropValue:: + // ParamRow`'s doc comment. Belongs on the no- + // selection (drawing-level) page, not a per-entity + // one: a parameter isn't owned by any one entity. + PropSection { + title: t!("Parameters").into_owned(), + props: { + let mut props: Vec = scene + .named_parameters() + .iter() + .enumerate() + .map(|(index, p)| Property { + label: String::new(), + field: "named_parameter", + value: PropValue::ParamRow { + index, + name: p.name.clone(), + formula: p.source.clone(), + resolved: scene.named_parameters().resolve(&p.name).map_err(|e| e.to_string()), + }, + }) + .collect(); + props.push(Property { + label: String::new(), + field: "named_parameter_add", + value: PropValue::ParamAddRow, + }); + props + }, + }, ]; ui::PropertiesPanel { title: t!("No selection").into_owned(), @@ -2126,6 +2158,53 @@ impl OpenCADStudio { if compact_solid { retain_compact_solid_sections(&mut sections); } + // Persistent sketch-constraint list for this entity — + // the Properties-panel analogue of the viewport's + // constraint glyph pills: one row per constraint + // touching `handle`, clickable to select every entity + // it relates (PropConstraintLinkClick). Omitted + // entirely when the entity has none — an unconstrained + // entity (the common case) shouldn't gain empty + // bookkeeping. + { + let scope = self.tabs[i].current_sketch_scope(); + let scene = &self.tabs[i].scene; + if let Some(set) = scene.sketch_constraint_set(scope) { + let props: Vec = set + .constraints_touching(handle) + .map(|c| { + let conflicting = set.conflicts.iter().any(|(id, _)| *id == c.id); + let value_suffix = match &c.driving_param { + Some(crate::scene::named_parameters::DrivingValue::Literal(v)) => { + format!(" = {v:.2}") + } + Some(crate::scene::named_parameters::DrivingValue::Named(name)) => { + match scene.named_parameters().resolve(name) { + Ok(v) => format!(" = {name} ({v:.2})"), + Err(_) => format!(" = {name} (?)"), + } + } + None => String::new(), + }; + let label = format!("{} {:?}{}", c.kind.glyph_symbol(), c.kind, value_suffix); + let mut handles: Vec = c.refs.iter().map(|r| r.entity).collect(); + handles.sort_by_key(|h| h.value()); + handles.dedup(); + crate::scene::model::object::Property { + label, + field: "sketch_constraint", + value: crate::scene::model::object::PropValue::EntityLink { handles, conflicting }, + } + }) + .collect(); + if !props.is_empty() { + sections.push(crate::scene::model::object::PropSection { + title: t!("Constraints").into_owned(), + props, + }); + } + } + } let title = match entity { acadrust::EntityType::Insert(ins) => { let is_xref = self.tabs[i] @@ -3102,6 +3181,15 @@ fn make_sections_read_only( } PropValue::Stepper { display, .. } => display.clone(), PropValue::AttrText { value, .. } => value.clone(), + // The Constraints/Parameters sections aren't per-entity editable + // fields to begin with (a constraint link is a navigation + // action, not a value; a parameter belongs to the whole + // document, not the locked entity) — locking the entity doesn't + // change either, so just carry through readable text instead of + // collapsing them to a value that was never editable anyway. + PropValue::EntityLink { handles, .. } => format!("{} entity link(s)", handles.len()), + PropValue::ParamRow { name, formula, .. } => format!("{name} = {formula}"), + PropValue::ParamAddRow => String::new(), }; property.field = "locked_read_only"; property.value = PropValue::ReadOnly(text); diff --git a/src/app/settings.rs b/src/app/settings.rs index 7e244126d..1f8fa5d5e 100644 --- a/src/app/settings.rs +++ b/src/app/settings.rs @@ -244,6 +244,12 @@ pub struct UserSettings { /// graph keeps it synced regardless of this setting). #[serde(default)] pub write_dwg_native_constraints: bool, + /// When true (default), a sketch constraint's viewport pill shows its + /// glyph plus a driven value or named-parameter name. When false, every + /// pill shows just the bare glyph, so the value/name text doesn't cover + /// canvas detail on a dense sketch. + #[serde(default = "default_show_constraint_values")] + pub show_constraint_values: bool, /// Minutes between autosaves to a `.sv$` recovery file (SAVETIME command). /// 0 disables autosave. pub savetime_min: i32, @@ -312,6 +318,10 @@ fn default_dimension_continue_mode() -> i16 { 1 } +fn default_show_constraint_values() -> bool { + true +} + fn deserialize_clipromptlines<'de, D>(de: D) -> Result where D: serde::Deserializer<'de>, @@ -361,6 +371,7 @@ impl Default for UserSettings { backup_on_save: true, file_assoc_enabled: true, write_dwg_native_constraints: false, + show_constraint_values: true, savetime_min: 10, default_save_format: crate::io::DEFAULT_SAVE_FORMAT.to_string(), pick_add: true, diff --git a/src/app/update/file.rs b/src/app/update/file.rs index 9c5aaab3b..b3465f2e3 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -397,6 +397,7 @@ impl OpenCADStudio { backup_on_save: self.backup_on_save, file_assoc_enabled: self.file_assoc_enabled, write_dwg_native_constraints: self.write_dwg_native_constraints, + show_constraint_values: self.show_constraint_values, savetime_min: self.savetime_min, default_save_format: self.default_save_format.clone(), pick_add: self.pick_add, @@ -467,6 +468,7 @@ impl OpenCADStudio { self.backup_on_save = s.backup_on_save; self.file_assoc_enabled = s.file_assoc_enabled; self.write_dwg_native_constraints = s.write_dwg_native_constraints; + self.show_constraint_values = s.show_constraint_values; self.savetime_min = s.savetime_min; self.default_save_format = crate::io::canonical_save_format(&s.default_save_format).to_string(); diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index 40b167542..0436a531f 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -5949,6 +5949,28 @@ impl OpenCADStudio { Task::none() } + // ── Parameters / Constraints sections embedded in the + // Properties panel ────────────────────────────────────────────── + Message::PropParamInput { index, field, value } => { + self.tabs[self.active_tab] + .properties + .edit_buf + .insert(crate::ui::properties::FieldKey::Param(index, field), value); + Task::none() + } + Message::PropParamCommit { index, field } => self.on_prop_param_commit(index, field), + Message::PropParamDelete(index) => self.on_prop_param_delete(index), + Message::PropParamAddNew => self.on_prop_param_add_new(), + Message::PropConstraintLinkClick(handles) => { + let i = self.active_tab; + self.tabs[i].scene.deselect_all(); + for h in handles { + self.tabs[i].scene.select_entity(h, false); + } + self.refresh_properties(); + Task::none() + } + // ── Options / About windows ─────────────────────────────────── Message::OptionsOpen => { self.active_modal = Some(super::ModalKind::Options); @@ -6312,6 +6334,11 @@ impl OpenCADStudio { self.persist_settings_if_changed(); Task::none() } + Message::ShowConstraintValuesChanged(enabled) => { + self.show_constraint_values = enabled; + self.persist_settings_if_changed(); + Task::none() + } Message::LanguageChanged(language) => { if self.language == language { return Task::none(); diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index 05d1abbde..c935be08a 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -839,10 +839,12 @@ bg={bg_ms:.1}ms n={view_count}" )?; let point = iced::Point::new(bounds.x + screen.x, bounds.y + screen.y); let is_conflicting = set.conflicts.iter().any(|(id, _)| *id == c.id); - point - .x - .is_finite() - .then(|| (point, crate::scene::sketch_constraints::glyph_label(c), is_conflicting)) + let label = if self.show_constraint_values { + crate::scene::sketch_constraints::glyph_label(c) + } else { + c.kind.glyph_symbol().to_string() + }; + point.x.is_finite().then(|| (point, label, is_conflicting)) }) .collect() } diff --git a/src/app/view/modal.rs b/src/app/view/modal.rs index 60cd94d7f..2e749248a 100644 --- a/src/app/view/modal.rs +++ b/src/app/view/modal.rs @@ -198,6 +198,7 @@ impl OpenCADStudio { &self.default_save_format, self.file_assoc_enabled, self.write_dwg_native_constraints, + self.show_constraint_values, &self.ui_theme, &self.theme_color_inputs, self.language, diff --git a/src/modules/draw/constrain/coincident.rs b/src/modules/draw/constrain/coincident.rs index b4da68fa3..53ad9ba0b 100644 --- a/src/modules/draw/constrain/coincident.rs +++ b/src/modules/draw/constrain/coincident.rs @@ -23,7 +23,7 @@ pub mod coincident_tool { ToolDef { id: "CCONSTRAINT", label: "Coincident", - icon: IconKind::Glyph("≡"), + icon: IconKind::Svg(include_bytes!("../../../../assets/icons/constrain/coincident.svg")), event: ModuleEvent::Command("CCONSTRAINT".to_string()), } } diff --git a/src/modules/draw/constrain/equal_distance.rs b/src/modules/draw/constrain/equal_distance.rs index f71725a3b..745b21f4f 100644 --- a/src/modules/draw/constrain/equal_distance.rs +++ b/src/modules/draw/constrain/equal_distance.rs @@ -17,7 +17,7 @@ pub mod equal_distance_tool { ToolDef { id: "EDCONSTRAINT", label: "Equal Distance", - icon: IconKind::Glyph("≐"), + icon: IconKind::Svg(include_bytes!("../../../../assets/icons/constrain/equal_distance.svg")), event: ModuleEvent::Command("EDCONSTRAINT".to_string()), } } diff --git a/src/modules/draw/constrain/point_on_entity.rs b/src/modules/draw/constrain/point_on_entity.rs index f572a2439..f4cc523b5 100644 --- a/src/modules/draw/constrain/point_on_entity.rs +++ b/src/modules/draw/constrain/point_on_entity.rs @@ -24,7 +24,7 @@ pub mod center_point_tool { ToolDef { id: "CPCONSTRAINT", label: "Center Point", - icon: IconKind::Glyph("⊕"), + icon: IconKind::Svg(include_bytes!("../../../../assets/icons/constrain/center_point.svg")), event: ModuleEvent::Command("CPCONSTRAINT".to_string()), } } @@ -36,7 +36,7 @@ pub mod midpoint_tool { ToolDef { id: "MPCONSTRAINT", label: "Midpoint", - icon: IconKind::Glyph("M"), + icon: IconKind::Svg(include_bytes!("../../../../assets/icons/constrain/midpoint.svg")), event: ModuleEvent::Command("MPCONSTRAINT".to_string()), } } @@ -48,7 +48,7 @@ pub mod point_on_curve_tool { ToolDef { id: "OCCONSTRAINT", label: "Point on Curve", - icon: IconKind::Glyph("∈"), + icon: IconKind::Svg(include_bytes!("../../../../assets/icons/constrain/point_on_curve.svg")), event: ModuleEvent::Command("OCCONSTRAINT".to_string()), } } diff --git a/src/modules/draw/constrain/tools.rs b/src/modules/draw/constrain/tools.rs index aaf83458c..79d2777e0 100644 --- a/src/modules/draw/constrain/tools.rs +++ b/src/modules/draw/constrain/tools.rs @@ -8,7 +8,7 @@ pub mod horizontal { ToolDef { id: "HCONSTRAINT", label: "Horizontal", - icon: IconKind::Glyph("—"), + icon: IconKind::Svg(include_bytes!("../../../../assets/icons/constrain/horizontal.svg")), event: ModuleEvent::Command("HCONSTRAINT".to_string()), } } @@ -20,7 +20,7 @@ pub mod vertical { ToolDef { id: "VCONSTRAINT", label: "Vertical", - icon: IconKind::Glyph("│"), + icon: IconKind::Svg(include_bytes!("../../../../assets/icons/constrain/vertical.svg")), event: ModuleEvent::Command("VCONSTRAINT".to_string()), } } @@ -32,7 +32,7 @@ pub mod parallel { ToolDef { id: "PCONSTRAINT", label: "Parallel", - icon: IconKind::Glyph("∥"), + icon: IconKind::Svg(include_bytes!("../../../../assets/icons/constrain/parallel.svg")), event: ModuleEvent::Command("PCONSTRAINT".to_string()), } } @@ -44,7 +44,7 @@ pub mod perpendicular { ToolDef { id: "QCONSTRAINT", label: "Perpendicular", - icon: IconKind::Glyph("⊥"), + icon: IconKind::Svg(include_bytes!("../../../../assets/icons/constrain/perpendicular.svg")), event: ModuleEvent::Command("QCONSTRAINT".to_string()), } } @@ -56,7 +56,7 @@ pub mod equal { ToolDef { id: "ECONSTRAINT", label: "Equal", - icon: IconKind::Glyph("="), + icon: IconKind::Svg(include_bytes!("../../../../assets/icons/constrain/equal.svg")), event: ModuleEvent::Command("ECONSTRAINT".to_string()), } } @@ -68,7 +68,7 @@ pub mod tangent { ToolDef { id: "TCONSTRAINT", label: "Tangent", - icon: IconKind::Glyph("⌒"), + icon: IconKind::Svg(include_bytes!("../../../../assets/icons/constrain/tangent.svg")), event: ModuleEvent::Command("TCONSTRAINT".to_string()), } } @@ -80,7 +80,7 @@ pub mod concentric { ToolDef { id: "NCONSTRAINT", label: "Concentric", - icon: IconKind::Glyph("◎"), + icon: IconKind::Svg(include_bytes!("../../../../assets/icons/constrain/concentric.svg")), event: ModuleEvent::Command("NCONSTRAINT".to_string()), } } @@ -100,7 +100,7 @@ pub mod normal { ToolDef { id: "NRCONSTRAINT", label: "Normal", - icon: IconKind::Glyph("⊾"), + icon: IconKind::Svg(include_bytes!("../../../../assets/icons/constrain/normal.svg")), event: ModuleEvent::Command("NRCONSTRAINT".to_string()), } } @@ -112,7 +112,7 @@ pub mod colinear { ToolDef { id: "LCONSTRAINT", label: "Colinear", - icon: IconKind::Glyph("L"), + icon: IconKind::Svg(include_bytes!("../../../../assets/icons/constrain/colinear.svg")), event: ModuleEvent::Command("LCONSTRAINT".to_string()), } } @@ -124,7 +124,7 @@ pub mod fixed { ToolDef { id: "FXCONSTRAINT", label: "Fixed", - icon: IconKind::Glyph("F"), + icon: IconKind::Svg(include_bytes!("../../../../assets/icons/constrain/fixed.svg")), event: ModuleEvent::Command("FXCONSTRAINT".to_string()), } } @@ -136,7 +136,7 @@ pub mod symmetric { ToolDef { id: "SYCONSTRAINT", label: "Symmetric", - icon: IconKind::Glyph("S"), + icon: IconKind::Svg(include_bytes!("../../../../assets/icons/constrain/symmetric.svg")), event: ModuleEvent::Command("SYCONSTRAINT".to_string()), } } diff --git a/src/modules/draw/constrain/value.rs b/src/modules/draw/constrain/value.rs index 5b7363f75..8809d284f 100644 --- a/src/modules/draw/constrain/value.rs +++ b/src/modules/draw/constrain/value.rs @@ -57,7 +57,7 @@ pub mod distance_tool { ToolDef { id: "DCONSTRAINT", label: "Distance", - icon: IconKind::Glyph("↔"), + icon: IconKind::Svg(include_bytes!("../../../../assets/icons/constrain/distance.svg")), event: ModuleEvent::Command("DCONSTRAINT".to_string()), } } @@ -69,7 +69,7 @@ pub mod angle_tool { ToolDef { id: "ACONSTRAINT", label: "Angle", - icon: IconKind::Glyph("∠"), + icon: IconKind::Svg(include_bytes!("../../../../assets/icons/constrain/angle.svg")), event: ModuleEvent::Command("ACONSTRAINT".to_string()), } } diff --git a/src/scene/model/object.rs b/src/scene/model/object.rs index 2d0eee684..99bd2e6c3 100644 --- a/src/scene/model/object.rs +++ b/src/scene/model/object.rs @@ -1,6 +1,6 @@ // Shared value types used by the dispatch and grip systems. -use acadrust::types::{Color as AcadColor, LineWeight}; +use acadrust::types::{Color as AcadColor, Handle, LineWeight}; use glam::DVec3; /// The kind of value held by a property row. @@ -58,6 +58,28 @@ pub enum PropValue { /// an editable text_input. Unlike the other rows the routing key is the /// tag carried here, not the row's `&'static str` field. AttrText { tag: String, value: String }, + /// A clickable link to one or more entities — the Constraints section's + /// row for one persistent sketch constraint. Clicking it selects every + /// entity in `handles` in the viewport. `conflicting` mirrors + /// `SketchConstraintSet::conflicts`, tinting the row the same danger + /// color the constraint's glyph pill already uses. + EntityLink { handles: Vec, conflicting: bool }, + /// One row of the document-wide named-parameter table (Parameters + /// section, shown when nothing is selected). `index` is the row's + /// position in `ParameterTable::iter()` order — stable across edits to + /// other rows, used to route `PropParamInput`/`PropParamCommit`/ + /// `PropParamDelete` back to the right parameter without re-keying on a + /// name that might itself be mid-edit. `resolved` is the live value or + /// error, recomputed fresh every render (same "cheap enough to rebuild" + /// approach the old modal's preview column already used). + ParamRow { + index: usize, + name: String, + formula: String, + resolved: Result, + }, + /// The trailing "+ Add parameter" row in the Parameters section. + ParamAddRow, } /// A single property row in the Properties panel. diff --git a/src/scene/selection.rs b/src/scene/selection.rs index 42e6e8888..a98c7e119 100644 --- a/src/scene/selection.rs +++ b/src/scene/selection.rs @@ -735,7 +735,10 @@ impl Scene { PropValue::Stepper { .. } | PropValue::ColorVaries | PropValue::LwVaries - | PropValue::FieldLwVaries { .. } => continue, + | PropValue::FieldLwVaries { .. } + | PropValue::EntityLink { .. } + | PropValue::ParamRow { .. } + | PropValue::ParamAddRow => continue, } }; out.push(choice(prop.field, prop.label, editor)); @@ -883,7 +886,10 @@ impl Scene { PropValue::Stepper { display, .. } => display, PropValue::ColorVaries | PropValue::LwVaries - | PropValue::FieldLwVaries { .. } => return None, + | PropValue::FieldLwVaries { .. } + | PropValue::EntityLink { .. } + | PropValue::ParamRow { .. } + | PropValue::ParamAddRow => return None, }) } } diff --git a/src/ui/properties.rs b/src/ui/properties.rs index 9033adb99..376a0d8b1 100644 --- a/src/ui/properties.rs +++ b/src/ui/properties.rs @@ -267,6 +267,10 @@ pub enum FieldKey { Geom(&'static str), /// A block attribute value field, keyed by its tag. Attr(String), + /// One column of one named-parameter row, keyed by its stable + /// `ParameterTable::iter()` position (see `PropValue::ParamRow`'s doc + /// comment for why index rather than name). + Param(usize, crate::ui::window::named_parameters::ParamField), } impl FieldKey { @@ -277,10 +281,21 @@ impl FieldKey { match self { FieldKey::Geom(field) => prop_geom_field_id(field), FieldKey::Attr(tag) => prop_attr_field_id(tag), + FieldKey::Param(index, field) => prop_param_field_id(*index, *field), } } } +/// Text-input widget id for one column of a named-parameter row. +pub fn prop_param_field_id(index: usize, field: crate::ui::window::named_parameters::ParamField) -> iced::widget::Id { + use crate::ui::window::named_parameters::ParamField; + let suffix = match field { + ParamField::Name => "name", + ParamField::Formula => "formula", + }; + iced::widget::Id::from(format!("props-param-field-{index}-{suffix}")) +} + /// Edit-buffer / active-field key for a block attribute value, keyed by its /// tag. Kept in one place so the live-input handler and the row renderer agree. pub fn attr_edit_key(tag: &str) -> FieldKey { @@ -338,6 +353,13 @@ pub fn build_field_key_map( if let Some(key) = key { map.insert(key.widget_id(), key); } + if let PropValue::ParamRow { index, .. } = &prop.value { + use crate::ui::window::named_parameters::ParamField; + for field in [ParamField::Name, ParamField::Formula] { + let key = FieldKey::Param(*index, field); + map.insert(key.widget_id(), key); + } + } } } map @@ -842,6 +864,13 @@ impl PropertiesPanel { self.render_hatch_pattern_row(label, current) } PropValue::AttrText { tag, value } => self.render_attr_row(tag, value), + PropValue::EntityLink { handles, conflicting } => { + render_entity_link_row(label, handles.clone(), *conflicting) + } + PropValue::ParamRow { index, name, formula, resolved } => { + self.render_param_row(*index, name, formula, resolved) + } + PropValue::ParamAddRow => render_param_add_row(), } } @@ -1367,6 +1396,96 @@ impl PropertiesPanel { prop_row_with_active(tag, ti.into(), active) } + /// One named-parameter row: editable name + formula, with the live- + /// resolved value (or error) shown alongside — the "AutoCAD style" the + /// Parameters section is modeled on, embedded in the panel instead of + /// the old separate modal. Each field commits on submit (Enter / losing + /// focus), not per keystroke — `ParameterTable::set` validates + /// immediately and would otherwise reject a formula mid-type (the same + /// reasoning the old modal's buffered-Apply design used, just applied + /// per-field instead of per-whole-table). + fn render_param_row<'a>( + &'a self, + index: usize, + name: &'a str, + formula: &'a str, + resolved: &'a Result, + ) -> Element<'a, Message> { + use crate::ui::window::named_parameters::ParamField; + + let name_key = FieldKey::Param(index, ParamField::Name); + let name_display = self.edit_buf.get(&name_key).map(|s| s.as_str()).unwrap_or(name); + let name_active = self.active_field.as_ref() == Some(&name_key); + let name_input = text_input("name", name_display) + .id(name_key.widget_id()) + .on_input(move |v| Message::PropParamInput { index, field: ParamField::Name, value: v }) + .on_submit(Message::PropParamCommit { index, field: ParamField::Name }) + .size(FONT_SZ) + .style(text_input_style) + .padding([3, 6]) + .width(Length::FillPortion(3)); + + let formula_key = FieldKey::Param(index, ParamField::Formula); + let formula_display = self.edit_buf.get(&formula_key).map(|s| s.as_str()).unwrap_or(formula); + let formula_active = self.active_field.as_ref() == Some(&formula_key); + let formula_input = text_input("formula", formula_display) + .id(formula_key.widget_id()) + .on_input(move |v| Message::PropParamInput { index, field: ParamField::Formula, value: v }) + .on_submit(Message::PropParamCommit { index, field: ParamField::Formula }) + .size(FONT_SZ) + .style(text_input_style) + .padding([3, 6]) + .width(Length::FillPortion(3)); + + let (value_text, is_error) = match resolved { + Ok(v) => (format!("{v:.4}"), false), + Err(_) => ("—".to_string(), true), + }; + let value_label = container( + text(value_text).size(FONT_SZ).style(move |theme: &Theme| iced::widget::text::Style { + color: is_error.then_some(theme.palette().danger.base.color), + }), + ) + .width(Length::FillPortion(2)) + .align_x(iced::Right); + + let delete_btn = button(text("\u{2715}").size(FONT_SZ)) + .on_press(Message::PropParamDelete(index)) + .style(button::text) + .padding([2, 6]); + + let active = name_active || formula_active; + let bg = move |theme: &Theme| { + if active { + Background::Color(theme.palette().primary.weak.color) + } else { + Background::Color(theme.palette().background.base.color) + } + }; + let content = container(row![name_input, formula_input, value_label, delete_btn].spacing(4).align_y(iced::Center)) + .style(move |theme: &Theme| container::Style { background: Some(bg(theme)), ..Default::default() }) + .padding([2, 6]) + .width(Length::Fill); + + if let Err(err) = resolved { + tooltip(content, text(err.as_str()).size(FONT_SZ), tooltip::Position::Top) + .gap(4.0) + .padding(6.0) + .style(|theme: &Theme| { + let palette = theme.palette(); + container::Style { + background: Some(Background::Color(palette.danger.weak.color)), + text_color: Some(palette.danger.weak.text), + border: Border { color: palette.danger.base.color, width: 1.0, radius: 4.0.into() }, + ..Default::default() + } + }) + .into() + } else { + content.into() + } + } + fn render_hatch_pattern_row<'a>( &'a self, label: &'a str, @@ -1805,6 +1924,55 @@ fn render_ro_with_tooltip_row<'a>( prop_row_widget(label, wrapped.into()) } +// ── Constraints section row (clickable entity link) ─────────────────────── + +/// One persistent-constraint row in the Constraints properties section: +/// `label` is the glyph + kind name + resolved value (e.g. "↔ Distance: +/// hole_dia = 12.00"), rendered as the row's own clickable surface — there's +/// no natural separate "value" for a constraint row, unlike a Layer/Color +/// field, so this doesn't use the usual `prop_row_widget` label|value split. +/// Clicking it selects every entity in `handles`; a conflicting/redundant +/// constraint (mirrors the viewport glyph pill's own color cue) tints red. +fn render_entity_link_row<'a>(label: &'a str, handles: Vec, conflicting: bool) -> Element<'a, Message> { + let btn = button(text(label).size(FONT_SZ).width(Length::Fill)) + .on_press(Message::PropConstraintLinkClick(handles)) + .style(move |theme: &Theme, status| { + let palette = theme.palette(); + let pair = if conflicting { + palette.danger.weak + } else { + match status { + button::Status::Hovered | button::Status::Pressed => palette.background.weak, + _ => palette.background.base, + } + }; + button::Style { + background: Some(Background::Color(pair.color)), + border: Border { color: palette.background.neutral.color, width: 1.0, radius: 2.0.into() }, + text_color: pair.text, + ..Default::default() + } + }) + .padding([3, 8]) + .width(Length::Fill); + container(btn).width(Length::Fill).into() +} + +// ── Parameters section: "+ Add parameter" row ────────────────────────────── + +/// The trailing "+ Add parameter" row: appends a fresh, uniquely-named +/// parameter (`param1`, `param2`, …) the user then renames/redefines inline +/// — avoids a separate "pending new row" concept, since every row always +/// reflects a real committed table entry. +fn render_param_add_row<'a>() -> Element<'a, Message> { + let btn = button(row![text("+").size(FONT_SZ), text(t!("Add parameter").into_owned()).size(FONT_SZ)].spacing(6).align_y(iced::Center)) + .on_press(Message::PropParamAddNew) + .style(button::text) + .padding([4, 8]) + .width(Length::Fill); + container(btn).width(Length::Fill).into() +} + /// Build a label | widget property row. fn prop_row_widget<'a>(label: &'a str, widget: Element<'a, Message>) -> Element<'a, Message> { prop_row_with_active(label, widget, false) diff --git a/src/ui/window/named_parameters.rs b/src/ui/window/named_parameters.rs index bfe494f02..1cff42bf9 100644 --- a/src/ui/window/named_parameters.rs +++ b/src/ui/window/named_parameters.rs @@ -88,7 +88,7 @@ fn used_by_cell<'a>(scene: &Scene, name: &str) -> Element<'a, Message> { } /// Which column of a parameter row a text edit targets. -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum ParamField { Name, Formula, diff --git a/src/ui/window/options.rs b/src/ui/window/options.rs index 1aacbcb93..a5ffa6a22 100644 --- a/src/ui/window/options.rs +++ b/src/ui/window/options.rs @@ -51,6 +51,7 @@ pub fn view_window<'a>( default_save_format: &'a str, file_assoc_enabled: bool, write_dwg_native_constraints: bool, + show_constraint_values: bool, ui_theme: &'a UiThemeConfig, theme_color_inputs: &'a [String; 6], language: crate::i18n::Language, @@ -230,6 +231,23 @@ pub fn view_window<'a>( )) .size(11) .width(sizing.width), + Space::new().height(14), + row![ + iced::widget::checkbox(show_constraint_values) + .on_toggle(Message::ShowConstraintValuesChanged) + .size(15), + text(crate::t!("Show values and parameter names on constraint markers")).size(12), + ] + .spacing(8) + .align_y(iced::Center), + Space::new().height(6), + text(crate::t!( + "On by default. Turn off to show just the constraint glyph in the viewport — \ + enough to see that a constraint is present — without the driven value or named \ + parameter text covering nearby geometry." + )) + .size(11) + .width(sizing.width), ] .spacing(0) .width(sizing.width);