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 <noreply@anthropic.com>
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

+8
View File
@@ -1627,6 +1627,13 @@ pub struct Scene {
/// Currently selected entity handles.
pub selected: HashSet<Handle>,
selected_order: Vec<Handle>,
/// The sketch-constraint glyph pill currently selected in the viewport
/// (click-to-select — see `sketch_constraints::constraint_glyph_hit`),
/// independent of `selected`'s entity handles. Cleared whenever entity
/// selection changes so the two stay mutually exclusive; validated
/// against the current scope's constraint set at use, since switching
/// sketch scope can leave a stale id behind.
pub selected_constraint: Option<crate::scene::sketch_constraints::ConstraintId>,
/// Session-only ISOLATEOBJECTS / HIDEOBJECTS state. Never written to DWG/DXF.
pub object_isolation: ObjectIsolationState,
/// Entity handles temporarily removed from the base render while an
@@ -2171,6 +2178,7 @@ impl Scene {
lighting_cache: RefCell::new(HashMap::default()),
selected: HashSet::default(),
selected_order: Vec::new(),
selected_constraint: None,
object_isolation: ObjectIsolationState::default(),
preview_hidden: HashSet::default(),
command_preview_hidden: HashSet::default(),
+7 -2
View File
@@ -62,8 +62,13 @@ pub enum PropValue {
/// 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 },
/// color the constraint's glyph pill already uses. `id` routes the
/// row's own delete button back to this specific constraint.
EntityLink {
id: crate::scene::sketch_constraints::ConstraintId,
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
+4
View File
@@ -72,6 +72,7 @@ impl Scene {
if handles.is_empty() {
return;
}
self.selected_constraint = None;
let expanded = self.expanded_with_leaders(handles);
let mut changed = false;
for handle in expanded {
@@ -111,6 +112,7 @@ impl Scene {
expanded
}
pub fn select_entity(&mut self, handle: Handle, exclusive: bool) {
self.selected_constraint = None;
let handles = self.handles_expanded_for_leader_annotations(&[handle]);
let mut changed = false;
@@ -133,6 +135,7 @@ impl Scene {
}
pub fn deselect_all(&mut self) {
self.selected_constraint = None;
if self.selected.is_empty() {
return;
}
@@ -215,6 +218,7 @@ impl Scene {
/// only when its contents actually changed. History/file/command paths must
/// use this instead of assigning `selected` directly.
pub(crate) fn replace_selection(&mut self, selected: HashSet<Handle>) {
self.selected_constraint = None;
let handles: Vec<Handle> = selected.iter().copied().collect();
let selected: HashSet<Handle> = self
.handles_expanded_for_leader_annotations(&handles)
+92
View File
@@ -640,6 +640,98 @@ impl super::Scene {
}
}
/// Screen-projected sketch-constraint glyph placements for `scope`:
/// `(id, anchor, outward_screen_direction, label, is_conflicting)` for
/// every enabled, visible constraint whose glyph projects on-screen.
/// `vp_size` is the full canvas size (as `SelectionState::vp_size`
/// reports it), matching what `viewport_edit_frame`/
/// `active_model_tile_bounds` expect. Mirrors the projection
/// `crate::app::view` builds its own render list with, and is reused by
/// [`constraint_glyph_hit`](Self::constraint_glyph_hit) — both feed the
/// same `(anchor, outward, label)` triples into
/// `crate::ui::overlay::constraint_glyph_box`/`constraint_glyph_offsets`,
/// so hit-testing can never drift from what's actually drawn.
pub fn constraint_glyph_placements_screen(
&self,
scope: SketchScope,
vp_size: (f32, f32),
show_values: bool,
) -> Vec<(ConstraintId, iced::Point, [f32; 2], String, bool)> {
let Some(set) = self.sketch_constraint_set(scope) else {
return Vec::new();
};
if set.constraints.is_empty() {
return Vec::new();
}
let edit_frame = self.viewport_edit_frame(vp_size);
let bounds = match &edit_frame {
Some((_, full)) => *full,
None => self.active_model_tile_bounds(vp_size.0, vp_size.1),
};
let (view_rot, eye) = if let Some((cam, _)) = &edit_frame {
(cam.view_proj_rte(bounds), cam.eye())
} else {
let cam = self.camera.borrow();
(cam.view_proj_rte(bounds), cam.eye())
};
set.constraints
.iter()
.filter(|c| c.enabled)
.filter(|c| self.is_sketch_constraint_visible(scope, c.id))
.filter_map(|c| {
let (anchor, outward) = glyph_placement(&self.document, c)?;
let screen = crate::scene::pick::grip::project_rte(
glam::DVec3::new(anchor.x, anchor.y, anchor.z),
view_rot,
eye,
bounds,
)?;
let outward_screen = crate::scene::pick::grip::project_rte(
glam::DVec3::new(
anchor.x + outward.x,
anchor.y + outward.y,
anchor.z + outward.z,
),
view_rot,
eye,
bounds,
)?;
let direction = (outward_screen - screen).normalize_or(glam::Vec2::NEG_Y);
let point = iced::Point::new(bounds.x + screen.x, bounds.y + screen.y);
let is_conflicting = set.conflicts.iter().any(|(id, _)| *id == c.id);
let label = if show_values {
glyph_label(c)
} else {
c.kind.glyph_symbol().to_string()
};
point.x.is_finite().then_some((c.id, point, direction.to_array(), label, is_conflicting))
})
.collect()
}
/// Hit-tests screen point `p` (same coordinate space as `p_full` in the
/// viewport click handler) against the glyph pills from
/// [`constraint_glyph_placements_screen`](Self::constraint_glyph_placements_screen),
/// via `crate::ui::overlay::constraint_glyph_hit_test`'s shared layout
/// math, so a click only registers where the pill is actually drawn.
pub fn constraint_glyph_hit(
&self,
scope: SketchScope,
vp_size: (f32, f32),
show_values: bool,
p: iced::Point,
) -> Option<ConstraintId> {
let placements = self.constraint_glyph_placements_screen(scope, vp_size, show_values);
let glyphs: Vec<(iced::Point, [f32; 2], String, bool)> = placements
.iter()
.map(|(_, point, direction, label, is_conflicting)| {
(*point, *direction, label.clone(), *is_conflicting)
})
.collect();
let index = crate::ui::overlay::constraint_glyph_hit_test(&glyphs, p)?;
Some(placements[index].0)
}
/// Handle remapping lives in each command that duplicates
/// entities — `Scene::copy_entities`' `handle_map` (COPY/ARRAY/MIRROR,
/// `src/scene/modify.rs`) and `OpenCADStudio::finalize_paste`'s own