feat(layers): enforce layer lock (select/modify/fade + cursor badge)

Locked-layer objects were fully editable. Now they stay visible and
snappable but are otherwise protected:

- Not selectable: the five interactive pick paths (box, lasso, single
  click, cycling, box-release) plus SELECTALL and QSELECT skip locked
  entities.
- Not modifiable (defense-in-depth even if a handle slips through):
  transform_entities, erase_entities, apply_grip and the Properties
  commit all skip locked entities.
- Faded: entities on a locked layer render dimmed toward the background
  (render_style + wire tessellation); toggling lock re-tessellates.
- Snap still works: locked entities are intentionally kept in the wire
  set (visibility_ok untouched), so object snap keeps finding them.
- Feedback: a small padlock is drawn by the crosshair when hovering a
  locked object, and clicking one prints "Object is on locked layer …".

New Scene::is_layer_locked / locked_layer_name helpers centralise the
check (mirroring layer_hidden).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-07-01 13:10:39 +03:00
1 parent c71950fa7c
commit 56e1c03ac2
11 files changed
+149 -2

No files matched your search

+2
View File
@@ -16,6 +16,8 @@ impl OpenCADStudio {
.entity_wires()
.iter()
.filter_map(|w| Scene::handle_from_wire_name(&w.name))
// Objects on a locked layer aren't selectable.
.filter(|&h| !self.tabs[i].scene.is_layer_locked(h))
.collect();
let count = handles.len();
for h in handles {
+4
View File
@@ -1153,6 +1153,10 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
}
} else {
for &handle in &handles {
// Skip objects on a locked layer.
if self.tabs[i].scene.is_layer_locked(handle) {
continue;
}
if let Some(entity) =
self.tabs[i].scene.document.get_entity_mut(handle)
{
+16 -1
View File
@@ -2133,6 +2133,8 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task<Message> {
handles.extend(self.tabs[i].scene.block_mesh_box_hit(
a, p, crossing, view_rot, eye, bounds,
));
// Objects on a locked layer aren't selectable.
handles.retain(|&h| !self.tabs[i].scene.is_layer_locked(h));
// Box/lasso accumulates like individual picks
// (issue #83): a plain box adds to the current
// selection, Shift+box removes the boxed
@@ -2193,6 +2195,8 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task<Message> {
));
// Selection filter: keep only allowed types.
handles.retain(|&h| self.tabs[i].scene.passes_selection_filter(h));
// Objects on a locked layer aren't selectable.
handles.retain(|&h| !self.tabs[i].scene.is_layer_locked(h));
// Accumulate like the box path (issue #83): plain
// lasso adds, Shift+lasso removes. An empty lasso
// leaves the current selection untouched so a stray
@@ -2238,6 +2242,7 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task<Message> {
.into_iter()
.filter_map(|s| Scene::handle_from_wire_name(s))
.filter(|&h| self.tabs[i].scene.passes_selection_filter(h))
.filter(|&h| !self.tabs[i].scene.is_layer_locked(h))
.collect();
if cands.len() >= 2 {
// Overlap: open the list box at the cursor.
@@ -2285,7 +2290,15 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task<Message> {
// each plain click adds to the selection,
// Shift+click removes the picked entity.
// Esc / empty-space click clears.
if self.shift_down {
if let Some(layer) =
self.tabs[i].scene.locked_layer_name(handle)
{
// Locked layer: visible + snappable, but
// not selectable. Tell the user why.
self.command_line.push_info(&format!(
"Object is on locked layer \"{layer}\" — unlock the layer to select or edit it."
));
} else if self.shift_down {
self.tabs[i].scene.deselect_entity(handle);
} else {
self.tabs[i].scene.select_entity(handle, false);
@@ -2341,6 +2354,8 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task<Message> {
));
// Selection filter: keep only allowed types.
handles.retain(|&h| self.tabs[i].scene.passes_selection_filter(h));
// Objects on a locked layer aren't selectable.
handles.retain(|&h| !self.tabs[i].scene.is_layer_locked(h));
// Accumulate (issue #83): a plain box adds to the
// current selection, Shift+box removes the boxed
// entities. An empty box leaves the selection alone
+6
View File
@@ -389,6 +389,11 @@ impl OpenCADStudio {
_ => (None, None),
};
let hover_locked = tab
.scene
.hover_highlight
.map(|h| tab.scene.is_layer_locked(h))
.unwrap_or(false);
crate::ui::overlay::selection_overlay(
sel,
snap_info,
@@ -402,6 +407,7 @@ impl OpenCADStudio {
pane_drop_rect,
tab.pan_mode,
self.ribbon.open_dropdown.is_some(),
hover_locked,
)
};