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

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