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);