fix(layers): protect locked entities

Keep locked-layer objects selectable for inspection while suppressing grips, property edits, command mutations, and stale editor writes.
This commit is contained in:
Hakan Seven 2026-08-12 12:46:41 +03:00
commit 0be9c41773
25 changed files with 524 additions and 118 deletions

View file

@ -4,6 +4,16 @@ use acadrust::Handle;
use iced::Task; use iced::Task;
impl OpenCADStudio { impl OpenCADStudio {
pub(super) fn reject_locked_edit(&mut self, i: usize, handle: Handle) -> bool {
let Some(layer) = self.tabs[i].scene.locked_layer_name(handle) else {
return false;
};
self.command_line.push_info(crate::tf!(
"Object is on locked layer \"{layer}\" — unlock the layer to edit it."
).as_ref());
true
}
fn refresh_area_preview(&mut self, i: usize) { fn refresh_area_preview(&mut self, i: usize) {
let regions = self.tabs[i] let regions = self.tabs[i]
.active_cmd .active_cmd
@ -499,16 +509,12 @@ impl OpenCADStudio {
.entity_wires() .entity_wires()
.iter() .iter()
.filter_map(|w| crate::scene::Scene::handle_from_wire_name(&w.name)) .filter_map(|w| crate::scene::Scene::handle_from_wire_name(&w.name))
.filter(|&h| !self.tabs[i].scene.is_layer_locked(h))
.collect(), .collect(),
"P" | "PREVIOUS" => self.tabs[i] "P" | "PREVIOUS" => self.tabs[i]
.prev_selection .prev_selection
.iter() .iter()
.copied() .copied()
.filter(|&h| { .filter(|&h| self.tabs[i].scene.document.get_entity(h).is_some())
self.tabs[i].scene.document.get_entity(h).is_some()
&& !self.tabs[i].scene.is_layer_locked(h)
})
.collect(), .collect(),
// Highest handle among the selectable wires of the current space — // Highest handle among the selectable wires of the current space —
// handles are handed out monotonically, so that is the most // handles are handed out monotonically, so that is the most
@ -518,7 +524,6 @@ impl OpenCADStudio {
.entity_wires() .entity_wires()
.iter() .iter()
.filter_map(|w| crate::scene::Scene::handle_from_wire_name(&w.name)) .filter_map(|w| crate::scene::Scene::handle_from_wire_name(&w.name))
.filter(|&h| !self.tabs[i].scene.is_layer_locked(h))
.max_by_key(|h| h.value()) .max_by_key(|h| h.value())
.into_iter() .into_iter()
.collect(), .collect(),
@ -713,6 +718,17 @@ impl OpenCADStudio {
} }
} }
CmdResult::CommitEntity(entity) => { CmdResult::CommitEntity(entity) => {
let source_handle = entity.common().handle;
if !source_handle.is_null()
&& self.tabs[i]
.scene
.document
.get_entity(source_handle)
.is_some()
&& self.reject_locked_edit(i, source_handle)
{
return Task::none();
}
// A line/arc drawn by a repeating command advances the ARC_CONT // A line/arc drawn by a repeating command advances the ARC_CONT
// continuation anchor, so ending one run and launching another // continuation anchor, so ending one run and launching another
// keeps continuing from the last segment (mirrors the // keeps continuing from the last segment (mirrors the
@ -741,6 +757,17 @@ impl OpenCADStudio {
} }
} }
CmdResult::CommitEntities(entities) => { CmdResult::CommitEntities(entities) => {
let locked_source = entities.iter().find_map(|entity| {
let handle = entity.common().handle;
(!handle.is_null()
&& self.tabs[i].scene.document.get_entity(handle).is_some()
&& self.tabs[i].scene.is_layer_locked(handle))
.then_some(handle)
});
if let Some(handle) = locked_source {
self.reject_locked_edit(i, handle);
return Task::none();
}
let label = self.history_label_from_active_cmd(i, "ENTITY"); let label = self.history_label_from_active_cmd(i, "ENTITY");
let delta_safe = entities let delta_safe = entities
.iter() .iter()
@ -1000,7 +1027,12 @@ impl OpenCADStudio {
self.restore_pre_cmd_tangent(); self.restore_pre_cmd_tangent();
return self.on_layout_switch(layout); return self.on_layout_switch(layout);
} }
CmdResult::TransformSelected(handles, transform) => { CmdResult::TransformSelected(mut handles, transform) => {
handles.retain(|handle| !self.tabs[i].scene.is_layer_locked(*handle));
if handles.is_empty() {
self.tabs[i].active_cmd = None;
return Task::none();
}
let label = self.history_label_from_active_cmd(i, "MOVE"); let label = self.history_label_from_active_cmd(i, "MOVE");
// A move/rotate/scale/mirror mutates only the selected entities // A move/rotate/scale/mirror mutates only the selected entities
// (and their baked dimension sub-entities) through // (and their baked dimension sub-entities) through
@ -1017,7 +1049,11 @@ impl OpenCADStudio {
self.commit_undo_delta(i, p); self.commit_undo_delta(i, p);
} }
} }
CmdResult::CopySelected(handles, transform) => { CmdResult::CopySelected(mut handles, transform) => {
handles.retain(|handle| !self.tabs[i].scene.is_layer_locked(*handle));
if handles.is_empty() {
return Task::none();
}
let label = self.history_label_from_active_cmd(i, "COPY"); let label = self.history_label_from_active_cmd(i, "COPY");
// Copying a dimension clones a *D block record, so gate delta on // Copying a dimension clones a *D block record, so gate delta on
// the selection being dimension-free. // the selection being dimension-free.
@ -1160,10 +1196,16 @@ impl OpenCADStudio {
} }
} }
CmdResult::CreateBlock { CmdResult::CreateBlock {
handles, mut handles,
name, name,
base, base,
} => { } => {
handles.retain(|handle| !self.tabs[i].scene.is_layer_locked(*handle));
if handles.is_empty() {
self.command_line
.push_info(crate::t!("No editable objects selected.").as_ref());
return Task::none();
}
self.push_undo_snapshot(i, "BLOCK"); self.push_undo_snapshot(i, "BLOCK");
let ucs = self.tabs[i].ucs_xform(); let ucs = self.tabs[i].ucs_xform();
let world_to_block = ucs.to_ucs_transform_at(base); let world_to_block = ucs.to_ucs_transform_at(base);
@ -1218,7 +1260,12 @@ impl OpenCADStudio {
self.commit_undo_delta(i, pd); self.commit_undo_delta(i, pd);
} }
} }
CmdResult::BatchCopy(handles, transforms) => { CmdResult::BatchCopy(mut handles, transforms) => {
handles.retain(|handle| !self.tabs[i].scene.is_layer_locked(*handle));
if handles.is_empty() {
self.tabs[i].active_cmd = None;
return Task::none();
}
let label = self.history_label_from_active_cmd(i, "ARRAY"); let label = self.history_label_from_active_cmd(i, "ARRAY");
let count = transforms.len(); let count = transforms.len();
// Same gate as COPY (dimension-free), sized by the total number // Same gate as COPY (dimension-free), sized by the total number
@ -1242,6 +1289,14 @@ impl OpenCADStudio {
} }
} }
CmdResult::ReplaceMany(replacements, additions) => { CmdResult::ReplaceMany(replacements, additions) => {
if let Some((handle, _)) = replacements
.iter()
.find(|(handle, _)| self.tabs[i].scene.is_layer_locked(*handle))
{
self.reject_locked_edit(i, *handle);
self.tabs[i].active_cmd = None;
return Task::none();
}
let label = self.history_label_from_active_cmd(i, "FILLET"); let label = self.history_label_from_active_cmd(i, "FILLET");
let was_catchment = self.tabs[i] let was_catchment = self.tabs[i]
.active_cmd .active_cmd
@ -1275,6 +1330,13 @@ impl OpenCADStudio {
self.refresh_properties(); self.refresh_properties();
} }
CmdResult::ReplaceManyContinue(replacements) => { CmdResult::ReplaceManyContinue(replacements) => {
if let Some((handle, _)) = replacements
.iter()
.find(|(handle, _)| self.tabs[i].scene.is_layer_locked(*handle))
{
self.reject_locked_edit(i, *handle);
return Task::none();
}
let label = self.history_label_from_active_cmd(i, "TRIM"); let label = self.history_label_from_active_cmd(i, "TRIM");
self.push_undo_snapshot(i, label); self.push_undo_snapshot(i, label);
for (handle, entities) in replacements { for (handle, entities) in replacements {
@ -1300,6 +1362,9 @@ impl OpenCADStudio {
self.refresh_properties(); self.refresh_properties();
} }
CmdResult::ReplaceEntity(handle, new_entities) => { CmdResult::ReplaceEntity(handle, new_entities) => {
if self.reject_locked_edit(i, handle) {
return Task::none();
}
// Detect SPLINEDIT sentinel: a single XLine with a magic layer name. // Detect SPLINEDIT sentinel: a single XLine with a magic layer name.
if new_entities.len() == 1 { if new_entities.len() == 1 {
if let acadrust::EntityType::XLine(ref xl) = new_entities[0] { if let acadrust::EntityType::XLine(ref xl) = new_entities[0] {
@ -1673,7 +1738,14 @@ impl OpenCADStudio {
.document .document
.get_entity(src) .get_entity(src)
.map(|e| e.common().layer.clone()); .map(|e| e.common().layer.clone());
if let Some(layer) = src_layer { let dest: Vec<_> = dest
.into_iter()
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.collect();
if dest.is_empty() {
self.command_line
.push_info(crate::t!("No editable objects selected.").as_ref());
} else if let Some(layer) = src_layer {
self.push_undo_snapshot(i, "LAYMATCH"); self.push_undo_snapshot(i, "LAYMATCH");
for h in &dest { for h in &dest {
if let Some(e) = self.tabs[i].scene.document.get_entity_mut(*h) { if let Some(e) = self.tabs[i].scene.document.get_entity_mut(*h) {
@ -1692,7 +1764,11 @@ impl OpenCADStudio {
self.command_line.push_error(crate::t!("Source object not found.").as_ref()); self.command_line.push_error(crate::t!("Source object not found.").as_ref());
} }
} }
CmdResult::MatchProperties { dest, src } => { CmdResult::MatchProperties { mut dest, src } => {
dest.retain(|handle| !self.tabs[i].scene.is_layer_locked(*handle));
if dest.is_empty() {
return Task::none();
}
// The command stays active after each apply so more targets // The command stays active after each apply so more targets
// can keep being picked; Enter / Esc ends it (#362). // can keep being picked; Enter / Esc ends it (#362).
// Special (type-specific) properties travel like AutoCAD's // Special (type-specific) properties travel like AutoCAD's
@ -1868,10 +1944,14 @@ impl OpenCADStudio {
.push_info(crate::tf!("{count} object(s) pasted.").as_ref()); .push_info(crate::tf!("{count} object(s) pasted.").as_ref());
} }
} }
CmdResult::CreateGroup { handles, name } => { CmdResult::CreateGroup { mut handles, name } => {
handles.retain(|handle| !self.tabs[i].scene.is_layer_locked(*handle));
self.tabs[i].active_cmd = None; self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None; self.tabs[i].snap_result = None;
self.tabs[i].scene.clear_preview_wire(); self.tabs[i].scene.clear_preview_wire();
if handles.is_empty() {
return Task::none();
}
let undo = self.begin_group_undo(i, "GROUP"); let undo = self.begin_group_undo(i, "GROUP");
self.tabs[i].scene.create_group(name.clone(), handles); self.tabs[i].scene.create_group(name.clone(), handles);
self.tabs[i].dirty = true; self.tabs[i].dirty = true;
@ -1879,10 +1959,14 @@ impl OpenCADStudio {
self.command_line self.command_line
.push_info(crate::tf!("Group \"{}\" created.", name).as_ref()); .push_info(crate::tf!("Group \"{}\" created.", name).as_ref());
} }
CmdResult::DeleteGroups { handles } => { CmdResult::DeleteGroups { mut handles } => {
handles.retain(|handle| !self.tabs[i].scene.is_layer_locked(*handle));
self.tabs[i].active_cmd = None; self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None; self.tabs[i].snap_result = None;
self.tabs[i].scene.clear_preview_wire(); self.tabs[i].scene.clear_preview_wire();
if handles.is_empty() {
return Task::none();
}
let undo = self.begin_group_undo(i, "UNGROUP"); let undo = self.begin_group_undo(i, "UNGROUP");
let count = self.tabs[i].scene.delete_groups_containing(&handles); let count = self.tabs[i].scene.delete_groups_containing(&handles);
self.tabs[i].dirty = true; self.tabs[i].dirty = true;
@ -2044,12 +2128,13 @@ impl OpenCADStudio {
} }
} }
CmdResult::AlignSelected { CmdResult::AlignSelected {
handles, mut handles,
src1, src1,
dst1, dst1,
angle_rad, angle_rad,
scale, scale,
} => { } => {
handles.retain(|handle| !self.tabs[i].scene.is_layer_locked(*handle));
if handles.is_empty() { if handles.is_empty() {
self.tabs[i].active_cmd = None; self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None; self.tabs[i].snap_result = None;
@ -2113,6 +2198,9 @@ impl OpenCADStudio {
pick_pt, pick_pt,
mode, mode,
} => { } => {
if self.reject_locked_edit(i, handle) {
return Task::none();
}
use crate::modules::draw::modify::lengthen::lengthen_entity; use crate::modules::draw::modify::lengthen::lengthen_entity;
let result = self.tabs[i] let result = self.tabs[i]
.scene .scene
@ -2195,6 +2283,9 @@ impl OpenCADStudio {
self.restore_pre_cmd_tangent(); self.restore_pre_cmd_tangent();
} }
CmdResult::PeditOp { handle, op } => { CmdResult::PeditOp { handle, op } => {
if self.reject_locked_edit(i, handle) {
return Task::none();
}
use crate::modules::draw::modify::pedit::{ use crate::modules::draw::modify::pedit::{
apply_pedit, convert_to_polyline, PeditOp, apply_pedit, convert_to_polyline, PeditOp,
}; };
@ -2248,6 +2339,13 @@ impl OpenCADStudio {
} }
} }
CmdResult::JoinEntities(handles) => { CmdResult::JoinEntities(handles) => {
if let Some(handle) = handles
.iter()
.find(|handle| self.tabs[i].scene.is_layer_locked(**handle))
{
self.reject_locked_edit(i, *handle);
return Task::none();
}
use crate::modules::draw::modify::join::join_entities; use crate::modules::draw::modify::join::join_entities;
let pairs: Vec<_> = handles let pairs: Vec<_> = handles
.iter() .iter()
@ -2285,6 +2383,9 @@ impl OpenCADStudio {
} }
} }
CmdResult::BreakEntity { handle, p1, p2 } => { CmdResult::BreakEntity { handle, p1, p2 } => {
if self.reject_locked_edit(i, handle) {
return Task::none();
}
use crate::modules::draw::modify::break_cmd::break_entity; use crate::modules::draw::modify::break_cmd::break_entity;
let replacement = self.tabs[i] let replacement = self.tabs[i]
.scene .scene
@ -2403,11 +2504,16 @@ impl OpenCADStudio {
self.tabs[i].active_cmd = Some(Box::new(cmd)); self.tabs[i].active_cmd = Some(Box::new(cmd));
} }
CmdResult::StretchEntities { CmdResult::StretchEntities {
handles, mut handles,
win_min, win_min,
win_max, win_max,
delta, delta,
} => { } => {
handles.retain(|handle| !self.tabs[i].scene.is_layer_locked(*handle));
if handles.is_empty() {
self.tabs[i].active_cmd = None;
return Task::none();
}
let structural = handles.iter().any(|handle| { let structural = handles.iter().any(|handle| {
matches!( matches!(
self.tabs[i].scene.document.get_entity(*handle), self.tabs[i].scene.document.get_entity(*handle),
@ -2702,6 +2808,10 @@ impl OpenCADStudio {
height, height,
color, color,
} => { } => {
if self.reject_locked_edit(i, handle) {
self.tabs[i].active_cmd = None;
return Task::none();
}
use crate::modules::insert::solid3d_cmds::empty_solid3d; use crate::modules::insert::solid3d_cmds::empty_solid3d;
use crate::scene::model::{solid_model, sweep_model}; use crate::scene::model::{solid_model, sweep_model};
@ -2753,6 +2863,10 @@ impl OpenCADStudio {
angle_deg, angle_deg,
color, color,
} => { } => {
if self.reject_locked_edit(i, handle) {
self.tabs[i].active_cmd = None;
return Task::none();
}
use crate::modules::insert::solid3d_cmds::empty_solid3d; use crate::modules::insert::solid3d_cmds::empty_solid3d;
use crate::scene::model::{solid_model, sweep_model}; use crate::scene::model::{solid_model, sweep_model};
@ -2815,6 +2929,12 @@ impl OpenCADStudio {
path_handle, path_handle,
color, color,
} => { } => {
if self.reject_locked_edit(i, profile_handle)
|| self.reject_locked_edit(i, path_handle)
{
self.tabs[i].active_cmd = None;
return Task::none();
}
use crate::modules::insert::solid3d_cmds::empty_solid3d; use crate::modules::insert::solid3d_cmds::empty_solid3d;
use crate::scene::model::sweep_model; use crate::scene::model::sweep_model;
@ -2869,6 +2989,15 @@ impl OpenCADStudio {
// ── LOFT ────────────────────────────────────────────────────── // ── LOFT ──────────────────────────────────────────────────────
CmdResult::LoftEntities { handles, color } => { CmdResult::LoftEntities { handles, color } => {
if let Some(handle) = handles
.iter()
.find(|handle| self.tabs[i].scene.is_layer_locked(**handle))
.copied()
{
self.reject_locked_edit(i, handle);
self.tabs[i].active_cmd = None;
return Task::none();
}
use crate::modules::insert::solid3d_cmds::empty_solid3d; use crate::modules::insert::solid3d_cmds::empty_solid3d;
use crate::scene::model::sweep_model; use crate::scene::model::sweep_model;
@ -2914,6 +3043,9 @@ impl OpenCADStudio {
scale, scale,
angle, angle,
} => { } => {
if self.reject_locked_edit(i, handle) {
return Task::none();
}
if let Some(mut model) = self.tabs[i].scene.hatches.get(&handle).cloned() { if let Some(mut model) = self.tabs[i].scene.hatches.get(&handle).cloned() {
let layer = self.tabs[i] let layer = self.tabs[i]
.scene .scene
@ -3025,6 +3157,10 @@ impl OpenCADStudio {
self.tabs[i].scene.clear_preview_wire(); self.tabs[i].scene.clear_preview_wire();
} }
CmdResult::DdeditEntity { handle, new_text } => { CmdResult::DdeditEntity { handle, new_text } => {
if self.reject_locked_edit(i, handle) {
self.tabs[i].active_cmd = None;
return Task::none();
}
self.push_undo_snapshot(i, "DDEDIT"); self.push_undo_snapshot(i, "DDEDIT");
let mut updated = false; let mut updated = false;
let mut is_dim = false; let mut is_dim = false;

View file

@ -71,6 +71,7 @@ impl OpenCADStudio {
.selected_entities() .selected_entities()
.into_iter() .into_iter()
.map(|(h, _)| h) .map(|(h, _)| h)
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.collect(); .collect();
if handles.is_empty() { if handles.is_empty() {
use crate::modules::draw::select::SelectObjectsCommand; use crate::modules::draw::select::SelectObjectsCommand;
@ -106,6 +107,7 @@ impl OpenCADStudio {
.selected_entities() .selected_entities()
.into_iter() .into_iter()
.map(|(h, _)| h) .map(|(h, _)| h)
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.collect(); .collect();
if handles.is_empty() { if handles.is_empty() {
use crate::modules::draw::select::SelectObjectsCommand; use crate::modules::draw::select::SelectObjectsCommand;
@ -132,6 +134,7 @@ impl OpenCADStudio {
.selected_entities() .selected_entities()
.into_iter() .into_iter()
.map(|(h, _)| h) .map(|(h, _)| h)
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.collect(); .collect();
if coords.len() == 3 && !handles.is_empty() { if coords.len() == 3 && !handles.is_empty() {
let base = glam::DVec3::new(coords[0], coords[1], coords[2]); let base = glam::DVec3::new(coords[0], coords[1], coords[2]);
@ -160,6 +163,7 @@ impl OpenCADStudio {
.selected_entities() .selected_entities()
.into_iter() .into_iter()
.map(|(h, _)| h) .map(|(h, _)| h)
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.collect(); .collect();
if handles.is_empty() { if handles.is_empty() {
use crate::modules::draw::select::SelectObjectsCommand; use crate::modules::draw::select::SelectObjectsCommand;
@ -393,32 +397,55 @@ impl OpenCADStudio {
.collect() .collect()
}) })
.unwrap_or_default(); .unwrap_or_default();
let inserts: Vec<_> = self.tabs[i]
.scene
.document
.entities()
.filter_map(|entity| match entity {
acadrust::EntityType::Insert(insert)
if insert.block_name.eq_ignore_ascii_case(&block)
&& !self.tabs[i]
.scene
.is_layer_locked(insert.common.handle) =>
{
Some(insert.common.handle)
}
_ => None,
})
.collect();
if inserts.is_empty() {
self.command_line.push_error(
crate::t!("ATTSYNC: no editable block references.").as_ref(),
);
return Some(Task::none());
}
self.push_undo_snapshot(i, "ATTSYNC"); self.push_undo_snapshot(i, "ATTSYNC");
let mut synced = 0usize; let mut synced = 0usize;
let mut changes = Vec::new(); let mut changes = Vec::new();
for e in self.tabs[i].scene.document.entities_mut() { for handle in inserts {
if let acadrust::EntityType::Insert(ins) = e { let Some(acadrust::EntityType::Insert(ins)) =
if ins.block_name.eq_ignore_ascii_case(&block) { self.tabs[i].scene.document.get_entity_mut(handle)
ins.attributes.retain(|a| { else {
attdefs.iter().any(|(t, _)| t.eq_ignore_ascii_case(&a.tag)) continue;
}); };
for (tag, default) in &attdefs { ins.attributes.retain(|a| {
if !ins attdefs.iter().any(|(t, _)| t.eq_ignore_ascii_case(&a.tag))
.attributes });
.iter() for (tag, default) in &attdefs {
.any(|a| a.tag.eq_ignore_ascii_case(tag)) if !ins
{ .attributes
ins.attributes .iter()
.push(acadrust::entities::AttributeEntity::new( .any(|a| a.tag.eq_ignore_ascii_case(tag))
tag.clone(), {
default.clone(), ins.attributes
)); .push(acadrust::entities::AttributeEntity::new(
} tag.clone(),
default.clone(),
));
} }
synced += 1;
changes.push((ins.common.handle, crate::scene::ChangeKind::Modified));
}
} }
synced += 1;
changes.push((ins.common.handle, crate::scene::ChangeKind::Modified));
} }
self.tabs[i].scene.bump_entities(&changes); self.tabs[i].scene.bump_entities(&changes);
self.tabs[i].dirty = true; self.tabs[i].dirty = true;
@ -626,6 +653,7 @@ impl OpenCADStudio {
.iter() .iter()
.filter(|(_, e)| matches!(e, acadrust::EntityType::Insert(_))) .filter(|(_, e)| matches!(e, acadrust::EntityType::Insert(_)))
.map(|(h, _)| *h) .map(|(h, _)| *h)
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.collect(); .collect();
if inserts.is_empty() { if inserts.is_empty() {
self.command_line self.command_line

View file

@ -86,6 +86,7 @@ impl OpenCADStudio {
.selected_entities() .selected_entities()
.iter() .iter()
.map(|(h, _)| *h) .map(|(h, _)| *h)
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.collect(); .collect();
if handles.is_empty() { if handles.is_empty() {
self.command_line self.command_line
@ -208,6 +209,7 @@ impl OpenCADStudio {
.selected_entities() .selected_entities()
.iter() .iter()
.map(|(h, _)| *h) .map(|(h, _)| *h)
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.collect(); .collect();
if handles.is_empty() { if handles.is_empty() {
self.command_line self.command_line
@ -297,6 +299,7 @@ impl OpenCADStudio {
) )
}) })
.map(|(h, _)| *h) .map(|(h, _)| *h)
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.collect(); .collect();
if handles.is_empty() { if handles.is_empty() {
self.command_line self.command_line
@ -374,6 +377,7 @@ impl OpenCADStudio {
.iter() .iter()
.filter(|(_, e)| matches!(e, acadrust::EntityType::Text(_))) .filter(|(_, e)| matches!(e, acadrust::EntityType::Text(_)))
.map(|(h, _)| *h) .map(|(h, _)| *h)
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.collect(); .collect();
if handles.is_empty() { if handles.is_empty() {
self.command_line self.command_line
@ -469,7 +473,9 @@ impl OpenCADStudio {
.selected_entities() .selected_entities()
.iter() .iter()
.filter_map(|(h, e)| match e { .filter_map(|(h, e)| match e {
acadrust::EntityType::Text(t) => { acadrust::EntityType::Text(t)
if !self.tabs[i].scene.is_layer_locked(*h) =>
{
Some((*h, t.insertion_point.x, t.insertion_point.y)) Some((*h, t.insertion_point.x, t.insertion_point.y))
} }
_ => None, _ => None,
@ -1127,7 +1133,12 @@ impl OpenCADStudio {
"EXPLODE" => { "EXPLODE" => {
use crate::modules::draw::modify::explode::explode_entity; use crate::modules::draw::modify::explode::explode_entity;
let entities: Vec<_> = self.tabs[i].scene.selected_entities().into_iter().collect(); let entities: Vec<_> = self.tabs[i]
.scene
.selected_entities()
.into_iter()
.filter(|(handle, _)| !self.tabs[i].scene.is_layer_locked(*handle))
.collect();
if entities.is_empty() { if entities.is_empty() {
use crate::modules::draw::select::SelectObjectsCommand; use crate::modules::draw::select::SelectObjectsCommand;
let cmd = SelectObjectsCommand::new("EXPLODE"); let cmd = SelectObjectsCommand::new("EXPLODE");

View file

@ -30,6 +30,7 @@ impl OpenCADStudio {
.selected_entities() .selected_entities()
.iter() .iter()
.map(|(h, _)| *h) .map(|(h, _)| *h)
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.collect(); .collect();
let mut found = false; let mut found = false;
for sh in &selected_handles { for sh in &selected_handles {
@ -389,8 +390,20 @@ impl OpenCADStudio {
"SET" => { "SET" => {
let app = parts.get(1).copied().unwrap_or("OpenCADStudio"); let app = parts.get(1).copied().unwrap_or("OpenCADStudio");
let val = parts.get(2).copied().unwrap_or(""); let val = parts.get(2).copied().unwrap_or("");
let editable: Vec<_> = selected_handles
.iter()
.copied()
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.collect();
if editable.is_empty() {
self.command_line.push_error(
crate::t!("XDATA: selected entities are on locked layers.")
.as_ref(),
);
return Some(Task::none());
}
self.push_undo_snapshot(i, "XDATA SET"); self.push_undo_snapshot(i, "XDATA SET");
for sh in &selected_handles { for sh in &editable {
if let Some(entity) = if let Some(entity) =
self.tabs[i].scene.document.get_entity_mut(*sh) self.tabs[i].scene.document.get_entity_mut(*sh)
{ {
@ -402,13 +415,25 @@ impl OpenCADStudio {
self.tabs[i].dirty = true; self.tabs[i].dirty = true;
self.command_line.push_output(crate::tf!( self.command_line.push_output(crate::tf!(
"XDATA: set [{app}] = \"{val}\" on {} entity/entities.", "XDATA: set [{app}] = \"{val}\" on {} entity/entities.",
selected_handles.len() editable.len()
).as_ref()); ).as_ref());
} }
"CLEAR" => { "CLEAR" => {
let app_filter = parts.get(1).copied(); let app_filter = parts.get(1).copied();
let editable: Vec<_> = selected_handles
.iter()
.copied()
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.collect();
if editable.is_empty() {
self.command_line.push_error(
crate::t!("XDATA: selected entities are on locked layers.")
.as_ref(),
);
return Some(Task::none());
}
self.push_undo_snapshot(i, "XDATA CLEAR"); self.push_undo_snapshot(i, "XDATA CLEAR");
for sh in &selected_handles { for sh in &editable {
if let Some(entity) = if let Some(entity) =
self.tabs[i].scene.document.get_entity_mut(*sh) self.tabs[i].scene.document.get_entity_mut(*sh)
{ {
@ -600,6 +625,7 @@ impl OpenCADStudio {
.selected_entities() .selected_entities()
.iter() .iter()
.map(|(h, _)| *h) .map(|(h, _)| *h)
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.collect(); .collect();
if handles.is_empty() { if handles.is_empty() {
self.command_line self.command_line
@ -706,6 +732,7 @@ impl OpenCADStudio {
.selected_entities() .selected_entities()
.iter() .iter()
.map(|(h, _)| *h) .map(|(h, _)| *h)
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.collect(); .collect();
if handles.is_empty() { if handles.is_empty() {
self.command_line self.command_line
@ -765,8 +792,13 @@ impl OpenCADStudio {
self.command_line.push_info(crate::t!("Usage: HYPERLINK <url> (select objects first)").as_ref()); self.command_line.push_info(crate::t!("Usage: HYPERLINK <url> (select objects first)").as_ref());
return Some(Task::none()); return Some(Task::none());
} }
let handles: Vec<acadrust::Handle> = let handles: Vec<acadrust::Handle> = self.tabs[i]
self.tabs[i].scene.selected_entities().iter().map(|(h, _)| *h).collect(); .scene
.selected_entities()
.iter()
.map(|(h, _)| *h)
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.collect();
if handles.is_empty() { if handles.is_empty() {
self.command_line.push_error(crate::t!("HYPERLINK: select objects first.").as_ref()); self.command_line.push_error(crate::t!("HYPERLINK: select objects first.").as_ref());
return Some(Task::none()); return Some(Task::none());
@ -816,6 +848,7 @@ impl OpenCADStudio {
.selected_entities() .selected_entities()
.iter() .iter()
.map(|(h, _)| *h) .map(|(h, _)| *h)
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.collect(); .collect();
if handles.is_empty() { if handles.is_empty() {
self.command_line self.command_line
@ -938,6 +971,7 @@ impl OpenCADStudio {
.selected_entities() .selected_entities()
.iter() .iter()
.map(|(handle, _)| *handle) .map(|(handle, _)| *handle)
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.collect(); .collect();
if handles.is_empty() { if handles.is_empty() {
self.command_line self.command_line

View file

@ -145,6 +145,9 @@ impl OpenCADStudio {
let mut changed = 0usize; let mut changed = 0usize;
self.push_undo_snapshot(i, "ATTEDIT"); self.push_undo_snapshot(i, "ATTEDIT");
for sh in &selected_handles { for sh in &selected_handles {
if self.tabs[i].scene.is_layer_locked(*sh) {
continue;
}
if let Some(acadrust::EntityType::Insert(ins)) = self.tabs[i] if let Some(acadrust::EntityType::Insert(ins)) = self.tabs[i]
.scene .scene
.document .document
@ -195,10 +198,22 @@ impl OpenCADStudio {
let sub = cmd.split_whitespace().nth(1).unwrap_or("").to_uppercase(); let sub = cmd.split_whitespace().nth(1).unwrap_or("").to_uppercase();
match sub.as_str() { match sub.as_str() {
"ON" | "OFF" | "NORMAL" => { "ON" | "OFF" | "NORMAL" => {
let handles: Vec<_> = self.tabs[i]
.scene
.document
.entities()
.filter_map(|entity| {
matches!(entity, acadrust::EntityType::AttributeDefinition(_))
.then_some(entity.common().handle)
})
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.collect();
self.push_undo_snapshot(i, "ATTDISP"); self.push_undo_snapshot(i, "ATTDISP");
let mut count = 0usize; let mut count = 0usize;
for entity in self.tabs[i].scene.document.entities_mut() { for handle in handles {
if let acadrust::EntityType::AttributeDefinition(ad) = entity { if let Some(acadrust::EntityType::AttributeDefinition(ad)) =
self.tabs[i].scene.document.get_entity_mut(handle)
{
match sub.as_str() { match sub.as_str() {
"ON" => { "ON" => {
ad.flags.invisible = false; ad.flags.invisible = false;
@ -208,8 +223,6 @@ impl OpenCADStudio {
ad.flags.invisible = true; ad.flags.invisible = true;
count += 1; count += 1;
} }
"NORMAL" => { /* leave existing flags — they are already the "normal" state */
}
_ => {} _ => {}
} }
} }
@ -490,6 +503,7 @@ impl OpenCADStudio {
.selected_entities() .selected_entities()
.into_iter() .into_iter()
.map(|(h, _)| h) .map(|(h, _)| h)
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.collect(); .collect();
if handles.is_empty() { if handles.is_empty() {
use crate::modules::draw::select::SelectObjectsCommand; use crate::modules::draw::select::SelectObjectsCommand;
@ -730,6 +744,7 @@ impl OpenCADStudio {
.selected_entities() .selected_entities()
.into_iter() .into_iter()
.map(|(h, _)| h) .map(|(h, _)| h)
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.collect(); .collect();
if handles.is_empty() { if handles.is_empty() {
use crate::modules::draw::select::SelectObjectsCommand; use crate::modules::draw::select::SelectObjectsCommand;

View file

@ -16,8 +16,6 @@ impl OpenCADStudio {
.entity_wires() .entity_wires()
.iter() .iter()
.filter_map(|w| Scene::handle_from_wire_name(&w.name)) .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(); .collect();
let count = handles.len(); let count = handles.len();
for h in handles { for h in handles {
@ -362,6 +360,9 @@ impl OpenCADStudio {
let handle_u64: u64 = cmd["BEDIT_BEGIN:".len()..].parse().unwrap_or(0); let handle_u64: u64 = cmd["BEDIT_BEGIN:".len()..].parse().unwrap_or(0);
let insert_handle = Handle::new(handle_u64); let insert_handle = Handle::new(handle_u64);
if self.reject_locked_edit(i, insert_handle) {
return Some(Task::none());
}
let insert = match self.tabs[i].scene.document.get_entity(insert_handle) { let insert = match self.tabs[i].scene.document.get_entity(insert_handle) {
Some(acadrust::EntityType::Insert(ins)) => ins.clone(), Some(acadrust::EntityType::Insert(ins)) => ins.clone(),
@ -637,6 +638,9 @@ impl OpenCADStudio {
let handle_u64: u64 = cmd["REFEDIT_BEGIN:".len()..].parse().unwrap_or(0); let handle_u64: u64 = cmd["REFEDIT_BEGIN:".len()..].parse().unwrap_or(0);
let insert_handle = Handle::new(handle_u64); let insert_handle = Handle::new(handle_u64);
if self.reject_locked_edit(i, insert_handle) {
return Some(Task::none());
}
// Get INSERT entity. // Get INSERT entity.
let insert = match self.tabs[i].scene.document.get_entity(insert_handle) { let insert = match self.tabs[i].scene.document.get_entity(insert_handle) {
@ -995,9 +999,13 @@ impl OpenCADStudio {
.document .document
.entities() .entities()
.map(|e| e.common().handle) .map(|e| e.common().handle)
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.collect() .collect()
} else { } else {
sel.into_iter().map(|(h, _)| h).collect() sel.into_iter()
.map(|(h, _)| h)
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.collect()
} }
}; };
if handles.is_empty() { if handles.is_empty() {
@ -1165,7 +1173,10 @@ impl OpenCADStudio {
handles.clone() handles.clone()
} else { } else {
handles.iter().copied().take(1).collect() handles.iter().copied().take(1).collect()
}; }
.into_iter()
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.collect();
if targets.is_empty() { if targets.is_empty() {
self.command_line self.command_line
.push_output(crate::tf!("FIND: \"{}\" not found.", search).as_ref()); .push_output(crate::tf!("FIND: \"{}\" not found.", search).as_ref());

View file

@ -106,6 +106,7 @@ impl OpenCADStudio {
} }
self.push_undo_snapshot(i, "LAYER LOCK"); self.push_undo_snapshot(i, "LAYER LOCK");
self.tabs[i].dirty = true; self.tabs[i].dirty = true;
self.refresh_properties();
self.command_line.push_output(crate::t!("LAYER: layers locked.").as_ref()); self.command_line.push_output(crate::t!("LAYER: layers locked.").as_ref());
} }
"UNLOCK" | "UL" => { "UNLOCK" | "UL" => {
@ -116,6 +117,7 @@ impl OpenCADStudio {
} }
self.push_undo_snapshot(i, "LAYER UNLOCK"); self.push_undo_snapshot(i, "LAYER UNLOCK");
self.tabs[i].dirty = true; self.tabs[i].dirty = true;
self.refresh_properties();
self.command_line.push_output(crate::t!("LAYER: layers unlocked.").as_ref()); self.command_line.push_output(crate::t!("LAYER: layers unlocked.").as_ref());
} }
"COLOR" | "C" => { "COLOR" | "C" => {

View file

@ -399,6 +399,7 @@ impl OpenCADStudio {
self.tabs[i].dirty = true; self.tabs[i].dirty = true;
self.commit_layer_undo(i, undo); self.commit_layer_undo(i, undo);
self.refresh_layer_panel(); self.refresh_layer_panel();
self.refresh_properties();
self.command_line.push_info(crate::t!("Layer(s) locked.").as_ref()); self.command_line.push_info(crate::t!("Layer(s) locked.").as_ref());
} }
} }
@ -508,6 +509,7 @@ impl OpenCADStudio {
self.tabs[i].dirty = true; self.tabs[i].dirty = true;
self.commit_layer_undo(i, undo); self.commit_layer_undo(i, undo);
self.refresh_layer_panel(); self.refresh_layer_panel();
self.refresh_properties();
self.command_line.push_info(crate::t!("Layer(s) unlocked.").as_ref()); self.command_line.push_info(crate::t!("Layer(s) unlocked.").as_ref());
} }
} }
@ -686,6 +688,7 @@ impl OpenCADStudio {
.selected_entities() .selected_entities()
.into_iter() .into_iter()
.map(|(h, _)| h) .map(|(h, _)| h)
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.collect(); .collect();
if handles.is_empty() { if handles.is_empty() {
use crate::modules::draw::groups::ungroup::UngroupCommand; use crate::modules::draw::groups::ungroup::UngroupCommand;

View file

@ -607,6 +607,7 @@ impl OpenCADStudio {
.selected_entities() .selected_entities()
.into_iter() .into_iter()
.map(|(h, _)| h) .map(|(h, _)| h)
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.collect(); .collect();
if handles.is_empty() { if handles.is_empty() {
self.command_line self.command_line
@ -708,6 +709,7 @@ impl OpenCADStudio {
.selected_entities() .selected_entities()
.into_iter() .into_iter()
.map(|(h, _)| h) .map(|(h, _)| h)
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.collect(); .collect();
if handles.is_empty() { if handles.is_empty() {
self.command_line self.command_line
@ -756,7 +758,8 @@ impl OpenCADStudio {
.document .document
.entities() .entities()
.filter(|e| { .filter(|e| {
selected.is_empty() || selected.contains(&e.common().handle.value()) (selected.is_empty() || selected.contains(&e.common().handle.value()))
&& !self.tabs[i].scene.is_layer_locked(e.common().handle)
}) })
.map(|e| { .map(|e| {
let key = crate::entities::names::dxf_name(e).to_string(); let key = crate::entities::names::dxf_name(e).to_string();
@ -2284,6 +2287,7 @@ impl OpenCADStudio {
.selected_entities() .selected_entities()
.iter() .iter()
.map(|(h, _)| *h) .map(|(h, _)| *h)
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.collect(); .collect();
if selected_handles.is_empty() { if selected_handles.is_empty() {
self.command_line self.command_line

View file

@ -652,6 +652,16 @@ impl OpenCADStudio {
} }
}) })
.collect(); .collect();
if to_erase
.iter()
.any(|handle| self.tabs[i].scene.is_layer_locked(*handle))
{
self.command_line.push_error(
crate::t!("VPORTS: unlock existing viewport layers first.")
.as_ref(),
);
return Some(Task::none());
}
self.push_undo_snapshot(i, "VPORTS"); self.push_undo_snapshot(i, "VPORTS");
self.tabs[i].scene.erase_entities(&to_erase); self.tabs[i].scene.erase_entities(&to_erase);
// Create new viewports. // Create new viewports.
@ -814,6 +824,7 @@ impl OpenCADStudio {
.selected_entities() .selected_entities()
.iter() .iter()
.map(|(h, _)| *h) .map(|(h, _)| *h)
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.collect(); .collect();
if selected.is_empty() { if selected.is_empty() {
self.command_line self.command_line
@ -1004,6 +1015,7 @@ impl OpenCADStudio {
.iter() .iter()
.filter(|(_, e)| matches!(e, acadrust::EntityType::Viewport(_))) .filter(|(_, e)| matches!(e, acadrust::EntityType::Viewport(_)))
.map(|(h, _)| *h) .map(|(h, _)| *h)
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.collect(); .collect();
if vps.len() < 2 { if vps.len() < 2 {
self.command_line.push_error( self.command_line.push_error(

View file

@ -98,6 +98,10 @@ impl OpenCADStudio {
let search = self.find_replace.search.clone(); let search = self.find_replace.search.clone();
let replacement = self.find_replace.replacement.clone(); let replacement = self.find_replace.replacement.clone();
if match_is_locked(&self.tabs[i].scene, target) {
self.find_replace.status = "The matching object is on a locked layer.".to_string();
return;
}
self.push_undo_snapshot(i, "FIND/REPLACE"); self.push_undo_snapshot(i, "FIND/REPLACE");
let replaced = replace_match_text( let replaced = replace_match_text(
&mut self.tabs[i].scene.document, &mut self.tabs[i].scene.document,
@ -152,7 +156,11 @@ impl OpenCADStudio {
let mut replaced = 0usize; let mut replaced = 0usize;
let mut changed = Vec::new(); let mut changed = Vec::new();
let mut changed_outside_active_space = false; let mut changed_outside_active_space = false;
for target in matches { let editable: Vec<_> = matches
.into_iter()
.filter(|target| !match_is_locked(&self.tabs[i].scene, *target))
.collect();
for target in editable {
let count = replace_match_text( let count = replace_match_text(
&mut self.tabs[i].scene.document, &mut self.tabs[i].scene.document,
target, target,
@ -357,6 +365,11 @@ fn match_document_handle(target: FindMatchKey) -> acadrust::Handle {
} }
} }
fn match_is_locked(scene: &crate::scene::Scene, target: FindMatchKey) -> bool {
scene.is_layer_locked(match_owner_handle(target))
|| scene.is_layer_locked(match_document_handle(target))
}
fn match_label(target: FindMatchKey) -> String { fn match_label(target: FindMatchKey) -> String {
match target { match target {
FindMatchKey::Entity(handle) => format!("handle {:X}", handle.value()), FindMatchKey::Entity(handle) => format!("handle {:X}", handle.value()),

View file

@ -46,6 +46,7 @@ impl super::OpenCADStudio {
.selected .selected
.iter() .iter()
.copied() .copied()
.filter(|h| !self.tabs[i].scene.is_layer_locked(*h))
.filter(|h| self.tabs[i].scene.solid_models.contains_key(h)) .filter(|h| self.tabs[i].scene.solid_models.contains_key(h))
.collect(); .collect();
if handles.len() != 2 { if handles.len() != 2 {
@ -96,6 +97,7 @@ impl super::OpenCADStudio {
.selected .selected
.iter() .iter()
.copied() .copied()
.filter(|h| !self.tabs[i].scene.is_layer_locked(*h))
.filter(|h| self.tabs[i].scene.solid_models.contains_key(h)) .filter(|h| self.tabs[i].scene.solid_models.contains_key(h))
.collect(); .collect();
if handles.len() != 1 { if handles.len() != 1 {
@ -171,6 +173,7 @@ impl super::OpenCADStudio {
.selected .selected
.iter() .iter()
.copied() .copied()
.filter(|h| !self.tabs[i].scene.is_layer_locked(*h))
.filter(|h| self.tabs[i].scene.solid_models.contains_key(h)) .filter(|h| self.tabs[i].scene.solid_models.contains_key(h))
.collect(); .collect();
if handles.len() != 2 { if handles.len() != 2 {
@ -210,6 +213,7 @@ impl super::OpenCADStudio {
.selected .selected
.iter() .iter()
.copied() .copied()
.filter(|h| !self.tabs[i].scene.is_layer_locked(*h))
.filter(|h| self.tabs[i].scene.solid_models.contains_key(h)) .filter(|h| self.tabs[i].scene.solid_models.contains_key(h))
.collect(); .collect();
if handles.len() != 1 { if handles.len() != 1 {
@ -258,6 +262,7 @@ impl super::OpenCADStudio {
.scene .scene
.selected_entities() .selected_entities()
.iter() .iter()
.filter(|(h, _)| !self.tabs[i].scene.is_layer_locked(*h))
.find_map(|(h, e)| match e { .find_map(|(h, e)| match e {
EntityType::LwPolyline(pl) => Some(( EntityType::LwPolyline(pl) => Some((
*h, *h,
@ -343,6 +348,7 @@ impl super::OpenCADStudio {
.selected .selected
.iter() .iter()
.copied() .copied()
.filter(|h| !self.tabs[i].scene.is_layer_locked(*h))
.filter(|h| self.tabs[i].scene.solid_models.contains_key(h)) .filter(|h| self.tabs[i].scene.solid_models.contains_key(h))
.collect(); .collect();
if handles.len() != 1 { if handles.len() != 1 {
@ -394,6 +400,7 @@ impl super::OpenCADStudio {
.selected .selected
.iter() .iter()
.copied() .copied()
.filter(|h| !self.tabs[i].scene.is_layer_locked(*h))
.filter(|h| self.tabs[i].scene.solid_models.contains_key(h)) .filter(|h| self.tabs[i].scene.solid_models.contains_key(h))
.collect(); .collect();
if handles.len() != 1 { if handles.len() != 1 {
@ -461,6 +468,7 @@ impl super::OpenCADStudio {
.selected .selected
.iter() .iter()
.copied() .copied()
.filter(|h| !self.tabs[i].scene.is_layer_locked(*h))
.filter(|h| self.tabs[i].scene.solid_models.contains_key(h)) .filter(|h| self.tabs[i].scene.solid_models.contains_key(h))
.collect(); .collect();
if handles.len() != 1 { if handles.len() != 1 {
@ -561,6 +569,7 @@ impl super::OpenCADStudio {
.scene .scene
.selected_entities() .selected_entities()
.iter() .iter()
.filter(|(h, _)| !self.tabs[i].scene.is_layer_locked(*h))
.find_map(|(h, e)| match e { .find_map(|(h, e)| match e {
EntityType::LwPolyline(pl) => Some(( EntityType::LwPolyline(pl) => Some((
*h, *h,
@ -632,6 +641,7 @@ impl super::OpenCADStudio {
.selected .selected
.iter() .iter()
.copied() .copied()
.filter(|h| !self.tabs[i].scene.is_layer_locked(*h))
.filter(|h| self.tabs[i].scene.solid_models.contains_key(h)) .filter(|h| self.tabs[i].scene.solid_models.contains_key(h))
.collect(); .collect();
if handles.is_empty() { if handles.is_empty() {
@ -672,6 +682,7 @@ impl super::OpenCADStudio {
.selected .selected
.iter() .iter()
.copied() .copied()
.filter(|h| !self.tabs[i].scene.is_layer_locked(*h))
.filter(|h| self.tabs[i].scene.solid_models.contains_key(h)) .filter(|h| self.tabs[i].scene.solid_models.contains_key(h))
.collect(); .collect();
if handles.is_empty() { if handles.is_empty() {

View file

@ -606,6 +606,9 @@ impl super::OpenCADStudio {
initial: &str, initial: &str,
height: f64, height: f64,
) { ) {
if handle.is_some_and(|h| self.tabs[self.active_tab].scene.is_layer_locked(h)) {
return;
}
let mut state = MTextEditorState::new(pos, initial, height, handle); let mut state = MTextEditorState::new(pos, initial, height, handle);
if let Some(p) = self.tabs[self.active_tab].scene.selection.borrow().last_move_pos { if let Some(p) = self.tabs[self.active_tab].scene.selection.borrow().last_move_pos {
state.screen_anchor = p; state.screen_anchor = p;
@ -1222,6 +1225,10 @@ impl super::OpenCADStudio {
return false; return false;
} }
if let Some(h) = ed.editing { if let Some(h) = ed.editing {
if self.tabs[i].scene.is_layer_locked(h) {
self.refresh_properties();
return false;
}
self.push_undo_snapshot(i, "MTEXT"); self.push_undo_snapshot(i, "MTEXT");
match self.tabs[i].scene.document.get_entity_mut(h) { match self.tabs[i].scene.document.get_entity_mut(h) {
Some(EntityType::MText(t)) => { Some(EntityType::MText(t)) => {
@ -1307,6 +1314,9 @@ impl super::OpenCADStudio {
return; return;
} }
if let Some(h) = editing { if let Some(h) = editing {
if self.tabs[i].scene.is_layer_locked(h) {
return;
}
self.push_undo_snapshot(i, "MTEXT"); self.push_undo_snapshot(i, "MTEXT");
match self.tabs[i].scene.document.get_entity_mut(h) { match self.tabs[i].scene.document.get_entity_mut(h) {
Some(EntityType::MText(t)) => { Some(EntityType::MText(t)) => {

View file

@ -117,6 +117,9 @@ impl<'a> HostSession<'a> {
/// missing so the file stays valid for other CAD apps. Returns `false` when /// missing so the file stays valid for other CAD apps. Returns `false` when
/// the entity does not exist. /// the entity does not exist.
pub fn write_record(&mut self, handle: Handle, record: ExtendedDataRecord) -> bool { pub fn write_record(&mut self, handle: Handle, record: ExtendedDataRecord) -> bool {
if self.app.tabs[self.tab].scene.is_layer_locked(handle) {
return false;
}
let app = record.application_name.clone(); let app = record.application_name.clone();
self.ensure_app_id(&app); self.ensure_app_id(&app);
let app_handle = self.document().app_ids.get(&app).map(|a| a.handle.value()); let app_handle = self.document().app_ids.get(&app).map(|a| a.handle.value());
@ -150,6 +153,9 @@ impl<'a> HostSession<'a> {
/// Remove the XDATA record for `app_name` from entity `handle`. Returns /// Remove the XDATA record for `app_name` from entity `handle`. Returns
/// `true` when a record was actually removed. /// `true` when a record was actually removed.
pub fn remove_record(&mut self, handle: Handle, app_name: &str) -> bool { pub fn remove_record(&mut self, handle: Handle, app_name: &str) -> bool {
if self.app.tabs[self.tab].scene.is_layer_locked(handle) {
return false;
}
let app_handle = self.document().app_ids.get(app_name).map(|a| a.handle.value()); let app_handle = self.document().app_ids.get(app_name).map(|a| a.handle.value());
let Some(entity) = self.document_mut().get_entity_mut(handle) else { let Some(entity) = self.document_mut().get_entity_mut(handle) else {
return false; return false;

View file

@ -1445,6 +1445,26 @@ impl OpenCADStudio {
panel.source_handles = new_handles; panel.source_handles = new_handles;
panel.prop_vertex = prop_vertex; panel.prop_vertex = prop_vertex;
panel.prop_vertex_indicator_active = prop_vertex_indicator_active; panel.prop_vertex_indicator_active = prop_vertex_indicator_active;
let property_handles = panel.selected_handles();
let property_handles = if property_handles.is_empty() {
&panel.source_handles
} else {
&property_handles
};
let locked_only = !property_handles.is_empty()
&& property_handles
.iter()
.all(|handle| self.tabs[i].scene.is_layer_locked(*handle));
if locked_only {
make_sections_read_only(&mut panel.sections);
panel.edit_buf.clear();
panel.color_picker_open = false;
panel.color_palette_open = false;
panel.bg_color_picker_open = false;
panel.open_color_field = None;
panel.hatch_pattern_picker_open = false;
panel.edit_choice_open = false;
}
panel panel
}; };
@ -1563,6 +1583,15 @@ impl OpenCADStudio {
/// Rebuild the cached selected_grips from the current entity selection. /// Rebuild the cached selected_grips from the current entity selection.
pub(super) fn refresh_selected_grips(&mut self) { pub(super) fn refresh_selected_grips(&mut self) {
let i = self.active_tab; let i = self.active_tab;
let locked_active_grip = self.tabs[i].active_grip.as_ref().is_some_and(|grip| {
grip.targets
.iter()
.any(|target| self.tabs[i].scene.is_layer_locked(target.handle))
});
if locked_active_grip {
self.cancel_active_grip_edit();
return;
}
let is_paper = self.tabs[i].scene.current_layout != "Model"; let is_paper = self.tabs[i].scene.current_layout != "Model";
// Paper-space entity coordinates are NOT offset by world_offset (same rule // Paper-space entity coordinates are NOT offset by world_offset (same rule
// as wire tessellation in wires_for_block). Only subtract in model space. // as wire tessellation in wires_for_block). Only subtract in model space.
@ -1574,10 +1603,15 @@ impl OpenCADStudio {
let (new_handle, new_grips, new_grip_handles) = { let (new_handle, new_grips, new_grip_handles) = {
let annotation_scale_handle = self.tabs[i].scene.displayed_annotation_scale_handle(); let annotation_scale_handle = self.tabs[i].scene.displayed_annotation_scale_handle();
let selected = self.tabs[i].scene.selected_entities(); let selected = self.tabs[i].scene.selected_entities();
let single_handle = (selected.len() == 1).then(|| selected[0].0); let single_handle = (selected.len() == 1
&& !self.tabs[i].scene.is_layer_locked(selected[0].0))
.then(|| selected[0].0);
let mut grips = Vec::new(); let mut grips = Vec::new();
let mut handles = Vec::new(); let mut handles = Vec::new();
for (handle, entity) in selected { for (handle, entity) in selected {
if self.tabs[i].scene.is_layer_locked(handle) {
continue;
}
let contextual = crate::scene::annotative::entity_for_annotation_context( let contextual = crate::scene::annotative::entity_for_annotation_context(
&self.tabs[i].scene.document, &self.tabs[i].scene.document,
entity, entity,
@ -1618,12 +1652,21 @@ impl OpenCADStudio {
} }
pub(super) fn property_target_handles(&self, i: usize) -> Vec<Handle> { pub(super) fn property_target_handles(&self, i: usize) -> Vec<Handle> {
let handles = self.tabs[i].properties.selected_handles(); let mut handles = self.tabs[i].properties.selected_handles();
if !handles.is_empty() { if handles.is_empty() {
handles handles = self.tabs[i].properties.source_handles.clone();
} else {
self.tabs[i].selected_handle.into_iter().collect()
} }
if handles.is_empty() {
handles.extend(self.tabs[i].selected_handle);
}
handles.retain(|handle| !self.tabs[i].scene.is_layer_locked(*handle));
handles
}
pub(super) fn has_property_selection(&self, i: usize) -> bool {
!self.tabs[i].properties.selected_handles().is_empty()
|| !self.tabs[i].properties.source_handles.is_empty()
|| self.tabs[i].selected_handle.is_some()
} }
pub(super) fn invalidate_property_targets(&mut self, i: usize, handles: &[Handle]) { pub(super) fn invalidate_property_targets(&mut self, i: usize, handles: &[Handle]) {
@ -1864,6 +1907,45 @@ impl OpenCADStudio {
} }
} }
fn make_sections_read_only(
sections: &mut [crate::scene::model::object::PropSection],
) {
use crate::scene::model::object::PropValue;
for property in sections
.iter_mut()
.flat_map(|section| section.props.iter_mut())
{
let text = match &property.value {
PropValue::ReadOnly(value)
| PropValue::EditText(value)
| PropValue::LayerChoice(value)
| PropValue::LinetypeChoice(value)
| PropValue::HatchPatternChoice(value) => value.clone(),
PropValue::Choice { selected, .. } => selected.clone(),
PropValue::EditChoice { value, .. } => value.clone(),
PropValue::ColorChoice(color) => match color {
acadrust::types::Color::None => "None".to_string(),
acadrust::types::Color::ByLayer => "ByLayer".to_string(),
acadrust::types::Color::ByBlock => "ByBlock".to_string(),
acadrust::types::Color::Index(index) => index.to_string(),
acadrust::types::Color::Rgb { r, g, b } => format!("{r},{g},{b}"),
},
PropValue::ColorVaries | PropValue::LwVaries => VARIES_LABEL.to_string(),
PropValue::LwChoice(lineweight) => {
ui::properties::LwItem(*lineweight).to_string()
}
PropValue::BoolToggle { value, .. } => {
if *value { t!("Yes") } else { t!("No") }.into_owned()
}
PropValue::Stepper { display, .. } => display.clone(),
PropValue::AttrText { value, .. } => value.clone(),
};
property.field = "locked_read_only";
property.value = PropValue::ReadOnly(text);
}
}
// ── Multi-selection property aggregation ─────────────────────────────────── // ── Multi-selection property aggregation ───────────────────────────────────
pub(super) fn build_selection_groups( pub(super) fn build_selection_groups(

View file

@ -125,6 +125,9 @@ impl super::OpenCADStudio {
/// the plain box (the rich editor needs no field focus). /// the plain box (the rich editor needs no field focus).
pub(super) fn begin_text_edit(&mut self, handle: Handle) -> iced::Task<super::Message> { pub(super) fn begin_text_edit(&mut self, handle: Handle) -> iced::Task<super::Message> {
let i = self.active_tab; let i = self.active_tab;
if self.tabs[i].scene.is_layer_locked(handle) {
return iced::Task::none();
}
// Resolve a Leader chain to the annotated entity. // Resolve a Leader chain to the annotated entity.
let mut target = handle; let mut target = handle;
for _ in 0..8 { for _ in 0..8 {
@ -139,6 +142,9 @@ impl super::OpenCADStudio {
_ => break, _ => break,
} }
} }
if self.tabs[i].scene.is_layer_locked(target) {
return iced::Task::none();
}
// Snapshot what we need before borrowing `self` mutably to open. // Snapshot what we need before borrowing `self` mutably to open.
let Some(entity) = self.tabs[i].scene.document.get_entity(target) else { let Some(entity) = self.tabs[i].scene.document.get_entity(target) else {
return iced::Task::none(); return iced::Task::none();
@ -178,6 +184,9 @@ impl super::OpenCADStudio {
height: f64, height: f64,
field: TextEntityField, field: TextEntityField,
) { ) {
if handle.is_some_and(|h| self.tabs[self.active_tab].scene.is_layer_locked(h)) {
return;
}
let mut state = TextInlineState { let mut state = TextInlineState {
pos, pos,
value: initial.to_string(), value: initial.to_string(),
@ -202,6 +211,10 @@ impl super::OpenCADStudio {
return false; return false;
} }
if let Some(h) = ed.editing { if let Some(h) = ed.editing {
if self.tabs[i].scene.is_layer_locked(h) {
self.refresh_properties();
return false;
}
self.push_undo_snapshot(i, "TEXT"); self.push_undo_snapshot(i, "TEXT");
if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(h) { if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(h) {
write_text_field(entity, ed.field, ed.value.clone()); write_text_field(entity, ed.field, ed.value.clone());

View file

@ -239,6 +239,11 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
// Grip-menu value prompt — consume the typed number and // Grip-menu value prompt — consume the typed number and
// route it through `apply_grip_menu_value`. // route it through `apply_grip_menu_value`.
if let Some(pending) = self.grip_pending.take() { if let Some(pending) = self.grip_pending.take() {
let i = self.active_tab;
if self.reject_locked_edit(i, pending.handle) {
self.cancel_active_grip_edit();
return Task::none();
}
let raw = crate::app::expr_eval::eval_to_string(self.command_line.input.trim()); let raw = crate::app::expr_eval::eval_to_string(self.command_line.input.trim());
self.command_line.input.clear(); self.command_line.input.clear();
let Ok(v) = raw.parse::<f64>() else { let Ok(v) = raw.parse::<f64>() else {
@ -249,7 +254,6 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
self.grip_pending = Some(pending); self.grip_pending = Some(pending);
return self.focus_cmd_input(); return self.focus_cmd_input();
}; };
let i = self.active_tab;
let interactive_lengthen = self.tabs[i] let interactive_lengthen = self.tabs[i]
.active_grip .active_grip
.as_ref() .as_ref()
@ -1141,6 +1145,9 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
let Some(popup) = self.grip_popup.take() else { let Some(popup) = self.grip_popup.take() else {
return Task::none(); return Task::none();
}; };
if self.reject_locked_edit(i, popup.handle) {
return Task::none();
}
self.grip_hover = None; self.grip_hover = None;
let Some(item) = popup.items.get(idx).cloned() else { let Some(item) = popup.items.get(idx).cloned() else {
return Task::none(); return Task::none();
@ -1517,6 +1524,9 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
self.ribbon.close_dropdown(); self.ribbon.close_dropdown();
let handles = self.property_target_handles(i); let handles = self.property_target_handles(i);
if handles.is_empty() { if handles.is_empty() {
if self.has_property_selection(i) {
return Task::none();
}
// No selection — change the creation default. Persist // No selection — change the creation default. Persist
// into the tab's header (CLAYER) so it survives a tab // into the tab's header (CLAYER) so it survives a tab
// switch and rides the next save. #21. // switch and rides the next save. #21.
@ -1559,6 +1569,9 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
self.ribbon.close_dropdown(); self.ribbon.close_dropdown();
let handles = self.property_target_handles(i); let handles = self.property_target_handles(i);
if handles.is_empty() { if handles.is_empty() {
if self.has_property_selection(i) {
return Task::none();
}
// Persist the new default into the tab's header so it // Persist the new default into the tab's header so it
// round-trips through tab switches and writes back on // round-trips through tab switches and writes back on
// save (CECOLOR). #21. // save (CECOLOR). #21.
@ -1585,6 +1598,9 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
self.ribbon.close_dropdown(); self.ribbon.close_dropdown();
let handles = self.property_target_handles(i); let handles = self.property_target_handles(i);
if handles.is_empty() { if handles.is_empty() {
if self.has_property_selection(i) {
return Task::none();
}
// Persist into the tab's header (CELTYPE). Resolve to a // Persist into the tab's header (CELTYPE). Resolve to a
// handle when the name matches a line_types entry so the // handle when the name matches a line_types entry so the
// handle-based lookup stays in sync. #21. // handle-based lookup stays in sync. #21.
@ -2361,6 +2377,9 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
/// attributes. Entry points: double-clicking such a block, or ATTEDIT. /// attributes. Entry points: double-clicking such a block, or ATTEDIT.
pub(crate) fn open_attribute_editor(&mut self, handle: acadrust::Handle) { pub(crate) fn open_attribute_editor(&mut self, handle: acadrust::Handle) {
let i = self.active_tab; let i = self.active_tab;
if self.reject_locked_edit(i, handle) {
return;
}
let doc = &self.tabs[i].scene.document; let doc = &self.tabs[i].scene.document;
// Ok((block, rows)) to open; Err(msg) to report and stay closed. The // Ok((block, rows)) to open; Err(msg) to report and stay closed. The
// borrow of `doc` ends with this match, before any `self` mutation. // borrow of `doc` ends with this match, before any `self` mutation.

View file

@ -2320,6 +2320,7 @@ impl OpenCADStudio {
if locked { "locked" } else { "unlocked" } if locked { "locked" } else { "unlocked" }
).as_ref()); ).as_ref());
self.sync_ribbon_layers(); self.sync_ribbon_layers();
self.refresh_properties();
} }
} }
Task::none() Task::none()
@ -3203,6 +3204,9 @@ impl OpenCADStudio {
Message::AnnoObjectScaleToggle(name) => { Message::AnnoObjectScaleToggle(name) => {
let i = self.active_tab; let i = self.active_tab;
if let Some(entity) = self.anno_object_scale_target { if let Some(entity) = self.anno_object_scale_target {
if self.tabs[i].scene.is_layer_locked(entity) {
return Task::none();
}
if let Some(sh) = self.tabs[i].scene.scale_handle_ensuring(&name) { if let Some(sh) = self.tabs[i].scene.scale_handle_ensuring(&name) {
self.push_undo_snapshot(i, "OBJECTSCALE"); self.push_undo_snapshot(i, "OBJECTSCALE");
let doc = &mut self.tabs[i].scene.document; let doc = &mut self.tabs[i].scene.document;
@ -4249,6 +4253,9 @@ impl OpenCADStudio {
self.ribbon.close_dropdown(); self.ribbon.close_dropdown();
let handles = self.property_target_handles(i); let handles = self.property_target_handles(i);
if handles.is_empty() { if handles.is_empty() {
if self.has_property_selection(i) {
return Task::none();
}
// Persist into the tab's header (CELWEIGHT). #21. // Persist into the tab's header (CELWEIGHT). #21.
self.tabs[i].scene.document.header.current_line_weight = lw.value(); self.tabs[i].scene.document.header.current_line_weight = lw.value();
self.tabs[i].dirty = true; self.tabs[i].dirty = true;
@ -4322,6 +4329,9 @@ impl OpenCADStudio {
let i = self.active_tab; let i = self.active_tab;
let handles = self.property_target_handles(i); let handles = self.property_target_handles(i);
if handles.is_empty() { if handles.is_empty() {
if self.has_property_selection(i) {
return Task::none();
}
self.tabs[i].scene.document.header.current_line_weight = lw.value(); self.tabs[i].scene.document.header.current_line_weight = lw.value();
self.tabs[i].dirty = true; self.tabs[i].dirty = true;
self.ribbon.active_lineweight = lw; self.ribbon.active_lineweight = lw;

View file

@ -1079,6 +1079,14 @@ impl OpenCADStudio {
// ── Grip drag ───────────────────────────────────────────── // ── Grip drag ─────────────────────────────────────────────
if let Some(grip) = self.tabs[i].active_grip.clone() { if let Some(grip) = self.tabs[i].active_grip.clone() {
if grip
.targets
.iter()
.any(|target| self.tabs[i].scene.is_layer_locked(target.handle))
{
self.cancel_active_grip_edit();
return Task::none();
}
let grip_started = Instant::now(); let grip_started = Instant::now();
let (vw, vh) = vp_size; let (vw, vh) = vp_size;
let bounds = iced::Rectangle { let bounds = iced::Rectangle {
@ -3632,8 +3640,6 @@ impl OpenCADStudio {
bounds, bounds,
candidate_handles.as_ref(), candidate_handles.as_ref(),
)); ));
// 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 // Box/lasso accumulates like individual picks
// (issue #83): a plain box adds to the current // (issue #83): a plain box adds to the current
// selection, Shift+box removes the boxed // selection, Shift+box removes the boxed
@ -3746,7 +3752,6 @@ impl OpenCADStudio {
.into_iter() .into_iter()
.filter_map(|s| Scene::handle_from_wire_name(s)) .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.passes_selection_filter(h))
.filter(|&h| !self.tabs[i].scene.is_layer_locked(h))
.collect(); .collect();
if cands.len() >= 2 { if cands.len() >= 2 {
// Overlap: open the list box at the cursor. // Overlap: open the list box at the cursor.
@ -3806,45 +3811,33 @@ impl OpenCADStudio {
// Selection filter: drop a pick whose type is excluded. // Selection filter: drop a pick whose type is excluded.
let hit = hit.filter(|&h| self.tabs[i].scene.passes_selection_filter(h)); let hit = hit.filter(|&h| self.tabs[i].scene.passes_selection_filter(h));
if let Some(handle) = hit { if let Some(handle) = hit {
if let Some(layer) = self.tabs[i].scene.locked_layer_name(handle) { // Individual picks accumulate (issue #47):
// Locked layer: visible + snappable but // each plain click adds to the selection,
// not selectable. Report and do nothing // Shift+click removes the picked entity.
// else — in particular do NOT set // PICKADD 0 (#226): a plain click
// `selection_just_completed`, or a // REPLACES the selection instead and
// gather command (MOVE's "select // Shift+click toggles membership.
// objects") would wrongly finish. if self.shift_down || self.select_remove_mode {
self.command_line.push_info(crate::tf!( // Remove was asked for by name, so it only
"Object is on locked layer \"{layer}\" — unlock the layer to select or edit it." // ever takes away — the toggle below is
).as_ref()); // Shift's PICKADD-0 behaviour, not its.
} else { if !self.select_remove_mode
// Individual picks accumulate (issue #47): && !selection_pick_add
// each plain click adds to the selection, && !self.tabs[i].scene.selected.contains(&handle)
// Shift+click removes the picked entity. {
// PICKADD 0 (#226): a plain click self.tabs[i].scene.select_entity(handle, false);
// REPLACES the selection instead and
// Shift+click toggles membership.
if self.shift_down || self.select_remove_mode {
// Remove was asked for by name, so it only
// ever takes away — the toggle below is
// Shift's PICKADD-0 behaviour, not its.
if !self.select_remove_mode
&& !selection_pick_add
&& !self.tabs[i].scene.selected.contains(&handle)
{
self.tabs[i].scene.select_entity(handle, false);
self.tabs[i].scene.expand_selection_for_groups(&[handle]);
} else {
self.tabs[i].scene.deselect_entity(handle);
}
} else {
self.tabs[i]
.scene
.select_entity(handle, !selection_pick_add);
self.tabs[i].scene.expand_selection_for_groups(&[handle]); self.tabs[i].scene.expand_selection_for_groups(&[handle]);
} else {
self.tabs[i].scene.deselect_entity(handle);
} }
self.refresh_properties(); } else {
selection_just_completed = true; self.tabs[i]
.scene
.select_entity(handle, !selection_pick_add);
self.tabs[i].scene.expand_selection_for_groups(&[handle]);
} }
self.refresh_properties();
selection_just_completed = true;
} else { } else {
// Empty-space click only ARMS a box here; it // Empty-space click only ARMS a box here; it
// no longer clears the selection, so a box can // no longer clears the selection, so a box can
@ -4011,8 +4004,6 @@ impl OpenCADStudio {
)); ));
// Selection filter: keep only allowed types. // Selection filter: keep only allowed types.
handles.retain(|&h| self.tabs[i].scene.passes_selection_filter(h)); 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 // Accumulate (issue #83): a plain box adds to the
// current selection, Shift+box removes the boxed // current selection, Shift+box removes the boxed
// entities. An empty box leaves the selection alone // entities. An empty box leaves the selection alone

View file

@ -178,6 +178,10 @@ impl OpenCADStudio {
/// anonymous-block member visible/invisible per the state, then rebuild. /// anonymous-block member visible/invisible per the state, then rebuild.
pub(super) fn apply_visibility_state(&mut self, insert_handle: Handle, state_idx: usize) { pub(super) fn apply_visibility_state(&mut self, insert_handle: Handle, state_idx: usize) {
let i = self.active_tab; let i = self.active_tab;
if self.reject_locked_edit(i, insert_handle) {
self.visibility_popup = None;
return;
}
// Resolve everything against an immutable borrow first. // Resolve everything against an immutable borrow first.
let mapping = { let mapping = {

View file

@ -327,6 +327,9 @@ impl Scene {
#[cfg_attr(target_arch = "wasm32", allow(dead_code))] #[cfg_attr(target_arch = "wasm32", allow(dead_code))]
pub fn update_entity(&mut self, mut entity: EntityType) -> bool { pub fn update_entity(&mut self, mut entity: EntityType) -> bool {
let handle = entity.common().handle; let handle = entity.common().handle;
if self.is_layer_locked(handle) {
return false;
}
let Some(existing) = self.document.get_entity(handle) else { let Some(existing) = self.document.get_entity(handle) else {
return false; return false;
}; };

View file

@ -6412,8 +6412,7 @@ impl Scene {
} }
/// True when `handle`'s entity sits on a locked layer. Locked objects stay /// True when `handle`'s entity sits on a locked layer. Locked objects stay
/// visible and snappable but cannot be selected or modified — callers in /// visible, snappable and selectable, but mutation paths must skip them.
/// the pick / modify paths consult this to skip them.
pub fn is_layer_locked(&self, handle: Handle) -> bool { pub fn is_layer_locked(&self, handle: Handle) -> bool {
self.document self.document
.get_entity(handle) .get_entity(handle)
@ -7807,7 +7806,6 @@ impl Scene {
)); ));
} }
handles.retain(|&h| self.passes_selection_filter(h)); handles.retain(|&h| self.passes_selection_filter(h));
handles.retain(|&h| !self.is_layer_locked(h));
handles handles
} }

View file

@ -551,7 +551,7 @@ impl Scene {
} }
pub fn copy_entities(&mut self, handles: &[Handle], t: &EntityTransform) -> Vec<Handle> { pub fn copy_entities(&mut self, handles: &[Handle], t: &EntityTransform) -> Vec<Handle> {
// Objects on a locked layer can't be copied (they can't be selected). // Objects on a locked layer can be selected but not copied.
let clones: Vec<(Handle, EntityType)> = handles let clones: Vec<(Handle, EntityType)> = handles
.iter() .iter()
.filter(|&&h| !self.is_layer_locked(h)) .filter(|&&h| !self.is_layer_locked(h))

View file

@ -164,16 +164,6 @@ impl Scene {
let Some(e) = self.document.get_entity(h) else { let Some(e) = self.document.get_entity(h) else {
continue; 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;
}
let type_ok = type_name.is_none_or(|t| entity_type_name(e) == t); let type_ok = type_name.is_none_or(|t| entity_type_name(e) == t);
let prop_ok = if !type_ok { let prop_ok = if !type_ok {
true true

View file

@ -347,7 +347,7 @@ struct SelectionCanvas {
/// is usable instead of the cursor vanishing over it. (#227) /// is usable instead of the cursor vanishing over it. (#227)
suppressed: bool, suppressed: bool,
/// The entity under the crosshair is on a locked layer — draw a small lock /// The entity under the crosshair is on a locked layer — draw a small lock
/// badge by the cursor so the user knows it can't be selected/edited. /// badge by the cursor so the user knows it can't be edited.
hover_locked: bool, hover_locked: bool,
/// Background of the active drawing space. Crosshair contrast follows this /// Background of the active drawing space. Crosshair contrast follows this
/// rather than the UI theme, which may be light over a dark model viewport. /// rather than the UI theme, which may be light over a dark model viewport.
@ -1042,7 +1042,7 @@ impl canvas::Program<Message> for SelectionCanvas {
// Locked-layer badge: a small padlock beside the crosshair when // Locked-layer badge: a small padlock beside the crosshair when
// the hovered object sits on a locked layer (issue: locked // the hovered object sits on a locked layer (issue: locked
// objects are visible + snappable but not selectable/editable). // objects are visible, snappable and selectable but not editable).
if self.hover_locked { if self.hover_locked {
let warning = theme.palette().warning.base; let warning = theme.palette().warning.base;
let amber = warning.color.scale_alpha(0.98); let amber = warning.color.scale_alpha(0.98);