Add per-constraint delete; fix named-parameter reference bugs

Constraint delete: a Properties-panel row delete button, viewport
click-to-select on a constraint glyph pill with Delete-key support, and a
right-click "Delete Constraint" menu item — all routed through one
undoable Scene::selected_constraint-aware path.

Fixes two named-parameter bugs found while testing that feature:
- DIMCONSTRAINT: a parameter named like a mode-switch keyword ("r", "d",
  "a", "x", "y") could never be referenced, since the keyword check ran
  before the parameter-name check.
- PARAMETERS modal Apply rebuilt Scene::named_parameters but never
  refreshed the Properties panel, leaving renamed/deleted rows stale
  until an unrelated refresh (e.g. a selection change) happened to occur.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
Felix RiestraandClaude Sonnet 5 committed 2026-09-15 01:08:34 +02:00
1 parent 753df67ee4
commit 59ae38ae1a
14 files changed
+370 -77

No files matched your search

+55 -6
View File
@@ -94,6 +94,34 @@ fn constraint_glyph_offsets(glyphs: &[(Point, [f32; 2], String, bool)]) -> Vec<f
offsets
}
/// Hit-tests screen point `p` against the same glyph-pill layout `draw`
/// renders — reusing `constraint_glyph_offsets`/`constraint_glyph_box` so
/// the clickable area can never drift from what's actually drawn, including
/// the tangential fan-out applied when several glyphs share one anchor.
/// Returns the index into `glyphs` of the topmost (last-drawn) match.
/// `Scene::constraint_glyph_hit` (`src/scene/sketch_constraints.rs`) is the
/// only caller — it maps the index back to a `ConstraintId`, since this
/// plain-data layout math has no reason to know about that scene-layer type.
pub(crate) fn constraint_glyph_hit_test(
glyphs: &[(Point, [f32; 2], String, bool)],
p: Point,
) -> Option<usize> {
let offsets = constraint_glyph_offsets(glyphs);
glyphs
.iter()
.zip(offsets)
.enumerate()
.rev()
.find_map(|(index, ((anchor, outward, label, _), tangent_offset))| {
let (top_left, size) = constraint_glyph_box(*anchor, *outward, label, tangent_offset);
let within = p.x >= top_left.x
&& p.x <= top_left.x + size.width
&& p.y >= top_left.y
&& p.y <= top_left.y + size.height;
within.then_some(index)
})
}
/// Convert CURSORSIZE to a screen-space arm length while keeping the original
/// 60 px cursor at the default value and the full-viewport result at 100.
pub(crate) fn crosshair_arm_px(bounds: iced::Rectangle, value: i32) -> f32 {
@@ -723,7 +751,7 @@ pub fn selection_overlay<'a>(
crosshair_bg: [f32; 4],
crosshair: CrosshairOptions,
selection_visual: SelectionVisualOptions,
constraint_glyphs: Vec<(Point, [f32; 2], String, bool)>,
constraint_glyphs: Vec<(Point, [f32; 2], String, bool, bool)>,
) -> Element<'a, Message> {
canvas(SelectionCanvas {
selection,
@@ -806,8 +834,10 @@ struct SelectionCanvas {
crosshair_bg: [f32; 4],
crosshair: CrosshairOptions,
selection_visual: SelectionVisualOptions,
/// Constraint glyph anchor, outward screen direction, label, and conflict state.
constraint_glyphs: Vec<(Point, [f32; 2], String, bool)>,
/// Constraint glyph anchor, outward screen direction, label, conflict
/// state, and whether the pill itself is the current click-to-select
/// target (`Scene::selected_constraint`) — for the current scope.
constraint_glyphs: Vec<(Point, [f32; 2], String, bool, bool)>,
}
fn draw_grip_marker(
@@ -1703,9 +1733,21 @@ impl canvas::Program<Message> for SelectionCanvas {
frame.stroke(&b1, stroke.clone());
frame.stroke(&b2, stroke);
}
// Constraint glyphs are visual-only and have no hit testing.
// Click-to-select hit testing against these pills lives in
// `Scene::constraint_glyph_hit`, sharing this same size formula so
// the clickable area never drifts from what's drawn here.
if !self.constraint_glyphs.is_empty() {
let offsets = constraint_glyph_offsets(&self.constraint_glyphs);
// `constraint_glyph_offsets` only needs the anchor/outward/label/
// conflict quadruple it was written against; project away the
// trailing `is_selected` flag rather than widen its signature.
let glyphs_for_offsets: Vec<(Point, [f32; 2], String, bool)> = self
.constraint_glyphs
.iter()
.map(|(anchor, outward, label, is_conflicting, _)| {
(*anchor, *outward, label.clone(), *is_conflicting)
})
.collect();
let offsets = constraint_glyph_offsets(&glyphs_for_offsets);
let normal_bg = theme.palette().primary.base.color;
let normal_fg = theme.palette().primary.base.text;
// A redundant or conflicting constraint gets the danger palette
@@ -1714,7 +1756,8 @@ impl canvas::Program<Message> for SelectionCanvas {
// would show, surfaced right on the geometry.
let conflict_bg = theme.palette().danger.base.color;
let conflict_fg = theme.palette().danger.base.text;
for ((anchor, outward, label, is_conflicting), tangent_offset) in
let selected_ring = theme.palette().primary.strong.color;
for ((anchor, outward, label, is_conflicting, is_selected), tangent_offset) in
self.constraint_glyphs.iter().zip(offsets)
{
if !anchor.x.is_finite() || !anchor.y.is_finite() {
@@ -1729,6 +1772,12 @@ impl canvas::Program<Message> for SelectionCanvas {
(size.height * 0.5).into(),
);
frame.fill(&pill, bg);
if *is_selected {
frame.stroke(
&pill,
canvas::Stroke::default().with_color(selected_ring).with_width(2.0),
);
}
frame.fill_text(canvas::Text {
content: label.clone(),
position: Point::new(
+18 -5
View File
@@ -865,8 +865,8 @@ 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::EntityLink { id, handles, conflicting } => {
render_entity_link_row(label, *id, handles.clone(), *conflicting)
}
PropValue::ParamRow { index, name, formula, resolved } => {
self.render_param_row(*index, name, formula, resolved)
@@ -1933,8 +1933,15 @@ fn render_ro_with_tooltip_row<'a>(
/// 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))
/// A trailing `✕` deletes the constraint outright (undoable), matching
/// `PropParamDelete`'s own row-delete button in the Parameters section.
fn render_entity_link_row<'a>(
label: &'a str,
id: crate::scene::sketch_constraints::ConstraintId,
handles: Vec<Handle>,
conflicting: bool,
) -> Element<'a, Message> {
let link_btn = button(text(label).size(FONT_SZ).width(Length::Fill))
.on_press(Message::PropConstraintLinkClick(handles))
.style(move |theme: &Theme, status| {
let palette = theme.palette();
@@ -1955,7 +1962,13 @@ fn render_entity_link_row<'a>(label: &'a str, handles: Vec<Handle>, conflicting:
})
.padding([3, 8])
.width(Length::Fill);
container(btn).width(Length::Fill).into()
let delete_btn = button(text("\u{2715}").size(FONT_SZ))
.on_press(Message::PropConstraintDelete(id))
.style(button::text)
.padding([2, 6]);
container(row![link_btn, delete_btn].spacing(2).align_y(iced::Center))
.width(Length::Fill)
.into()
}
// ── Parameters section: "+ Add parameter" row ──────────────────────────────