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]>
No files matched your search
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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(¤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<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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||