Add Properties-panel Constraints/Parameters sections, a constraint-pill

value toggle, and redesigned constraint-ribbon icons

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 <[email protected]>
This commit is contained in:
Felix RiestraandClaude Sonnet 5 committed 2026-09-10 23:16:11 +02:00
1 parent faf3473b2b
commit bd9a2d08f7
36 files changed
+707 -26

No files matched your search

+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="64" height="64">
<line x1="4" y1="20" x2="20" y2="20" stroke="#B4B6B9" stroke-width="2.25" stroke-linecap="round"/>
<line x1="4" y1="20" x2="16" y2="5" stroke="#6DB7ED" stroke-width="2.25" stroke-linecap="round"/>
<path d="M 11 20 A 7 7 0 0 0 8.4 14.6" stroke="#B4B6B9" stroke-width="1.5" fill="none" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 406 B

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="64" height="64">
<circle cx="12" cy="12" r="8" stroke="#B4B6B9" stroke-width="2.25" fill="none"/>
<circle cx="12" cy="12" r="2.2" fill="#6DB7ED"/>
</svg>

After

Width:  |  Height:  |  Size: 225 B

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="64" height="64">
<circle cx="9" cy="12" r="3.4" stroke="#B4B6B9" stroke-width="2" fill="none"/>
<circle cx="14" cy="12" r="3.4" fill="#6DB7ED"/>
</svg>

After

Width:  |  Height:  |  Size: 223 B

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="64" height="64">
<line x1="2" y1="12" x2="9" y2="12" stroke="#B4B6B9" stroke-width="2.25" stroke-linecap="round"/>
<line x1="13" y1="12" x2="22" y2="12" stroke="#6DB7ED" stroke-width="2.25" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 293 B

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="64" height="64">
<circle cx="12" cy="12" r="9" stroke="#B4B6B9" stroke-width="2.25" fill="none"/>
<circle cx="12" cy="12" r="4" stroke="#6DB7ED" stroke-width="2.25" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 257 B

+7
View File
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="64" height="64">
<line x1="4" y1="6" x2="4" y2="18" stroke="#B4B6B9" stroke-width="1.75" stroke-linecap="round"/>
<line x1="20" y1="6" x2="20" y2="18" stroke="#B4B6B9" stroke-width="1.75" stroke-linecap="round"/>
<line x1="4" y1="12" x2="20" y2="12" stroke="#6DB7ED" stroke-width="2.25" stroke-linecap="round"/>
<path d="M 4 12 L 8 9 M 4 12 L 8 15" stroke="#6DB7ED" stroke-width="2" fill="none" stroke-linecap="round"/>
<path d="M 20 12 L 16 9 M 20 12 L 16 15" stroke="#6DB7ED" stroke-width="2" fill="none" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 616 B

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="64" height="64">
<line x1="5" y1="9" x2="19" y2="9" stroke="#B4B6B9" stroke-width="2.25" stroke-linecap="round"/>
<line x1="5" y1="15" x2="19" y2="15" stroke="#6DB7ED" stroke-width="2.25" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 291 B

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="64" height="64">
<line x1="3" y1="7" x2="13" y2="7" stroke="#B4B6B9" stroke-width="2" stroke-linecap="round"/>
<circle cx="3" cy="7" r="1.8" fill="#B4B6B9"/>
<circle cx="13" cy="7" r="1.8" fill="#B4B6B9"/>
<line x1="9" y1="18" x2="19" y2="18" stroke="#6DB7ED" stroke-width="2" stroke-linecap="round"/>
<circle cx="9" cy="18" r="1.8" fill="#6DB7ED"/>
<circle cx="19" cy="18" r="1.8" fill="#6DB7ED"/>
</svg>

After

Width:  |  Height:  |  Size: 485 B

+7
View File
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="64" height="64">
<line x1="4" y1="8" x2="20" y2="8" stroke="#6DB7ED" stroke-width="2.25" stroke-linecap="round"/>
<line x1="6" y1="8" x2="3" y2="13" stroke="#B4B6B9" stroke-width="1.6" stroke-linecap="round"/>
<line x1="11" y1="8" x2="8" y2="13" stroke="#B4B6B9" stroke-width="1.6" stroke-linecap="round"/>
<line x1="16" y1="8" x2="13" y2="13" stroke="#B4B6B9" stroke-width="1.6" stroke-linecap="round"/>
<line x1="21" y1="8" x2="18" y2="13" stroke="#B4B6B9" stroke-width="1.6" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 587 B

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="64" height="64">
<line x1="4" y1="12" x2="20" y2="12" stroke="#6DB7ED" stroke-width="2.25" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 192 B

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="64" height="64">
<line x1="3" y1="19" x2="21" y2="5" stroke="#B4B6B9" stroke-width="2.25" stroke-linecap="round"/>
<circle cx="12" cy="12" r="2.2" fill="#6DB7ED"/>
</svg>

