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 committed 2026-08-12 12:46:41 +03:00
1 parent ca331fff48
commit 0be9c41773
25 files changed
+524 -118

No files matched your search

+49 -21
View File
@@ -71,6 +71,7 @@ impl OpenCADStudio {
.selected_entities()
.into_iter()
.map(|(h, _)| h)
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.collect();
if handles.is_empty() {
use crate::modules::draw::select::SelectObjectsCommand;
@@ -106,6 +107,7 @@ impl OpenCADStudio {
.selected_entities()
.into_iter()
.map(|(h, _)| h)
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.collect();
if handles.is_empty() {
use crate::modules::draw::select::SelectObjectsCommand;
@@ -132,6 +134,7 @@ impl OpenCADStudio {
.selected_entities()
.into_iter()
.map(|(h, _)| h)
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.collect();
if coords.len() == 3 && !handles.is_empty() {
let base = glam::DVec3::new(coords[0], coords[1], coords[2]);
@@ -160,6 +163,7 @@ impl OpenCADStudio {
.selected_entities()
.into_iter()
.map(|(h, _)| h)
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.collect();
if handles.is_empty() {
use crate::modules::draw::select::SelectObjectsCommand;
@@ -393,32 +397,55 @@ impl OpenCADStudio {
.collect()
})
.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");
let mut synced = 0usize;
let mut changes = Vec::new();
for e in self.tabs[i].scene.document.entities_mut() {
if let acadrust::EntityType::Insert(ins) = e {
if ins.block_name.eq_ignore_ascii_case(&block) {
ins.attributes.retain(|a| {
attdefs.iter().any(|(t, _)| t.eq_ignore_ascii_case(&a.tag))
});
for (tag, default) in &attdefs {
if !ins
.attributes
.iter()
.any(|a| a.tag.eq_ignore_ascii_case(tag))
{
ins.attributes
.push(acadrust::entities::AttributeEntity::new(
tag.clone(),
default.clone(),
));
}
for handle in inserts {
let Some(acadrust::EntityType::Insert(ins)) =
self.tabs[i].scene.document.get_entity_mut(handle)
else {
continue;
};
ins.attributes.retain(|a| {
attdefs.iter().any(|(t, _)| t.eq_ignore_ascii_case(&a.tag))
});
for (tag, default) in &attdefs {
if !ins
.attributes
.iter()
.any(|a| a.tag.eq_ignore_ascii_case(tag))
{
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].dirty = true;
@@ -626,6 +653,7 @@ impl OpenCADStudio {
.iter()
.filter(|(_, e)| matches!(e, acadrust::EntityType::Insert(_)))
.map(|(h, _)| *h)
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.collect();
if inserts.is_empty() {
self.command_line
+13 -2
View File
@@ -86,6 +86,7 @@ impl OpenCADStudio {
.selected_entities()
.iter()
.map(|(h, _)| *h)
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.collect();
if handles.is_empty() {
self.command_line
@@ -208,6 +209,7 @@ impl OpenCADStudio {
.selected_entities()
.iter()
.map(|(h, _)| *h)
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.collect();
if handles.is_empty() {
self.command_line
@@ -297,6 +299,7 @@ impl OpenCADStudio {
)
})
.map(|(h, _)| *h)
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.collect();
if handles.is_empty() {
self.command_line
@@ -374,6 +377,7 @@ impl OpenCADStudio {
.iter()
.filter(|(_, e)| matches!(e, acadrust::EntityType::Text(_)))
.map(|(h, _)| *h)
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.collect();
if handles.is_empty() {
self.command_line
@@ -469,7 +473,9 @@ impl OpenCADStudio {
.selected_entities()
.iter()
.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))
}
_ => None,
@@ -1127,7 +1133,12 @@ impl OpenCADStudio {
"EXPLODE" => {
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() {
use crate::modules::draw::select::SelectObjectsCommand;
let cmd = SelectObjectsCommand::new("EXPLODE");
+39 -5
View File
@@ -30,6 +30,7 @@ impl OpenCADStudio {
.selected_entities()
.iter()
.map(|(h, _)| *h)
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.collect();
let mut found = false;
for sh in &selected_handles {
@@ -389,8 +390,20 @@ impl OpenCADStudio {
"SET" => {
let app = parts.get(1).copied().unwrap_or("OpenCADStudio");
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");
for sh in &selected_handles {
for sh in &editable {
if let Some(entity) =
self.tabs[i].scene.document.get_entity_mut(*sh)
{
@@ -402,13 +415,25 @@ impl OpenCADStudio {
self.tabs[i].dirty = true;
self.command_line.push_output(crate::tf!(
"XDATA: set [{app}] = \"{val}\" on {} entity/entities.",
selected_handles.len()
editable.len()
).as_ref());
}
"CLEAR" => {
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");
for sh in &selected_handles {
for sh in &editable {
if let Some(entity) =
self.tabs[i].scene.document.get_entity_mut(*sh)
{
@@ -600,6 +625,7 @@ impl OpenCADStudio {
.selected_entities()
.iter()
.map(|(h, _)| *h)
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.collect();
if handles.is_empty() {
self.command_line
@@ -706,6 +732,7 @@ impl OpenCADStudio {
.selected_entities()
.iter()
.map(|(h, _)| *h)
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.collect();
if handles.is_empty() {
self.command_line
@@ -765,8 +792,13 @@ impl OpenCADStudio {
self.command_line.push_info(crate::t!("Usage: HYPERLINK <url> (select objects first)").as_ref());
return Some(Task::none());
}
let handles: Vec<acadrust::Handle> =
self.tabs[i].scene.selected_entities().iter().map(|(h, _)| *h).collect();
let handles: Vec<acadrust::Handle> = self.tabs[i]
.scene
.selected_entities()
.iter()
.map(|(h, _)| *h)
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.collect();
if handles.is_empty() {
self.command_line.push_error(crate::t!("HYPERLINK: select objects first.").as_ref());
return Some(Task::none());
@@ -816,6 +848,7 @@ impl OpenCADStudio {
.selected_entities()
.iter()
.map(|(h, _)| *h)
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.collect();
if handles.is_empty() {
self.command_line
@@ -938,6 +971,7 @@ impl OpenCADStudio {
.selected_entities()
.iter()
.map(|(handle, _)| *handle)
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.collect();
if handles.is_empty() {
self.command_line
+19 -4
View File
@@ -145,6 +145,9 @@ impl OpenCADStudio {
let mut changed = 0usize;
self.push_undo_snapshot(i, "ATTEDIT");
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]
.scene
.document
@@ -195,10 +198,22 @@ impl OpenCADStudio {
let sub = cmd.split_whitespace().nth(1).unwrap_or("").to_uppercase();
match sub.as_str() {
"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");
let mut count = 0usize;
for entity in self.tabs[i].scene.document.entities_mut() {
if let acadrust::EntityType::AttributeDefinition(ad) = entity {
for handle in handles {
if let Some(acadrust::EntityType::AttributeDefinition(ad)) =
self.tabs[i].scene.document.get_entity_mut(handle)
{
match sub.as_str() {
"ON" => {
ad.flags.invisible = false;
@@ -208,8 +223,6 @@ impl OpenCADStudio {
ad.flags.invisible = true;
count += 1;
}
"NORMAL" => { /* leave existing flags — they are already the "normal" state */
}
_ => {}
}
}
@@ -490,6 +503,7 @@ impl OpenCADStudio {
.selected_entities()
.into_iter()
.map(|(h, _)| h)
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.collect();
if handles.is_empty() {
use crate::modules::draw::select::SelectObjectsCommand;
@@ -730,6 +744,7 @@ impl OpenCADStudio {
.selected_entities()
.into_iter()
.map(|(h, _)| h)
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.collect();
if handles.is_empty() {
use crate::modules::draw::select::SelectObjectsCommand;
+15 -4
View File
@@ -16,8 +16,6 @@ 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 {
@@ -362,6 +360,9 @@ impl OpenCADStudio {
let handle_u64: u64 = cmd["BEDIT_BEGIN:".len()..].parse().unwrap_or(0);
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) {
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 insert_handle = Handle::new(handle_u64);
if self.reject_locked_edit(i, insert_handle) {
return Some(Task::none());
}
// Get INSERT entity.
let insert = match self.tabs[i].scene.document.get_entity(insert_handle) {
@@ -995,9 +999,13 @@ impl OpenCADStudio {
.document
.entities()
.map(|e| e.common().handle)
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.collect()
} 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() {
@@ -1165,7 +1173,10 @@ impl OpenCADStudio {
handles.clone()
} else {
handles.iter().copied().take(1).collect()
};
}
.into_iter()
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.collect();
if targets.is_empty() {
self.command_line
.push_output(crate::tf!("FIND: \"{}\" not found.", search).as_ref());
+2
View File
@@ -106,6 +106,7 @@ impl OpenCADStudio {
}
self.push_undo_snapshot(i, "LAYER LOCK");
self.tabs[i].dirty = true;
self.refresh_properties();
self.command_line.push_output(crate::t!("LAYER: layers locked.").as_ref());
}
"UNLOCK" | "UL" => {
@@ -116,6 +117,7 @@ impl OpenCADStudio {
}
self.push_undo_snapshot(i, "LAYER UNLOCK");
self.tabs[i].dirty = true;
self.refresh_properties();
self.command_line.push_output(crate::t!("LAYER: layers unlocked.").as_ref());
}
"COLOR" | "C" => {
+3
View File
@@ -399,6 +399,7 @@ impl OpenCADStudio {
self.tabs[i].dirty = true;
self.commit_layer_undo(i, undo);
self.refresh_layer_panel();
self.refresh_properties();
self.command_line.push_info(crate::t!("Layer(s) locked.").as_ref());
}
}
@@ -508,6 +509,7 @@ impl OpenCADStudio {
self.tabs[i].dirty = true;
self.commit_layer_undo(i, undo);
self.refresh_layer_panel();
self.refresh_properties();
self.command_line.push_info(crate::t!("Layer(s) unlocked.").as_ref());
}
}
@@ -686,6 +688,7 @@ impl OpenCADStudio {
.selected_entities()
.into_iter()
.map(|(h, _)| h)
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.collect();
if handles.is_empty() {
use crate::modules::draw::groups::ungroup::UngroupCommand;
+5 -1
View File
@@ -607,6 +607,7 @@ impl OpenCADStudio {
.selected_entities()
.into_iter()
.map(|(h, _)| h)
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.collect();
if handles.is_empty() {
self.command_line
@@ -708,6 +709,7 @@ impl OpenCADStudio {
.selected_entities()
.into_iter()
.map(|(h, _)| h)
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.collect();
if handles.is_empty() {
self.command_line
@@ -756,7 +758,8 @@ impl OpenCADStudio {
.document
.entities()
.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| {
let key = crate::entities::names::dxf_name(e).to_string();
@@ -2284,6 +2287,7 @@ impl OpenCADStudio {
.selected_entities()
.iter()
.map(|(h, _)| *h)
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.collect();
if selected_handles.is_empty() {
self.command_line
+12
View File
@@ -652,6 +652,16 @@ impl OpenCADStudio {
}
})
.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.tabs[i].scene.erase_entities(&to_erase);
// Create new viewports.
@@ -814,6 +824,7 @@ impl OpenCADStudio {
.selected_entities()
.iter()
.map(|(h, _)| *h)
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.collect();
if selected.is_empty() {
self.command_line
@@ -1004,6 +1015,7 @@ impl OpenCADStudio {
.iter()
.filter(|(_, e)| matches!(e, acadrust::EntityType::Viewport(_)))
.map(|(h, _)| *h)
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.collect();
if vps.len() < 2 {
self.command_line.push_error(