Implement DELOBJ source deletion policies

This commit is contained in:
Marcel committed 2026-09-09 20:49:29 +02:00
1 parent f69f0b32be
commit 10d1d9b7d9
7 files changed
+196 -19

No files matched your search

+49 -9
View File
@@ -1779,7 +1779,7 @@ impl OpenCADStudio {
} => {
let label = self.history_label_from_active_cmd(i, "SOLID");
let erase_source = erase_source.filter(|handle| {
self.tabs[i].scene.document.header.delete_objects
self.delete_objects != 0
&& !self.tabs[i].scene.is_layer_locked(*handle)
});
let pending = self.begin_undo(
@@ -3661,7 +3661,7 @@ impl OpenCADStudio {
taper_angle,
color: _,
} => {
let delete_sources = self.tabs[i].scene.document.header.delete_objects;
let delete_sources = self.delete_objects != 0;
if handles.is_empty()
|| handles
.iter()
@@ -3948,7 +3948,7 @@ impl OpenCADStudio {
self.restore_pre_cmd_tangent();
return Task::none();
}
let delete_sources = self.tabs[i].scene.document.header.delete_objects;
let delete_sources = self.delete_objects != 0;
use crate::command::ExtrudeMode;
use crate::modules::insert::solid3d_cmds::{
empty_revolved_surface, empty_solid3d,
@@ -4082,7 +4082,9 @@ impl OpenCADStudio {
use crate::command::ExtrudeMode;
use crate::modules::insert::solid3d_cmds::empty_solid3d;
use crate::scene::model::sweep_model;
let delete_sources = self.tabs[i].scene.document.header.delete_objects;
let delete_objects = self.delete_objects;
let delete_profiles =
crate::app::delobj_deletes_profiles(delete_objects);
let path = self.tabs[i].scene.document.get_entity(path_handle).cloned();
let mut profiles = Vec::new();
let mut failed = 0usize;
@@ -4120,6 +4122,14 @@ impl OpenCADStudio {
failed += 1;
continue;
};
let deletes_path = crate::app::delobj_deletes_auxiliary(
delete_objects,
surface,
);
if deletes_path && self.tabs[i].scene.is_layer_locked(path_handle) {
failed += 1;
continue;
}
let created = if surface {
self.add_surface_model(sweep_model::swept_surface_entity(&record), body)
} else {
@@ -4132,9 +4142,12 @@ impl OpenCADStudio {
failed += 1;
} else {
created_handles.push(created);
if delete_sources {
if delete_profiles {
consumed.push(handle);
}
if deletes_path {
consumed.push(path_handle);
}
}
}
if !created_handles.is_empty() {
@@ -4175,12 +4188,15 @@ impl OpenCADStudio {
}).collect::<Vec<_>>();
sources.sort_unstable_by_key(|handle| handle.value());
sources.dedup();
let delete_sources = self.tabs[i].scene.document.header.delete_objects;
let locked = delete_sources && sources.iter().any(|handle| self.tabs[i].scene.is_layer_locked(*handle));
let delete_objects = self.delete_objects;
let delete_sections =
crate::app::delobj_deletes_profiles(delete_objects);
let section_locked = delete_sections
&& sources.iter().any(|handle| self.tabs[i].scene.is_layer_locked(*handle));
let available = sources.iter().chain(guides.iter()).copied().chain(path)
.filter_map(|handle| self.tabs[i].scene.document.get_entity(handle)
.cloned().map(|entity| (handle, entity))).collect::<Vec<_>>();
let result = if locked {
let result = if section_locked {
Err("LOFT: a source is on a locked layer; disable source deletion or unlock it.".to_string())
} else {
loft_command_model::record(&sections, &guides, path, &available, mode, options)
@@ -4189,6 +4205,19 @@ impl OpenCADStudio {
match result {
Ok((body, record)) => {
let surface = record.parameters.as_ref().is_some_and(|settings| settings.surface);
let delete_auxiliary = crate::app::delobj_deletes_auxiliary(
delete_objects,
surface,
);
if delete_auxiliary
&& guides.iter().copied().chain(path)
.any(|handle| self.tabs[i].scene.is_layer_locked(handle))
{
self.command_line.push_error(
"LOFT: a source is on a locked layer; disable source deletion or unlock it.",
);
return Task::none();
}
let dirty_before = self.tabs[i].dirty;
let pending = self.begin_undo(i, "LOFT", 1, true);
let created = if surface {
@@ -4210,7 +4239,18 @@ impl OpenCADStudio {
self.command_line.push_error(crate::t!("LOFT could not create a complete display. The source sections were preserved.").as_ref());
return Task::none();
} else {
if delete_sources { self.tabs[i].scene.erase_entities(&sources); }
let mut consumed = if delete_sections {
sources.clone()
} else {
Vec::new()
};
if delete_auxiliary {
consumed.extend(guides.iter().copied());
consumed.extend(path);
}
consumed.sort_unstable_by_key(|handle| handle.value());
consumed.dedup();
self.tabs[i].scene.erase_entities(&consumed);
self.tabs[i].scene.deselect_all();
self.tabs[i].scene.select_entity(created, true);
self.tabs[i].dirty = true;
+51 -1
View File
@@ -1113,7 +1113,8 @@ impl OpenCADStudio {
use acadrust::entities::Region;
use acadrust::types::Vector3;
let mut regions = Vec::new();
for (_, e) in self.tabs[i].scene.selected_entities().iter() {
let mut sources = Vec::new();
for (handle, e) in self.tabs[i].scene.selected_entities().iter() {
let supported = matches!(
e,
acadrust::EntityType::LwPolyline(pl)
@@ -1136,6 +1137,7 @@ impl OpenCADStudio {
);
region.common.layer = self.tabs[i].active_layer.clone();
regions.push((region, body));
sources.push(*handle);
}
}
if regions.is_empty() {
@@ -1154,6 +1156,10 @@ impl OpenCADStudio {
}
created.push(handle);
}
if self.delete_objects != 0 {
self.tabs[i].scene.erase_entities(&sources);
self.refresh_properties();
}
self.tabs[i].dirty = true;
self.command_line
.push_output(crate::tf!("REGION: created {count} region(s).").as_ref());
@@ -1530,3 +1536,47 @@ impl OpenCADStudio {
Some(self.finish_dispatch(cmd))
}
}
#[cfg(test)]
mod region_tests {
use crate::app::OpenCADStudio;
#[test]
fn region_respects_delobj_and_preserves_unconverted_sources() {
for delete_sources in [false, true] {
let mut app = OpenCADStudio::new_for_test();
app.automation_op(r#"{"op":"new"}"#);
app.automation_op(r#"{"op":"run","cmd":"CIRCLE 5,5 3"}"#);
app.automation_op(r#"{"op":"run","cmd":"LINE 0,0 10,10"}"#);
let i = app.active_tab;
let sources: Vec<_> = app.tabs[i].scene.document.entities()
.map(|entity| {
(entity.common().handle, matches!(entity, acadrust::EntityType::Circle(_)))
})
.collect();
assert_eq!(sources.len(), 2);
let handles: Vec<_> = sources.iter().map(|(handle, _)| *handle).collect();
app.tabs[i].scene.select_entities(&handles);
app.delete_objects = i16::from(delete_sources);
let _ = app.dispatch_command("REGION");
for (handle, is_circle) in &sources {
assert_eq!(
app.tabs[i].scene.document.get_entity(*handle).is_some(),
!delete_sources || !is_circle,
);
}
let region_count = app.tabs[i].scene.document.entities()
.filter(|entity| matches!(entity, acadrust::EntityType::Region(_)))
.count();
assert_eq!(region_count, 1);
app.automation_op(r#"{"op":"undo"}"#);
for (handle, _) in &sources {
assert!(app.tabs[i].scene.document.get_entity(*handle).is_some());
}
assert_eq!(app.tabs[i].scene.document.entities().count(), 2);
}
}
}
+33 -8
View File
@@ -1159,6 +1159,11 @@ impl OpenCADStudio {
"0" | "OFF" | "FALSE" => Some(false),
_ => None,
};
let current_delete_objects = self.delete_objects;
let requested_delete_objects = (name == "DELOBJ")
.then(|| value.as_deref()?.parse::<i16>().ok())
.flatten()
.filter(|value| (0..=3).contains(value));
let outcome: Result<(String, bool), String> = {
let h = &mut self.tabs[i].scene.document.header;
match name.as_str() {
@@ -1633,15 +1638,18 @@ impl OpenCADStudio {
}
},
"DELOBJ" => match &value {
Some(v) => parse_bool(v)
.map(|b| {
h.delete_objects = b;
(format!("DELOBJ = {}", b as i32), true)
Some(v) => v
.parse::<i16>()
.ok()
.filter(|value| (0..=3).contains(value))
.map(|value| {
(format!("DELOBJ = {value}"), true)
})
.ok_or_else(|| "SETVAR: 0 or 1 required.".into()),
None => {
Ok((format!("DELOBJ = {}", h.delete_objects as i32), false))
}
.ok_or_else(|| "SETVAR: integer from 0 to 3 required.".into()),
None => Ok((
format!("DELOBJ = {current_delete_objects}"),
false,
)),
},
"PLINEGEN" => match &value {
Some(v) => parse_bool(v)
@@ -2119,6 +2127,9 @@ impl OpenCADStudio {
};
match outcome {
Ok((msg, changed)) => {
if let Some(value) = requested_delete_objects {
self.delete_objects = value;
}
if changed {
if matches!(
name.as_str(),
@@ -2144,6 +2155,7 @@ impl OpenCADStudio {
| "GRIPHOT"
| "GRIPHOVER"
| "GRIPOBJLIMIT"
| "DELOBJ"
) {
self.persist_settings_if_changed();
} else {
@@ -3120,4 +3132,17 @@ mod tests {
assert!(app.tabs[app.active_tab].active_cmd.is_none());
assert_eq!(app.commandline_fade_ms, 5000);
}
#[test]
fn delobj_accepts_values_zero_through_three() {
let mut app = fresh_app();
for value in 0..=3 {
let _ = app.run_command_line(&format!("SETVAR DELOBJ {value}"));
assert_eq!(app.delete_objects, value);
}
let _ = app.run_command_line("SETVAR DELOBJ 4");
assert_eq!(app.delete_objects, 3);
}
}
+49
View File
@@ -347,6 +347,52 @@ pub(crate) enum TextEntryMode {
FreeText,
}
pub(crate) fn delobj_deletes_profiles(value: i16) -> bool {
value != 0
}
pub(crate) fn delobj_deletes_auxiliary(value: i16, creates_surface: bool) -> bool {
value == 2 || (value == 3 && !creates_surface)
}
pub(crate) fn delobj_deletes_conversion_source(value: i16) -> bool {
value >= 1
}
#[cfg(test)]
mod delobj_policy_tests {
use super::{
delobj_deletes_auxiliary, delobj_deletes_conversion_source,
delobj_deletes_profiles,
};
#[test]
fn values_zero_through_three_select_the_expected_sources() {
assert_eq!(
(0..=3).map(delobj_deletes_profiles).collect::<Vec<_>>(),
[false, true, true, true]
);
assert_eq!(
(0..=3)
.map(|value| delobj_deletes_auxiliary(value, true))
.collect::<Vec<_>>(),
[false, false, true, false]
);
assert_eq!(
(0..=3)
.map(|value| delobj_deletes_auxiliary(value, false))
.collect::<Vec<_>>(),
[false, false, true, true]
);
assert_eq!(
(0..=3)
.map(delobj_deletes_conversion_source)
.collect::<Vec<_>>(),
[false, true, true, true]
);
}
}
pub(super) struct OpenCADStudio {
start: Instant,
control: control::State,
@@ -566,6 +612,8 @@ pub(super) struct OpenCADStudio {
/// `MIRRTEXT`) — the next command-line entry is its new value, empty keeps
/// the current one.
pending_setvar: Option<String>,
/// DELOBJ system variable (0–3), shared by every open drawing.
delete_objects: i16,
/// Cursor is hovering over the UCS icon body — drives the hover highlight.
ucs_icon_hover: bool,
/// UCS icon is selected (clicked): its grips are shown and draggable.
@@ -3424,6 +3472,7 @@ impl OpenCADStudio {
block_usage_last_persist: None,
awaiting_vports: false,
pending_setvar: None,
delete_objects: 1,
ucs_icon_hover: false,
ucs_icon_selected: false,
ucs_grip_drag: None,
+3 -1
View File
@@ -2293,7 +2293,9 @@ impl super::OpenCADStudio {
surfaces.push(surf);
}
self.push_undo_snapshot(i, "CONVTOSURFACE");
self.tabs[i].scene.erase_entities(&handles);
if crate::app::delobj_deletes_conversion_source(self.delete_objects) {
self.tabs[i].scene.erase_entities(&handles);
}
let n = surfaces.len();
for surf in surfaces {
self.tabs[i].scene.add_entity(EntityType::Surface(surf));
+9
View File
@@ -219,6 +219,10 @@ pub struct UserSettings {
/// the current creation layer/style. Registry-style, app-level preference.
#[serde(default = "default_dimension_continue_mode")]
pub dimension_continue_mode: i16,
/// DELOBJ: source-geometry deletion policy (0–3). Registry-style,
/// app-level preference; first-run default is 1.
#[serde(default = "default_delete_objects")]
pub delete_objects: i16,
/// TEXTFILL: fill TrueType glyphs (true) or draw them hollow (false).
pub textfill: bool,
/// When true, saving over an existing file first copies it to a sibling
@@ -275,6 +279,10 @@ fn default_clipromptlines() -> i32 {
3
}
fn default_delete_objects() -> i16 {
1
}
fn default_commandline_fade_ms() -> i32 {
3000
}
@@ -339,6 +347,7 @@ impl Default for UserSettings {
texteditmode: false,
quick_dimension_snap_priority: 0,
dimension_continue_mode: 1,
delete_objects: 1,
textfill: true,
backup_on_save: true,
file_assoc_enabled: true,
+2
View File
@@ -392,6 +392,7 @@ impl OpenCADStudio {
texteditmode: self.texteditmode,
quick_dimension_snap_priority: self.quick_dimension_snap_priority,
dimension_continue_mode: self.dimension_continue_mode,
delete_objects: self.delete_objects,
textfill: crate::scene::text::sdf_atlas::textfill(),
backup_on_save: self.backup_on_save,
file_assoc_enabled: self.file_assoc_enabled,
@@ -460,6 +461,7 @@ impl OpenCADStudio {
self.texteditmode = s.texteditmode;
self.quick_dimension_snap_priority = s.quick_dimension_snap_priority.min(1);
self.dimension_continue_mode = s.dimension_continue_mode.clamp(0, 1);
self.delete_objects = s.delete_objects.clamp(0, 3);
crate::scene::text::sdf_atlas::set_textfill(s.textfill);
self.backup_on_save = s.backup_on_save;
self.file_assoc_enabled = s.file_assoc_enabled;