After

Width:  |  Height:  |  Size: 242 B

+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="64" height="64">
<circle cx="10" cy="10" r="6" stroke="#B4B6B9" stroke-width="2.25" fill="none"/>
<line x1="10" y1="10" x2="21" y2="21" stroke="#6DB7ED" stroke-width="2.25" stroke-linecap="round"/>
<circle cx="14.24" cy="14.24" r="1.8" fill="#6DB7ED"/>
</svg>

After

Width:  |  Height:  |  Size: 333 B

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="64" height="64">
<line x1="5" y1="19" x2="13" y2="5" stroke="#B4B6B9" stroke-width="2.25" stroke-linecap="round"/>
<line x1="11" y1="19" x2="19" y2="5" stroke="#6DB7ED" stroke-width="2.25" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 292 B

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="64" height="64">
<line x1="4" y1="17" x2="20" y2="17" stroke="#B4B6B9" stroke-width="2.25" stroke-linecap="round"/>
<line x1="12" y1="4" x2="12" y2="17" stroke="#6DB7ED" stroke-width="2.25" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 293 B

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="64" height="64">
<path d="M 4 18 A 11 11 0 0 1 20 6" stroke="#B4B6B9" stroke-width="2" fill="none" stroke-linecap="round"/>
<circle cx="8" cy="7" r="2.2" fill="#6DB7ED"/>
</svg>

After

Width:  |  Height:  |  Size: 249 B

+6
View File
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="64" height="64">
<line x1="12" y1="3" x2="12" y2="21" stroke="#6DB7ED" stroke-width="2.25" stroke-linecap="round"/>
<line x1="7" y1="12" x2="17" y2="12" stroke="#B4B6B9" stroke-width="1.6" stroke-linecap="round"/>
<circle cx="5" cy="12" r="2" fill="#B4B6B9"/>
<circle cx="19" cy="12" r="2" fill="#B4B6B9"/>
</svg>

After

Width:  |  Height:  |  Size: 389 B

+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="64" height="64">
<circle cx="9" cy="9" r="6" stroke="#B4B6B9" stroke-width="2.25" fill="none"/>
<line x1="2" y1="15" x2="21" y2="15" stroke="#6DB7ED" stroke-width="2.25" stroke-linecap="round"/>
<circle cx="9" cy="15" r="1.8" fill="#6DB7ED"/>
</svg>

After

Width:  |  Height:  |  Size: 323 B

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="64" height="64">
<line x1="12" y1="4" x2="12" y2="20" stroke="#6DB7ED" stroke-width="2.25" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 192 B

+215
View File
@@ -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<Handle> = 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<Message> {
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(&current.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(&current.name);
let outcome = self.tabs[i].scene.named_parameters_mut().set(&typed, &current.source);
if outcome.is_err() {
let _ = self.tabs[i].scene.named_parameters_mut().set(&current.name, &current.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<Message> {
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<Message> {
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<Message> {
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<Handle> =
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
+36
View File
@@ -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<acadrust::Handle>),
// ── 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,
+88
View File
@@ -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<Property> = 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<crate::scene::model::object::Property> = 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<acadrust::Handle> = 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);
+11
View File
@@ -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<i32, D::Error>
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,
+2
View File
@@ -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();
+27
View File
@@ -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();
+6 -4
View File
@@ -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()
}
+1
View File
@@ -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,
+1 -1
View File
@@ -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()),
}
}
+1 -1
View File
@@ -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()),
}
}
@@ -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()),
}
}
+11 -11
View File
@@ -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()),
}
}
+2 -2
View File
@@ -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()),
}
}
+23 -1
View File
@@ -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<Handle>, 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<f64, String>,
},
/// The trailing "+ Add parameter" row in the Parameters section.
ParamAddRow,
}
/// A single property row in the Properties panel.
+8 -2
View File
@@ -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,
})
}
}
+168
View File
@@ -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<f64, String>,
) -> 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<Handle>, 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)
+1 -1
View File
@@ -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,
+18
View File
@@ -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);