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

+18
View File
@@ -1,6 +1,22 @@
// Auto-split from scene/mod.rs. Pure text-move; behaviour unchanged.
use super::super::*;
/// Dim an already-bg-adapted colour toward the background when the entity's
/// layer is locked, so locked objects read as non-editable (they stay visible
/// and snappable). No-op for unlocked layers.
fn fade_if_locked(
document: &acadrust::CadDocument,
e: &EntityType,
color: [f32; 4],
bg: [f32; 4],
) -> [f32; 4] {
if view::render::layer_locked(document, e) {
crate::scene::cache::block_cache::fade_toward_bg(color, bg)
} else {
color
}
}
// ── Parallel tessellation free function ──────────────────────────────────────
//
// Takes only the `Send + Sync` data needed for tessellation so that
@@ -107,6 +123,7 @@ pub(crate) fn tessellate_entity(
let (entity_color, _, _, _, aci_idx) =
view::render::render_style_for(document, e);
let entity_color = view::render::adapt_to_bg(entity_color, bg_color);
let entity_color = fade_if_locked(document, e, entity_color, bg_color);
if is_3d_entity {
// `ab` is already in the local frame
// (entity_aabb subtracted world_offset
@@ -190,6 +207,7 @@ pub(crate) fn tessellate_entity(
let (entity_color, pattern_length, pattern, line_weight_px, aci) =
view::render::render_style_for(document, e);
let entity_color = view::render::adapt_to_bg(entity_color, bg_color);
let entity_color = fade_if_locked(document, e, entity_color, bg_color);
let lt_scale = document.header.linetype_scale as f32 * e.common().linetype_scale as f32;
let lt_name = view::render::linetype_name_for(document, e);
// PSLTSCALE: scale linetype dashes by viewport anno_scale so they appear uniform in paper space.
+2
View File
@@ -94,5 +94,7 @@ impl Scene {
if let Some(layer) = self.document.layers.get_mut(name) {
layer.flags.locked = !layer.flags.locked;
}
// Re-tessellate so the locked-layer dim (fade) appears/clears at once.
self.bump_geometry();
}
}
+18
View File
@@ -2296,6 +2296,24 @@ impl Scene {
.unwrap_or(false)
}
/// True when `handle`'s entity sits on a locked layer. Locked objects stay
/// visible and snappable but cannot be selected or modified — callers in
/// the pick / modify paths consult this to skip them.
pub fn is_layer_locked(&self, handle: Handle) -> bool {
self.document
.get_entity(handle)
.map(|e| e.common().layer.clone())
.and_then(|name| self.document.layers.get(&name).map(|l| l.is_locked()))
.unwrap_or(false)
}
/// The name of the locked layer `handle` sits on, if any (for messages).
pub fn locked_layer_name(&self, handle: Handle) -> Option<String> {
let name = self.document.get_entity(handle).map(|e| e.common().layer.clone())?;
let locked = self.document.layers.get(&name).map(|l| l.is_locked()).unwrap_or(false);
locked.then_some(name)
}
/// Visibility test for a solid mesh entity, mirroring the 2D wire path:
/// honour the invisible flag, the isolate/hide set, and the layer's
/// off/frozen state.
+9
View File
@@ -5,6 +5,11 @@ impl Scene {
// ── Modify (transform / copy) ─────────────────────────────────────────
pub fn transform_entities(&mut self, handles: &[Handle], t: &EntityTransform) {
// Never transform objects on a locked layer (defense-in-depth: the pick
// path already excludes them, but programmatic callers may not).
let handles: Vec<Handle> =
handles.iter().copied().filter(|&h| !self.is_layer_locked(h)).collect();
let handles = &handles[..];
// MIRRTEXT (header.mirror_text): when false AutoCAD positions text /
// mtext / shape by the mirror but keeps the original rotation +
// oblique so the text stays right-reading. Capture before the
@@ -254,6 +259,10 @@ impl Scene {
// ── Grip editing ──────────────────────────────────────────────────────
pub fn apply_grip(&mut self, handle: Handle, grip_id: usize, apply: GripApply) {
// Objects on a locked layer can't be grip-edited.
if self.is_layer_locked(handle) {
return;
}
// For Solid3D / Region / Body, record the old point_of_reference so we
// can translate the pre-tessellated MeshModel by the same delta after
// the grip is applied (the ACIS data itself is not modified).
+14
View File
@@ -130,6 +130,16 @@ impl Scene {
let Some(e) = self.document.get_entity(h) else {
continue;
};
// Never quick-select objects on a locked layer.
if self
.document
.layers
.get(&e.common().layer)
.map(|l| l.is_locked())
.unwrap_or(false)
{
continue;
}
if let Some(t) = type_name {
if entity_type_name(e) != t {
continue;
@@ -316,6 +326,10 @@ impl Scene {
pub fn erase_entities(&mut self, handles: &[Handle]) {
for &h in handles {
// Objects on a locked layer can't be erased.
if self.is_layer_locked(h) {
continue;
}
self.document.remove_entity(h);
self.selected.remove(&h);
self.hatches.remove(&h);
+19 -1
View File
@@ -483,10 +483,28 @@ impl Scene {
} else {
self.paper_bg_color
};
(adapt_to_bg(color, bg), pl, pat, lw, aci)
// Objects on a locked layer are dimmed toward the background so they
// read as "not editable" (they stay visible and snappable).
let adapted = adapt_to_bg(color, bg);
let final_color = if layer_locked(&self.document, e) {
crate::scene::cache::block_cache::fade_toward_bg(adapted, bg)
} else {
adapted
};
(final_color, pl, pat, lw, aci)
}
}
/// Whether an entity sits on a locked layer (via the document's layer table).
/// Document-only so it is safe from the parallel tessellation path.
pub(in crate::scene) fn layer_locked(document: &CadDocument, e: &EntityType) -> bool {
document
.layers
.get(&e.common().layer)
.map(|l| l.is_locked())
.unwrap_or(false)
}
// ── Document-only render-style helpers (no &self, safe to call from parallel contexts) ──
/// Resolves the effective linetype name for an entity, falling back to the