diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index 00268c3bc..11b618695 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -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( @@ -2534,7 +2534,7 @@ impl OpenCADStudio { CmdResult::EditTableCell { handle, point } => { // TABLEDIT's pick: end the pick phase and hand (table, point) // to the shared cell-edit launcher. A miss or a locked cell - // re-prompts, matching AutoCAD's select-until-valid loop. + // re-prompts until a valid table cell is selected. use crate::modules::annotate::table_cmd::{table_cell_at, TableCellEditStart}; self.tabs[i].active_cmd = None; self.tabs[i].snap_result = None; @@ -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,8 @@ 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 = delete_objects != 0; let path = self.tabs[i].scene.document.get_entity(path_handle).cloned(); let mut profiles = Vec::new(); let mut failed = 0usize; @@ -4120,6 +4121,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 +4141,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 +4187,14 @@ impl OpenCADStudio { }).collect::>(); 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 = delete_objects != 0; + 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::>(); - 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(§ions, &guides, path, &available, mode, options) @@ -4189,6 +4203,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 +4237,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; @@ -5835,6 +5873,71 @@ fn resample_widths(source: &[(f64, f64)], count: usize) -> Vec<(f64, f64)> { .collect() } +#[cfg(all(test, not(target_arch = "wasm32")))] +mod delobj_tests { + use super::*; + use acadrust::types::Vector3; + + fn sweep_source_presence(value: i16, mode: crate::command::ExtrudeMode) -> (bool, bool) { + let mut app = OpenCADStudio::new_for_test(); + app.automation_op(r#"{"op":"new"}"#); + let i = app.active_tab; + let mut circle = acadrust::Circle::new(); + circle.radius = 2.0; + let profile = app.tabs[i] + .scene + .add_entity(acadrust::EntityType::Circle(circle)); + let path = app.tabs[i].scene.add_entity(acadrust::EntityType::Line( + acadrust::Line::from_points( + Vector3::new(0.0, 0.0, 0.0), + Vector3::new(0.0, 0.0, 5.0), + ), + )); + app.delete_objects = value; + + let _ = app.apply_cmd_result(CmdResult::SweepEntities { + handles: vec![profile], + path_handle: path, + mode, + options: crate::command::SweepOptions::default(), + color: [1.0; 4], + }); + + let created = app.tabs[i].scene.document.entities().any(|entity| match mode { + crate::command::ExtrudeMode::Solid => { + matches!(entity, acadrust::EntityType::Solid3D(_)) + } + crate::command::ExtrudeMode::Surface => { + matches!(entity, acadrust::EntityType::Surface(_)) + } + }); + assert!(created, "SWEEP must produce the requested result"); + ( + app.tabs[i].scene.document.get_entity(profile).is_some(), + app.tabs[i].scene.document.get_entity(path).is_some(), + ) + } + + #[test] + fn sweep_applies_all_four_source_deletion_modes() { + let solid_expectations = [(true, true), (false, true), (false, false), (false, false)]; + for (value, expected) in solid_expectations.into_iter().enumerate() { + assert_eq!( + sweep_source_presence(value as i16, crate::command::ExtrudeMode::Solid), + expected, + ); + } + assert_eq!( + sweep_source_presence(2, crate::command::ExtrudeMode::Surface), + (false, false), + ); + assert_eq!( + sweep_source_presence(3, crate::command::ExtrudeMode::Surface), + (false, true), + ); + } +} + #[cfg(all(test, not(target_arch = "wasm32")))] mod thicken_tests { use super::*; diff --git a/src/app/commands/draw.rs b/src/app/commands/draw.rs index a17d1a9cb..6807abfa1 100644 --- a/src/app/commands/draw.rs +++ b/src/app/commands/draw.rs @@ -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); + } + } +} diff --git a/src/app/commands/styleprops.rs b/src/app/commands/styleprops.rs index 939eb38fb..4efc41f80 100644 --- a/src/app/commands/styleprops.rs +++ b/src/app/commands/styleprops.rs @@ -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::().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::() + .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); + } } diff --git a/src/app/mod.rs b/src/app/mod.rs index 1f34f7c8a..7ecbe186a 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -347,6 +347,10 @@ pub(crate) enum TextEntryMode { FreeText, } +pub(crate) fn delobj_deletes_auxiliary(value: i16, creates_surface: bool) -> bool { + value == 2 || (value == 3 && !creates_surface) +} + pub(super) struct OpenCADStudio { start: Instant, control: control::State, @@ -566,6 +570,8 @@ pub(super) struct OpenCADStudio { /// `MIRRTEXT`) — the next command-line entry is its new value, empty keeps /// the current one. pending_setvar: Option, + /// 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 +3430,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, diff --git a/src/app/model_ops.rs b/src/app/model_ops.rs index d767d9005..4bb5d2041 100644 --- a/src/app/model_ops.rs +++ b/src/app/model_ops.rs @@ -2293,7 +2293,9 @@ impl super::OpenCADStudio { surfaces.push(surf); } self.push_undo_snapshot(i, "CONVTOSURFACE"); - self.tabs[i].scene.erase_entities(&handles); + if self.delete_objects != 0 { + self.tabs[i].scene.erase_entities(&handles); + } let n = surfaces.len(); for surf in surfaces { self.tabs[i].scene.add_entity(EntityType::Surface(surf)); diff --git a/src/app/settings.rs b/src/app/settings.rs index fb63859e2..bdfab8ff9 100644 --- a/src/app/settings.rs +++ b/src/app/settings.rs @@ -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, diff --git a/src/app/update/file.rs b/src/app/update/file.rs index 4799d1cd9..771b2603b 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -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;