diff --git a/src/app/commands.rs b/src/app/commands.rs deleted file mode 100644 index a72caea6d..000000000 --- a/src/app/commands.rs +++ /dev/null @@ -1,6082 +0,0 @@ -use super::{Message, OpenCADStudio}; -use crate::command::CadCommand; -use crate::scene::Scene; -use iced::Task; -use std::path::PathBuf; - -impl OpenCADStudio { - /// First `"{prefix}{n}"` (n ≥ 1) not already used by a block record in the - /// active drawing. Used to auto-name a paste-as-block definition. - fn unique_block_name(&self, prefix: &str) -> String { - let i = self.active_tab; - let mut n = 1; - loop { - let name = format!("{prefix}{n}"); - if self.tabs[i].scene.document.block_records.get(&name).is_none() { - return name; - } - n += 1; - } - } - - pub(super) fn dispatch_command(&mut self, cmd: &str) -> Task { - let i = self.active_tab; - // Starting a command closes any open ribbon dropdown (e.g. a style - // combo left open) so it does not stay stuck behind the new tool. - self.ribbon.close_dropdown(); - // Cancel any running command before starting a new one. - if self.tabs[i].active_cmd.is_some() { - self.tabs[i].scene.clear_preview_wire(); - self.tabs[i].active_cmd = None; - } - // Starting any command leaves interactive PAN mode (the PAN arm below - // re-enables it). - self.tabs[i].pan_mode = false; - // Reset the last committed point so the first click of the new command - // is not constrained by ortho/polar relative to a previous command's endpoint. - self.last_point = None; - // A fresh command starts at the polar/cartesian default — clear - // any `,`-driven reshape from a previous command (#35). - self.dyn_user_reshaped = false; - - if let Some(path_str) = cmd.strip_prefix("OPEN_RECENT:") { - let path = PathBuf::from(path_str); - let size = std::fs::metadata(&path).map(|m| m.len()).unwrap_or(0); - return Task::done(Message::OpenPathPicked(Some((path, size)))); - } - - // The Start (welcome) tab has no drawing to act on, so a drawing - // command would silently do nothing. Allow only the commands that - // make sense there (create / open a document, or quit) and tell the - // user otherwise instead of running a no-op. See #96. - if self.tabs[i].is_start - && !matches!( - cmd, - "NEW" | "OPEN" | "EXIT" | "QUIT" | "REPORT" | "CHANGELOG" | "ABOUT" - | "PLUGINS" | "PLUGINMANAGER" | "DONATE" | "WEBVERSION" - ) - { - self.command_line - .push_info("No drawing open. Use NEW or OPEN to start a drawing."); - return Task::none(); - } - - if crate::plugin::try_dispatch(self, i, cmd) { - // try_dispatch returns true for both finished commands and interactive - // commands that it just installed. If no command is now active, the - // tool was a one-shot and we must turn the ribbon highlight off here — - // normally apply_cmd_result does that, but plugin dispatch can return - // without producing a CmdResult. - if self.tabs[i].active_cmd.is_none() { - self.ribbon.deactivate_tool(); - } - return Task::none(); - } - - match cmd { - "NEW" => return Task::done(Message::TabNew), - "OPEN" => return Task::done(Message::OpenFile), - "SAVE" | "QSAVE" => return Task::done(Message::SaveFile), - "SAVEAS" => return Task::done(Message::SaveAs), - "UNDO" | "U" => return Task::done(Message::Undo), - "REDO" => return Task::done(Message::Redo), - "CLEAR" | "CLR" => return Task::done(Message::ClearScene), - "WIREFRAME" | "VW" => return Task::done(Message::SetWireframe(true)), - "SOLID" | "VS" => return Task::done(Message::SetWireframe(false)), - "EXIT" | "QUIT" => { - // Funnel through the OS close path so the unsaved-changes - // dialog runs before `iced::exit()`. Falls back to a hard - // exit if there's no main window registered yet. - if let Some(id) = self.main_window { - return Task::done(Message::WindowCloseRequested(id)); - } - return iced::exit(); - } - - // ── Frame-budget HUD (Phase 5.3) ─────────────────────────────── - // Toggle the per-rebuild wire-tessellation readout overlay. - "PERF" => { - self.perf_hud = !self.perf_hud; - self.command_line.push_info(if self.perf_hud { - "PERF HUD on — shows last wire re-tessellation cost" - } else { - "PERF HUD off" - }); - return Task::none(); - } - - // ── Background color ─────────────────────────────────────────── - // Usage: BACKGROUND (0–255 each) - // BACKGROUND WHITE|BLACK|GRAY|DARKGRAY|LTGRAY (preset) - // BACKGROUND RESET (restore default) - // The chosen colour is also stored as the persisted default - // (`default_bg_color` / `default_paper_bg_color`) so it survives - // restarts and applies to new drawings (#188). - cmd if cmd == "BACKGROUND" || cmd.starts_with("BACKGROUND ") => { - let args = cmd.split_whitespace().skip(1).collect::>(); - let is_paper = self.tabs[i].scene.current_layout != "Model"; - if args - .first() - .map(|s| s.eq_ignore_ascii_case("RESET")) - .unwrap_or(false) - { - if is_paper { - self.tabs[i].paper_bg_color = None; - self.tabs[i].scene.paper_bg_color = [1.0, 1.0, 1.0, 1.0]; - self.default_paper_bg_color = None; - } else { - self.tabs[i].bg_color = None; - self.tabs[i].scene.bg_color = [0.11, 0.11, 0.11, 1.0]; - self.default_bg_color = None; - } - // Wire colour adaptation (`adapt_to_bg`) reads the bg - // at tessellation time, so the cached wires need to - // refresh — otherwise a light→dark bg flip leaves - // black lines invisible against the new bg. Meshes - // bake colour into per-vertex GPU buffers at upload - // time; `recolor_meshes` rewrites the CPU side so - // the next epoch-driven re-upload picks up the new - // colour. - self.tabs[i].scene.recolor_meshes(); - self.tabs[i].scene.bump_geometry(); - self.command_line - .push_output("Background reset to default."); - } else if let Some(rgba) = parse_background_color(&args) { - if is_paper { - self.tabs[i].paper_bg_color = Some(rgba); - self.tabs[i].scene.paper_bg_color = rgba; - self.default_paper_bg_color = Some(rgba); - } else { - self.tabs[i].bg_color = Some(rgba); - self.tabs[i].scene.bg_color = rgba; - self.default_bg_color = Some(rgba); - } - self.tabs[i].scene.recolor_meshes(); - self.tabs[i].scene.bump_geometry(); - let [r, g, b, _] = rgba; - self.command_line.push_output(&format!( - "Background: rgb({}, {}, {})", - (r * 255.0).round() as u8, - (g * 255.0).round() as u8, - (b * 255.0).round() as u8 - )); - // Persisted centrally after this message via - // `persist_settings_if_changed()`. - } else { - self.command_line.push_info( - "Usage: BACKGROUND (0–255) | WHITE|BLACK|GRAY|DARKGRAY|LTGRAY | RESET", - ); - } - } - "ORTHO" => return Task::done(Message::SetProjection(true)), - "PERSP" => return Task::done(Message::SetProjection(false)), - "LAYERS" | "LA" => return Task::done(Message::ToggleLayers), - - // ── Layer object commands ────────────────────────────────────── - "LAYOFF" => { - let handles: Vec<_> = self.tabs[i] - .scene - .selected_entities() - .into_iter() - .map(|(h, _)| h) - .collect(); - if handles.is_empty() { - use crate::modules::draw::select::SelectObjectsCommand; - let cmd = SelectObjectsCommand::new("LAYOFF"); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } else { - let layers: rustc_hash::FxHashSet = self.tabs[i] - .scene - .selected_entities() - .into_iter() - .map(|(_, e)| e.common().layer.clone()) - .collect(); - self.push_undo_snapshot(i, "LAYOFF"); - for name in &layers { - if name == "0" { - continue; - } - if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(name) { - dl.turn_off(); - } - } - self.tabs[i].scene.bump_geometry(); - self.tabs[i].dirty = true; - self.refresh_layer_panel(); - self.command_line.push_info("Layer(s) turned off."); - } - } - - "LAYFRZ" => { - let handles: Vec<_> = self.tabs[i] - .scene - .selected_entities() - .into_iter() - .map(|(h, _)| h) - .collect(); - if handles.is_empty() { - use crate::modules::draw::select::SelectObjectsCommand; - let cmd = SelectObjectsCommand::new("LAYFRZ"); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } else { - let layers: rustc_hash::FxHashSet = self.tabs[i] - .scene - .selected_entities() - .into_iter() - .map(|(_, e)| e.common().layer.clone()) - .collect(); - self.push_undo_snapshot(i, "LAYFRZ"); - for name in &layers { - if name == "0" { - continue; - } - if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(name) { - dl.freeze(); - } - } - self.tabs[i].scene.bump_geometry(); - self.tabs[i].dirty = true; - self.refresh_layer_panel(); - self.command_line.push_info("Layer(s) frozen."); - } - } - - "LAYLCK" => { - let handles: Vec<_> = self.tabs[i] - .scene - .selected_entities() - .into_iter() - .map(|(h, _)| h) - .collect(); - if handles.is_empty() { - use crate::modules::draw::select::SelectObjectsCommand; - let cmd = SelectObjectsCommand::new("LAYLCK"); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } else { - let layers: rustc_hash::FxHashSet = self.tabs[i] - .scene - .selected_entities() - .into_iter() - .map(|(_, e)| e.common().layer.clone()) - .collect(); - self.push_undo_snapshot(i, "LAYLCK"); - for name in &layers { - if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(name) { - dl.lock(); - } - } - self.tabs[i].scene.bump_geometry(); - self.tabs[i].dirty = true; - self.refresh_layer_panel(); - self.command_line.push_info("Layer(s) locked."); - } - } - - "LAYMCUR" => { - let entities = self.tabs[i].scene.selected_entities(); - if entities.is_empty() { - use crate::modules::draw::select::SelectObjectsCommand; - let cmd = SelectObjectsCommand::new("LAYMCUR"); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } else { - let layer = entities[0].1.common().layer.clone(); - // Keep the document header (CLAYER) in sync, not just the - // per-tab default, so a later no-selection ribbon refresh - // (e.g. after Esc) doesn't snap back to the stale header - // layer. See #93. - let handle = self.tabs[i] - .scene - .document - .layers - .get(&layer) - .map(|l| l.handle) - .unwrap_or(acadrust::types::Handle::NULL); - self.tabs[i].scene.document.header.current_layer_name = layer.clone(); - self.tabs[i].scene.document.header.current_layer_handle = handle; - self.tabs[i].active_layer = layer.clone(); - self.ribbon.active_layer = layer.clone(); - self.tabs[i].layers.current_layer = layer.clone(); - self.tabs[i].dirty = true; - self.command_line - .push_info(&format!("Current layer set to \"{layer}\".")); - self.refresh_layer_panel(); - } - } - - "LAYON" => { - self.push_undo_snapshot(i, "LAYON"); - for name in self.tabs[i] - .scene - .document - .layers - .iter() - .map(|l| l.name.clone()) - .collect::>() - { - if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(&name) { - dl.turn_on(); - } - } - self.tabs[i].scene.bump_geometry(); - self.tabs[i].dirty = true; - self.refresh_layer_panel(); - self.command_line.push_info("All layers turned on."); - } - - "LAYTHW" => { - self.push_undo_snapshot(i, "LAYTHW"); - for name in self.tabs[i] - .scene - .document - .layers - .iter() - .map(|l| l.name.clone()) - .collect::>() - { - if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(&name) { - dl.thaw(); - } - } - self.tabs[i].scene.bump_geometry(); - self.tabs[i].dirty = true; - self.refresh_layer_panel(); - self.command_line.push_info("All layers thawed."); - } - - "LAYULK" => { - let handles: Vec<_> = self.tabs[i] - .scene - .selected_entities() - .into_iter() - .map(|(h, _)| h) - .collect(); - if handles.is_empty() { - use crate::modules::draw::select::SelectObjectsCommand; - let cmd = SelectObjectsCommand::new("LAYULK"); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } else { - let layers: rustc_hash::FxHashSet = self.tabs[i] - .scene - .selected_entities() - .into_iter() - .map(|(_, e)| e.common().layer.clone()) - .collect(); - self.push_undo_snapshot(i, "LAYULK"); - for name in &layers { - if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(name) { - dl.unlock(); - } - } - self.tabs[i].scene.bump_geometry(); - self.tabs[i].dirty = true; - self.refresh_layer_panel(); - self.command_line.push_info("Layer(s) unlocked."); - } - } - - // LAYISO — turn off all layers except those used by selected entities - "LAYISO" => { - let sel_layers: rustc_hash::FxHashSet = self.tabs[i] - .scene - .selected_entities() - .into_iter() - .map(|(_, e)| e.common().layer.clone()) - .collect(); - if sel_layers.is_empty() { - self.command_line - .push_error("LAYISO: select entities on the layers to isolate first."); - } else { - self.push_undo_snapshot(i, "LAYISO"); - let names: Vec = self.tabs[i] - .scene - .document - .layers - .iter() - .map(|l| l.name.clone()) - .collect(); - for name in names { - if !sel_layers.contains(&name) { - if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(&name) { - dl.turn_off(); - } - } - } - self.tabs[i].scene.bump_geometry(); - self.tabs[i].dirty = true; - self.refresh_layer_panel(); - self.command_line - .push_info(&format!("LAYISO: isolated {} layer(s).", sel_layers.len())); - } - } - - // ISOLATEOBJECTS — hide every object except the current selection - "ISOLATEOBJECTS" => { - if self.tabs[i].scene.selected.is_empty() { - self.command_line - .push_error("ISOLATEOBJECTS: select the objects to isolate first."); - } else { - let n = self.tabs[i].scene.selected.len(); - self.tabs[i].scene.isolate_selected(); - self.command_line.push_info(&format!( - "Isolated {n} object(s). UNISOLATEOBJECTS to restore." - )); - } - } - - // HIDEOBJECTS — hide the current selection - "HIDEOBJECTS" => { - if self.tabs[i].scene.selected.is_empty() { - self.command_line - .push_error("HIDEOBJECTS: select the objects to hide first."); - } else { - let n = self.tabs[i].scene.selected.len(); - self.tabs[i].scene.hide_selected(); - self.command_line - .push_info(&format!("Hid {n} object(s). UNISOLATEOBJECTS to restore.")); - } - } - - // UNISOLATEOBJECTS — bring back everything hidden by Isolate / Hide - "UNISOLATEOBJECTS" => { - if self.tabs[i].scene.is_isolation_active() { - self.tabs[i].scene.end_isolation(); - self.command_line - .push_info("Isolation ended — all objects shown."); - } else { - self.command_line.push_info("No hidden objects."); - } - } - - // LAYUNISO — restore all layers that were turned off by LAYISO (turn all on) - "LAYUNISO" => { - self.push_undo_snapshot(i, "LAYUNISO"); - let names: Vec = self.tabs[i] - .scene - .document - .layers - .iter() - .map(|l| l.name.clone()) - .collect(); - for name in names { - if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(&name) { - dl.turn_on(); - } - } - self.tabs[i].scene.bump_geometry(); - self.tabs[i].dirty = true; - self.refresh_layer_panel(); - self.command_line - .push_info("LAYUNISO: all layers restored."); - } - - "LAYMATCH" | "LAYMCH" => { - use crate::modules::draw::layers::match_layer::LayMatchCommand; - let dest: Vec<_> = self.tabs[i] - .scene - .selected_entities() - .into_iter() - .map(|(h, _)| h) - .collect(); - let cmd = LayMatchCommand::new(dest); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } - - "MATCHPROP" | "MA" => { - use crate::modules::draw::properties::match_prop::MatchPropCommand; - self.tabs[i].scene.deselect_all(); - let cmd = MatchPropCommand::new(); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } - - "GROUP" | "G" => { - let handles: Vec<_> = self.tabs[i] - .scene - .selected_entities() - .into_iter() - .map(|(h, _)| h) - .collect(); - if handles.is_empty() { - use crate::modules::draw::select::SelectObjectsCommand; - let cmd = SelectObjectsCommand::new("GROUP"); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } else { - let auto_name = super::helpers::next_group_auto_name(&self.tabs[i].scene); - use crate::modules::draw::groups::group::GroupCommand; - let cmd = GroupCommand::new(handles, auto_name); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } - } - - "UNGROUP" | "UG" => { - let handles: Vec<_> = self.tabs[i] - .scene - .selected_entities() - .into_iter() - .map(|(h, _)| h) - .collect(); - if handles.is_empty() { - use crate::modules::draw::groups::ungroup::UngroupCommand; - let cmd = UngroupCommand::new(); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } else { - self.push_undo_snapshot(i, "UNGROUP"); - let count = self.tabs[i].scene.delete_groups_containing(&handles); - self.tabs[i].dirty = true; - if count > 0 { - self.command_line - .push_info(&format!("{} group(s) dissolved.", count)); - } else { - self.command_line - .push_info("No groups found for selected objects."); - } - } - } - - "COPYCLIP" | "CC" => { - let handles: Vec<_> = self.tabs[i] - .scene - .selected_entities() - .into_iter() - .map(|(h, _)| h) - .collect(); - if handles.is_empty() { - use crate::modules::draw::select::SelectObjectsCommand; - let cmd = SelectObjectsCommand::new("COPYCLIP"); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } else { - let entities: Vec<_> = handles - .iter() - .filter_map(|&h| self.tabs[i].scene.document.get_entity(h).cloned()) - .collect(); - self.clipboard_centroid = super::helpers::entities_centroid( - &self.tabs[i].scene.wire_models_for(&handles), - ); - self.clipboard = entities; - self.clipboard_deps = super::ClipboardDeps::capture( - &self.tabs[i].scene.document, - &self.clipboard, - ); - self.command_line.push_info(&format!( - "{} object(s) copied to clipboard.", - self.clipboard.len() - )); - } - } - - "CUTCLIP" | "CX" => { - let handles: Vec<_> = self.tabs[i] - .scene - .selected_entities() - .into_iter() - .map(|(h, _)| h) - .collect(); - if handles.is_empty() { - use crate::modules::draw::select::SelectObjectsCommand; - let cmd = SelectObjectsCommand::new("CUTCLIP"); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } else { - let entities: Vec<_> = handles - .iter() - .filter_map(|&h| self.tabs[i].scene.document.get_entity(h).cloned()) - .collect(); - self.clipboard_centroid = super::helpers::entities_centroid( - &self.tabs[i].scene.wire_models_for(&handles), - ); - let count = entities.len(); - self.clipboard = entities; - self.clipboard_deps = super::ClipboardDeps::capture( - &self.tabs[i].scene.document, - &self.clipboard, - ); - self.push_undo_snapshot(i, "CUTCLIP"); - self.tabs[i].scene.erase_entities(&handles); - self.tabs[i].scene.deselect_all(); - self.tabs[i].dirty = true; - self.refresh_properties(); - self.command_line - .push_info(&format!("{} object(s) cut to clipboard.", count)); - } - } - - "PASTECLIP" | "PC" => { - if self.clipboard.is_empty() { - self.command_line.push_error("Clipboard is empty."); - } else { - let wires = self.tabs[i].scene.wires_for_entities(&self.clipboard); - let centroid = self.clipboard_centroid; - use crate::modules::draw::clipboard::paste::PasteCommand; - // The ghost anchor is a display-only offset; the precise - // paste delta is computed in f64 at commit time. - let cmd = PasteCommand::new(wires, centroid.as_vec3()); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } - } - - // PASTEORIG — paste at the entities' original coordinates (no pick). - "PASTEORIG" => { - if self.clipboard.is_empty() { - self.command_line - .push_error("PASTEORIG: clipboard is empty."); - } else { - let count = self.clipboard.len(); - self.push_undo_snapshot(i, "PASTEORIG"); - // No transform: entities keep their original coordinates. - let _ = self.finalize_paste(i, None); - self.tabs[i].dirty = true; - self.refresh_layer_panel(); - self.refresh_properties(); - self.command_line.push_output(&format!( - "PASTEORIG: {} object(s) pasted at original coordinates.", - count - )); - } - } - - // PASTEBLOCK — wrap the clipboard contents in a new block definition - // and place one insert of it at the clipboard's original location. - "PASTEBLOCK" => { - if self.clipboard.is_empty() { - self.command_line - .push_error("PASTEBLOCK: clipboard is empty."); - } else { - self.push_undo_snapshot(i, "PASTEBLOCK"); - self.merge_clipboard_deps(i); - // Recreate any block definition the clipboard's INSERTs - // reference, so nested blocks inside the new wrapper block - // don't render empty. (#135 / #158) - self.merge_clipboard_blocks(i); - // Recreate each entity's xdictionary graph (XCLIP filters) - // and stamp the new root onto the wrapped entity, so the - // block's nested insert keeps its clip. (#xclip-paste) - let ext_roots = self.recreate_clipboard_ext_roots(i); - let name = self.unique_block_name("Block"); - let base = self.clipboard_centroid; - let mut entities = self.clipboard.clone(); - for (idx, root) in ext_roots { - if let Some(e) = entities.get_mut(idx) { - e.common_mut().xdictionary_handle = Some(root); - } - } - match self - .tabs[i] - .scene - .define_block_from_owned_entities(entities, &name, base) - { - Ok(()) => { - // Block defined; now place it interactively so the - // user picks the drop point (insertion uses the - // clipboard centroid as the block's base). The - // clipboard wires rubber-band under the cursor. - self.tabs[i].scene.populate_meshes_from_document(); - self.tabs[i].dirty = true; - let wires = self.tabs[i].scene.wires_for_entities(&self.clipboard); - use crate::modules::insert::insert_block::InsertBlockCommand; - let cmd = InsertBlockCommand::new_for_block(name, wires, base.as_vec3()); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } - Err(e) => self.command_line.push_error(&format!("PASTEBLOCK: {e}")), - } - } - } - - "BLOCK" => { - let handles: Vec<_> = self.tabs[i] - .scene - .selected_entities() - .into_iter() - .map(|(h, _)| h) - .collect(); - if handles.is_empty() { - use crate::modules::draw::select::SelectObjectsCommand; - let cmd = SelectObjectsCommand::new("BLOCK"); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } else { - use crate::modules::insert::create_block::CreateBlockCommand; - let cmd = CreateBlockCommand::new(handles); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } - } - - "INSERT" => { - let blocks = self.tabs[i].scene.custom_block_names(); - if blocks.is_empty() { - self.command_line - .push_error("No user-defined blocks found in this drawing."); - } else { - use crate::modules::insert::insert_block::InsertBlockCommand; - let cmd = InsertBlockCommand::new(blocks); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } - } - - "XATTACH" | "XA" => { - // Launch the file picker; XAttachPickResult will start the command. - return Task::done(Message::XAttachPick); - } - - cmd if cmd == "WBLOCK" || cmd == "WB" || cmd.starts_with("WBLOCK ") => { - let arg = cmd.splitn(2, ' ').nth(1).unwrap_or("").trim(); - if arg.is_empty() { - // No argument: use selected entities (*) if any, else ask. - let sel: Vec<_> = self.tabs[i].scene.selected.iter().copied().collect(); - if sel.is_empty() { - self.command_line.push_error( - "WBLOCK Select entities first, or: WBLOCK or WBLOCK *", - ); - } else { - return Task::done(Message::WblockSave("*".to_string())); - } - } else { - return Task::done(Message::WblockSave(arg.to_string())); - } - } - - "XREF" | "XR" => { - // List all xref blocks in the current drawing. - let xrefs: Vec = self.tabs[i] - .scene - .document - .block_records - .iter() - .filter(|br| br.flags.is_xref || br.flags.is_xref_overlay) - .map(|br| { - format!( - " {} — {}", - br.name, - if br.xref_path.is_empty() { - "(no path)".to_string() - } else { - br.xref_path.clone() - } - ) - }) - .collect(); - if xrefs.is_empty() { - self.command_line - .push_output("XREF No external references in this drawing."); - } else { - self.command_line.push_output("XREF External references:"); - for line in xrefs { - self.command_line.push_output(&line); - } - } - } - - "XRELOAD" => { - // Reload all xrefs for the current drawing. - if let Some(path) = &self.tabs[i].current_path.clone() { - if let Some(base_dir) = path.parent() { - let (infos, _dropped) = crate::io::xref::resolve_xrefs( - &mut self.tabs[i].scene.document, - base_dir, - ); - for info in &infos { - match info.status { - crate::io::xref::XrefStatus::Loaded => { - self.command_line - .push_output(&format!("XREF Reloaded \"{}\"", info.name)); - } - crate::io::xref::XrefStatus::NotFound => { - self.command_line.push_error(&format!( - "XREF Not found: \"{}\" ({})", - info.name, info.path - )); - } - crate::io::xref::XrefStatus::Unloaded => { - self.command_line.push_info(&format!( - "XREF Unloaded (skipped): \"{}\"", - info.name - )); - } - } - } - self.tabs[i].scene.populate_hatches_from_document(); - self.tabs[i].scene.populate_images_from_document(); - self.tabs[i].scene.populate_meshes_from_document(); - } - } else { - self.command_line - .push_error("XREF Save the drawing first to resolve relative XREF paths."); - } - } - - // ── Draw commands ────────────────────────────────────────────── - "LINE" | "L" => { - use crate::modules::draw::draw::line::LineCommand; - let new_cmd = LineCommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - - "MLINE" | "ML" => { - use crate::modules::draw::draw::mline::MlineCommand; - let style = self.tabs[i].scene.document.header.multiline_style.clone(); - let cmd_obj = MlineCommand::with_style(style); - self.command_line.push_info(&cmd_obj.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd_obj)); - } - - cmd if cmd == "WIPEOUT" || cmd == "WO" || cmd.starts_with("WIPEOUT ") => { - use crate::modules::draw::draw::wipeout::WipeoutCommand; - let args = cmd - .split_once(' ') - .map(|(_, r)| r.trim().to_uppercase()) - .unwrap_or_default(); - let wo_cmd = if args == "P" || args == "POLYGONAL" { - WipeoutCommand::new_polygonal() - } else { - WipeoutCommand::new_rectangular() - }; - self.command_line.push_info(&wo_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(wo_cmd)); - } - - cmd if cmd == "IMAGE" || cmd == "IMAGEATTACH" || cmd == "IM" => { - return Task::done(Message::ImagePick); - } - - "REVCLOUD" => { - use crate::modules::draw::draw::revcloud::RevCloudCommand; - let cmd = RevCloudCommand::new(); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } - - "ATTDEF" => { - use crate::modules::draw::draw::attdef::AttdefCommand; - let cmd = AttdefCommand::new(); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } - - // ATTEDIT — list or edit attribute values on selected Insert entities. - // Usage: - // ATTEDIT — list all attributes on selected Insert(s) - // ATTEDIT — set the value of attribute to - cmd if cmd == "ATTEDIT" || cmd.starts_with("ATTEDIT ") => { - let rest = cmd.trim_start_matches("ATTEDIT").trim(); - let parts: Vec<&str> = rest.splitn(2, char::is_whitespace).collect(); - let selected_handles: Vec = self.tabs[i] - .scene - .selected_entities() - .iter() - .map(|(h, _)| *h) - .collect(); - if selected_handles.is_empty() { - self.command_line - .push_error("ATTEDIT: select an Insert entity first."); - } else { - let mut found_any = false; - for sh in &selected_handles { - if let Some(acadrust::EntityType::Insert(ins)) = self.tabs[i] - .scene - .document - .entities() - .find(|e| e.common().handle == *sh) - { - found_any = true; - if rest.is_empty() { - // List attributes. - if ins.attributes.is_empty() { - self.command_line.push_output(&format!( - " Insert {:x}: no attributes.", - sh.value() - )); - } else { - for attr in &ins.attributes { - self.command_line.push_output(&format!( - " [{tag}] = {val}", - tag = attr.tag, - val = attr.get_value() - )); - } - } - } - } - } - if !found_any { - self.command_line - .push_error("ATTEDIT: no Insert entities in selection."); - } - // If tag + value supplied, mutate attributes. - if parts.len() == 2 && !parts[0].is_empty() { - let tag_up = parts[0].to_uppercase(); - let new_val = parts[1]; - let mut changed = 0usize; - self.push_undo_snapshot(i, "ATTEDIT"); - for sh in &selected_handles { - if let Some(acadrust::EntityType::Insert(ins)) = self.tabs[i] - .scene - .document - .entities_mut() - .find(|e| e.common().handle == *sh) - { - for attr in &mut ins.attributes { - if attr.tag.to_uppercase() == tag_up { - attr.set_value(new_val); - changed += 1; - } - } - } - } - if changed > 0 { - self.tabs[i].dirty = true; - self.command_line.push_output(&format!( - "ATTEDIT: updated {changed} attribute(s) [{tag_up}] = {new_val}." - )); - } else { - self.command_line.push_error(&format!( - "ATTEDIT: tag '{tag_up}' not found in selection." - )); - } - } - } - } - - // ATTDISP — control attribute display visibility. - // ATTDISP ON — make all AttributeDefinitions visible - // ATTDISP OFF — make all AttributeDefinitions invisible - // ATTDISP NORMAL — restore: show only those without the invisible flag - cmd if cmd == "ATTDISP" || cmd.starts_with("ATTDISP ") => { - let sub = cmd.split_whitespace().nth(1).unwrap_or("").to_uppercase(); - match sub.as_str() { - "ON" | "OFF" | "NORMAL" => { - 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 { - match sub.as_str() { - "ON" => { - ad.flags.invisible = false; - count += 1; - } - "OFF" => { - ad.flags.invisible = true; - count += 1; - } - "NORMAL" => { /* leave existing flags — they are already the "normal" state */ - } - _ => {} - } - } - } - self.tabs[i].dirty = true; - self.command_line.push_output(&format!( - "ATTDISP {sub}: {count} attribute definition(s) updated." - )); - } - _ => { - self.command_line - .push_info("Usage: ATTDISP ON | OFF | NORMAL"); - } - } - } - - "DONUT" | "DO" => { - use crate::modules::draw::draw::donut::DonutCommand; - let cmd = DonutCommand::new(); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } - - "CIRCLE" | "C" => { - use crate::modules::draw::draw::circle::CircleCommand; - let new_cmd = CircleCommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - "CIRCLE_CD" => { - use crate::modules::draw::draw::circle::CircleCDCommand; - let new_cmd = CircleCDCommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - "CIRCLE_2P" => { - use crate::modules::draw::draw::circle::Circle2PCommand; - let new_cmd = Circle2PCommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - "CIRCLE_3P" => { - use crate::modules::draw::draw::circle::Circle3PCommand; - let new_cmd = Circle3PCommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - "CIRCLE_TTR" => { - use crate::modules::draw::draw::circle::CircleTTRCommand; - let new_cmd = CircleTTRCommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.pre_cmd_tangent = Some(self.snapper.is_on(crate::snap::SnapType::Tangent)); - self.snapper.enabled.insert(crate::snap::SnapType::Tangent); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - "CIRCLE_TTT" => { - use crate::modules::draw::draw::circle::CircleTTTCommand; - let new_cmd = CircleTTTCommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.pre_cmd_tangent = Some(self.snapper.is_on(crate::snap::SnapType::Tangent)); - self.snapper.enabled.insert(crate::snap::SnapType::Tangent); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - - "ARC" | "A" => { - use crate::modules::draw::draw::arc::ArcCommand; - let new_cmd = ArcCommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - "ARC_3P" => { - use crate::modules::draw::draw::arc::Arc3PCommand; - let new_cmd = Arc3PCommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - "ARC_SCE" => { - use crate::modules::draw::draw::arc::ArcSCECommand; - let new_cmd = ArcSCECommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - "ARC_SCA" => { - use crate::modules::draw::draw::arc::ArcSCACommand; - let new_cmd = ArcSCACommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - "ARC_SCL" => { - use crate::modules::draw::draw::arc::ArcSCLCommand; - let new_cmd = ArcSCLCommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - "ARC_SEA" => { - use crate::modules::draw::draw::arc::ArcSEACommand; - let new_cmd = ArcSEACommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - "ARC_SER" => { - use crate::modules::draw::draw::arc::ArcSERCommand; - let new_cmd = ArcSERCommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - "ARC_SED" => { - use crate::modules::draw::draw::arc::ArcSEDCommand; - let new_cmd = ArcSEDCommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - "ARC_CSA" => { - use crate::modules::draw::draw::arc::ArcCSACommand; - let new_cmd = ArcCSACommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - "ARC_CSL" => { - use crate::modules::draw::draw::arc::ArcCSLCommand; - let new_cmd = ArcCSLCommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - - "RECT" | "RECTANG" | "REC" => { - use crate::modules::draw::draw::shapes::RectCommand; - let new_cmd = RectCommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - if self.ortho_mode { - self.rect_suppressed_ortho = true; - self.ortho_mode = false; - } - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - "RECT_ROT" => { - use crate::modules::draw::draw::shapes::RectRotCommand; - let new_cmd = RectRotCommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - if self.ortho_mode { - self.rect_suppressed_ortho = true; - self.ortho_mode = false; - } - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - "RECT_CEN" => { - use crate::modules::draw::draw::shapes::RectCenCommand; - let new_cmd = RectCenCommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - if self.ortho_mode { - self.rect_suppressed_ortho = true; - self.ortho_mode = false; - } - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - "POLY" | "POLYGON" | "POL" => { - use crate::modules::draw::draw::shapes::PolyCommand; - let new_cmd = PolyCommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - "POLY_C" => { - use crate::modules::draw::draw::shapes::PolyCCommand; - let new_cmd = PolyCCommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - "POLY_E" => { - use crate::modules::draw::draw::shapes::PolyECommand; - let new_cmd = PolyECommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - - "PLINE" | "PL" => { - use crate::modules::draw::draw::polyline::PlineCommand; - let new_cmd = PlineCommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - - // ── Modify commands ──────────────────────────────────────────── - "MOVE" | "M" => { - let handles: Vec<_> = self.tabs[i] - .scene - .selected_entities() - .into_iter() - .map(|(h, _)| h) - .collect(); - if handles.is_empty() { - use crate::modules::draw::select::SelectObjectsCommand; - let cmd = SelectObjectsCommand::new("MOVE"); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } else { - use crate::modules::draw::modify::translate::MoveCommand; - let wires = self.tabs[i].scene.wire_models_for(&handles); - let new_cmd = MoveCommand::new(handles, wires); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - } - - "COPY" | "CO" => { - let handles: Vec<_> = self.tabs[i] - .scene - .selected_entities() - .into_iter() - .map(|(h, _)| h) - .collect(); - if handles.is_empty() { - use crate::modules::draw::select::SelectObjectsCommand; - let cmd = SelectObjectsCommand::new("COPY"); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } else { - use crate::modules::draw::modify::copy::CopyCommand; - let wires = self.tabs[i].scene.wire_models_for(&handles); - let new_cmd = CopyCommand::new(handles, wires); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - } - - "ROTATE" | "RO" => { - let handles: Vec<_> = self.tabs[i] - .scene - .selected_entities() - .into_iter() - .map(|(h, _)| h) - .collect(); - if handles.is_empty() { - use crate::modules::draw::select::SelectObjectsCommand; - let cmd = SelectObjectsCommand::new("ROTATE"); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } else { - use crate::modules::draw::modify::rotate::RotateCommand; - let wires = self.tabs[i].scene.wire_models_for(&handles); - let new_cmd = RotateCommand::new(handles, wires); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - } - - "TORIENT" => { - let handles: Vec<_> = self.tabs[i] - .scene - .selected_entities() - .into_iter() - .map(|(h, _)| h) - .collect(); - if handles.is_empty() { - use crate::modules::draw::select::SelectObjectsCommand; - let cmd = SelectObjectsCommand::new("TORIENT"); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } else { - use crate::modules::draw::modify::torient::TorientCommand; - let entities: Vec<_> = handles - .iter() - .filter_map(|&h| self.tabs[i].scene.document.get_entity(h).cloned().map(|e| (h, e))) - .collect(); - let cam_rot = self.tabs[i].scene.camera.borrow().rotation; - let right = cam_rot * glam::Vec3::X; - let view_twist = right.y.atan2(right.x) as f64; - let new_cmd = TorientCommand::new(entities, view_twist); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - } - - "POINT" | "PO" => { - use crate::modules::draw::draw::point::PointCommand; - let new_cmd = PointCommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - - "RAY" => { - use crate::modules::draw::draw::ray::RayCommand; - let new_cmd = RayCommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - - "XLINE" | "XL" | "CONSTRUCTIONLINE" => { - use crate::modules::draw::draw::ray::XLineCommand; - let new_cmd = XLineCommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - - "HATCH" | "H" => { - use crate::modules::draw::draw::hatch::HatchCommand; - let outlines = self.tabs[i].scene.closed_outlines(); - let new_cmd = HatchCommand::new(outlines); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - - "HATCHEDIT" | "HE" => { - use crate::modules::draw::draw::hatchedit::HatcheditCommand; - // If a single hatch is already selected, skip the pick step. - let sel = self.tabs[i].scene.selected_entities(); - if sel.len() == 1 { - let (h, _) = sel[0]; - if let Some(model) = self.tabs[i].scene.hatches.get(&h).cloned() { - let cmd = HatcheditCommand::with_handle( - h, - model.name.clone(), - model.scale, - model.angle_offset, - ); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } else { - self.command_line - .push_error("HATCHEDIT: selected entity is not a hatch."); - } - } else { - let cmd = HatcheditCommand::new(); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } - } - - "GRADIENT" => { - use crate::modules::draw::draw::hatch::GradientCommand; - let outlines = self.tabs[i].scene.closed_outlines(); - let new_cmd = GradientCommand::new(outlines); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - - "BOUNDARY" => { - use crate::modules::draw::draw::hatch::BoundaryCommand; - let outlines = self.tabs[i].scene.closed_outlines(); - let new_cmd = BoundaryCommand::new(outlines); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - - "ELLIPSE" | "EL" => { - use crate::modules::draw::draw::ellipse::EllipseCommand; - let new_cmd = EllipseCommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - - "ELLIPSE_AXIS" => { - use crate::modules::draw::draw::ellipse::EllipseAxisCommand; - let new_cmd = EllipseAxisCommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - - "ELLIPSE_ARC" => { - use crate::modules::draw::draw::ellipse::EllipseArcCommand; - let new_cmd = EllipseArcCommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - - "SPLINE" | "SPL" => { - use crate::modules::draw::draw::spline::SplineCommand; - let new_cmd = SplineCommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - - "SCALE" | "SC" => { - let handles: Vec<_> = self.tabs[i] - .scene - .selected_entities() - .into_iter() - .map(|(h, _)| h) - .collect(); - if handles.is_empty() { - use crate::modules::draw::select::SelectObjectsCommand; - let cmd = SelectObjectsCommand::new("SCALE"); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } else { - use crate::modules::draw::modify::scale::ScaleCommand; - let wires = self.tabs[i].scene.wire_models_for(&handles); - let new_cmd = ScaleCommand::new(handles, wires); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - } - - "MIRROR" | "MI" => { - let handles: Vec<_> = self.tabs[i] - .scene - .selected_entities() - .into_iter() - .map(|(h, _)| h) - .collect(); - if handles.is_empty() { - use crate::modules::draw::select::SelectObjectsCommand; - let cmd = SelectObjectsCommand::new("MIRROR"); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } else { - use crate::modules::draw::modify::mirror::MirrorCommand; - let wires = self.tabs[i].scene.wire_models_for(&handles); - let new_cmd = MirrorCommand::new(handles, wires); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - } - - "ERASE" | "E" => { - let handles: Vec<_> = self.tabs[i] - .scene - .selected_entities() - .into_iter() - .map(|(h, _)| h) - .collect(); - if handles.is_empty() { - use crate::modules::draw::select::SelectObjectsCommand; - let cmd = SelectObjectsCommand::new("ERASE"); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } else { - let n = handles.len(); - self.push_undo_snapshot(i, "ERASE"); - self.tabs[i].scene.erase_entities(&handles); - self.tabs[i].dirty = true; - self.refresh_properties(); - self.command_line - .push_output(&format!("{n} object(s) erased.")); - } - } - - // ── Model commands (3D primitives) ───────────────────────────── - "BOX" | "WEDGE" | "CYLINDER" | "CONE" | "SPHERE" | "TORUS" => { - use crate::modules::model::primitive_cmd::PrimitiveCommand; - let new_cmd = PrimitiveCommand::new(cmd); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - - // ── Design commands (solid booleans) ─────────────────────────── - "UNION" | "SUBTRACT" | "INTERSECT" => { - use crate::modules::model::boolean_cmd::BoolOp; - if let Some(op) = BoolOp::from_id(cmd) { - return self.solid_boolean(op); - } - } - - // ── Annotate commands ────────────────────────────────────────── - "TEXT" | "T" | "DT" => { - use crate::modules::annotate::text::TextCommand; - let new_cmd = TextCommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - - "DDEDIT" | "ED" => { - use crate::modules::annotate::ddedit::DdeditCommand; - // A single text entity already selected opens its in-place - // editor directly; otherwise prompt for a pick. - let sel = self.tabs[i].scene.selected_entities(); - let editable = (sel.len() == 1).then(|| sel[0].0).filter(|h| { - self.tabs[i].scene.document.get_entity(*h).is_some_and(|e| { - super::text_inline::read_text_field(e).is_some() - || matches!(e, acadrust::EntityType::Leader(_)) - }) - }); - if let Some(h) = editable { - return self.begin_text_edit(h); - } - if sel.len() == 1 { - self.command_line - .push_error("DDEDIT: selected entity is not text."); - } else { - let cmd = DdeditCommand::new(); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } - } - - "MTEXT" | "MT" => { - use crate::modules::annotate::mtext::MTextCommand; - let new_cmd = MTextCommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - - "TEXTEDIT" | "TEDIT" => { - use crate::modules::annotate::textedit::TexteditCommand; - let mode_str = if self.texteditmode { "Single" } else { "Multiple" }; - self.command_line.push_output(&format!("Current settings: Edit mode = {}", mode_str)); - let new_cmd = TexteditCommand::new(self.texteditmode); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - - "TEXTEDITMODE" => { - use crate::modules::annotate::textedit::TexteditmodeCommand; - let cmd = TexteditmodeCommand::new(self.texteditmode); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } - - "DIMALIGNED" | "DAL" => { - use crate::modules::annotate::aligned_dim::AlignedDimensionCommand; - let cmd = AlignedDimensionCommand::new(); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } - - "DIMDIAMETER" | "DDI" => { - use crate::modules::annotate::diameter_dim::DiameterDimensionCommand; - let cmd = DiameterDimensionCommand::new(); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } - - "DIMLINEAR" => { - use crate::modules::annotate::linear_dim::LinearDimensionCommand; - let new_cmd = LinearDimensionCommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - - "DIMRADIUS" => { - use crate::modules::annotate::radius_dim::RadiusDimensionCommand; - let new_cmd = RadiusDimensionCommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - - "DIMANGULAR" => { - use crate::modules::annotate::angular_dim::AngularDimensionCommand; - let new_cmd = AngularDimensionCommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - - "DIMORDINATE" | "DOR" => { - use crate::modules::annotate::ordinate_dim::OrdinateDimCommand; - let new_cmd = OrdinateDimCommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - - "LEADER" | "LE" => { - use crate::modules::annotate::leader_cmd::LeaderCommand; - let new_cmd = LeaderCommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - - "MLEADER" | "MLD" => { - use crate::modules::annotate::mleader_cmd::MLeaderCommand; - let new_cmd = MLeaderCommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - - "TOLERANCE" | "TOL" => { - use crate::modules::annotate::tolerance_cmd::ToleranceCommand; - let cmd = ToleranceCommand::new(); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } - - "TABLE" => { - use crate::modules::annotate::table_cmd::TableCommand; - let cmd = TableCommand::new(); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } - - "DIMCONTINUE" | "DCO" => { - use crate::modules::annotate::dim_continue::DimContinueCommand; - let cmd = if let Some((p1, p2, dp, rot)) = find_last_linear_dim(&self.tabs[i].scene) - { - DimContinueCommand::from_base(p1, p2, dp, rot) - } else { - DimContinueCommand::new() - }; - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } - - "DIMBASELINE" | "DBA" => { - use crate::modules::annotate::dim_baseline::DimBaselineCommand; - let cmd = if let Some((p1, p2, dp, rot)) = find_last_linear_dim(&self.tabs[i].scene) - { - let doc = &self.tabs[i].scene.document; - let dimdli = doc - .dim_styles - .iter() - .find(|s| { - s.name - .eq_ignore_ascii_case(&doc.header.current_dimstyle_name) - }) - .map(|s| s.dimdli as f32) - .unwrap_or(1.5); - DimBaselineCommand::from_base(p1, p2, dp, rot, dimdli) - } else { - DimBaselineCommand::new() - }; - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } - - "QDIM" => { - use crate::modules::annotate::qdim::QdimCommand; - let cmd = QdimCommand::new(); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } - - "DIMEDIT" | "DED" => { - use crate::modules::annotate::dimedit::DimEditCommand; - let cmd = DimEditCommand::new(); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } - - "DIMTEDIT" | "DIMTED" => { - use crate::modules::annotate::dimtedit::DimTeditCommand; - let cmd = DimTeditCommand::new(); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } - - "DIMBREAK" | "DBR" => { - use crate::modules::annotate::dimbreak::DimBreakCommand; - let cmd = DimBreakCommand::new(); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } - - "DIMSPACE" | "DSPACE" => { - use crate::modules::annotate::dimspace::DimSpaceCommand; - let cmd = DimSpaceCommand::new(); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } - - "DIMJOGLINE" | "DJL" => { - use crate::modules::annotate::dimjogline::DimJogLineCommand; - let cmd = DimJogLineCommand::new(); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } - - "MLEADERADD" | "MLA" => { - use crate::modules::annotate::mleader_edit::MLeaderAddCommand; - let cmd = MLeaderAddCommand::new(); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } - - "MLEADERREMOVE" | "MLR" => { - use crate::modules::annotate::mleader_edit::MLeaderRemoveCommand; - let cmd = MLeaderRemoveCommand::new(); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } - - "MLEADERALIGN" | "MLAL" => { - use crate::modules::annotate::mleader_edit::MLeaderAlignCommand; - let cmd = MLeaderAlignCommand::new(); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } - - "MLEADERCOLLECT" | "MLC" => { - use crate::modules::annotate::mleader_edit::MLeaderCollectCommand; - let cmd = MLeaderCollectCommand::new(); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } - - "ZOOM EXTENTS" | "ZOOMEXTENTS" | "ZE" => { - self.tabs[i].scene.fit_all(); - self.command_line.push_output("Zoom Extents"); - } - - "ZOOM IN" | "ZI" => { - self.tabs[i].scene.zoom_camera(1.0 / 1.5); - self.command_line.push_output("Zoom In"); - } - - "ZOOM OUT" | "ZO" => { - self.tabs[i].scene.zoom_camera(1.5); - self.command_line.push_output("Zoom Out"); - } - - // ZOOM ALL — fit all entities (same as EXTENTS for now) - "ZOOM ALL" | "ZOOM A" | "ZA" => { - self.tabs[i].scene.fit_all(); - self.command_line.push_output("Zoom All"); - } - - // ZOOM SCALE — set zoom factor (e.g. "ZOOM SCALE 2" or "ZS 0.5") - cmd if cmd.starts_with("ZOOM SCALE ") || cmd.starts_with("ZS ") => { - let rest = cmd - .split_once(' ') - .and_then(|(_, r)| r.split_once(' ').map(|(_, v)| v).or(Some(r))) - .unwrap_or("1"); - if let Ok(factor) = rest.trim().parse::() { - if factor > 0.0 { - self.tabs[i].scene.zoom_camera(1.0 / factor); - self.command_line - .push_output(&format!("Zoom Scale ×{factor:.3}")); - } - } - } - - "PLOTWINDOW" | "PW" => { - use crate::modules::view::plot_window::PlotWindowCommand; - let cmd = PlotWindowCommand::new(); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } - - "ZOOM WINDOW" | "ZOOM W" | "ZW" => { - use crate::modules::view::zoom_window::ZoomWindowCommand; - let new_cmd = ZoomWindowCommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - - "STRETCH" | "SS" => { - let handles: Vec<_> = self.tabs[i] - .scene - .selected_entities() - .into_iter() - .map(|(h, _)| h) - .collect(); - if handles.is_empty() { - use crate::modules::draw::select::SelectObjectsCommand; - let cmd = SelectObjectsCommand::new("STRETCH"); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } else { - use crate::modules::draw::modify::stretch::StretchCommand; - let wires = self.tabs[i].scene.wire_models_for(&handles); - let new_cmd = StretchCommand::new(handles, wires); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - } - - "FILLET" | "F" => { - use crate::modules::draw::modify::fillet::FilletCommand; - let entities: Vec<_> = self.tabs[i] - .scene - .entity_wires() - .iter() - .filter_map(|w| { - let h = Scene::handle_from_wire_name(&w.name)?; - self.tabs[i] - .scene - .document - .get_entity(h) - .cloned() - .map(|e| (h, e)) - }) - .collect(); - let all_entities: Vec<_> = entities.into_iter().map(|(_, e)| e).collect(); - let new_cmd = FilletCommand::new( - crate::modules::draw::defaults::get_fillet_radius(), - all_entities, - ); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - - "ARRAY" | "AR" | "ARRAYRECT" => { - let handles: Vec<_> = self.tabs[i] - .scene - .selected_entities() - .into_iter() - .map(|(h, _)| h) - .collect(); - if handles.is_empty() { - use crate::modules::draw::select::SelectObjectsCommand; - let cmd = SelectObjectsCommand::new("ARRAYRECT"); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } else { - use crate::modules::draw::modify::array::ArrayRectCommand; - let wires = self.tabs[i].scene.wire_models_for(&handles); - let new_cmd = ArrayRectCommand::new(handles, wires); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - } - - "ARRAYPOLAR" => { - let handles: Vec<_> = self.tabs[i] - .scene - .selected_entities() - .into_iter() - .map(|(h, _)| h) - .collect(); - if handles.is_empty() { - use crate::modules::draw::select::SelectObjectsCommand; - let cmd = SelectObjectsCommand::new("ARRAYPOLAR"); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } else { - use crate::modules::draw::modify::array::ArrayPolarCommand; - let wires = self.tabs[i].scene.wire_models_for(&handles); - let new_cmd = ArrayPolarCommand::new(handles, wires); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - } - - "ARRAYPATH" => { - let handles: Vec<_> = self.tabs[i] - .scene - .selected_entities() - .into_iter() - .map(|(h, _)| h) - .collect(); - if handles.is_empty() { - use crate::modules::draw::select::SelectObjectsCommand; - let cmd = SelectObjectsCommand::new("ARRAYPATH"); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } else { - use crate::modules::draw::modify::array::ArrayPathCommand; - let wires = self.tabs[i].scene.wire_models_for(&handles); - let all_entities: Vec<_> = self.tabs[i] - .scene - .entity_wires() - .iter() - .filter_map(|w| { - let h = Scene::handle_from_wire_name(&w.name)?; - self.tabs[i].scene.document.get_entity(h).cloned() - }) - .collect(); - let new_cmd = ArrayPathCommand::new(handles, wires, all_entities); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - } - - "ARRAY3D" | "3DARRAY" => { - let handles: Vec<_> = self.tabs[i] - .scene - .selected_entities() - .into_iter() - .map(|(h, _)| h) - .collect(); - if handles.is_empty() { - use crate::modules::draw::select::SelectObjectsCommand; - let cmd = SelectObjectsCommand::new("ARRAY3D"); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } else { - use crate::modules::draw::modify::array::Array3DCommand; - let new_cmd = Array3DCommand::new(handles); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - } - - "CHAMFER" | "CHA" => { - use crate::modules::draw::modify::fillet::ChamferCommand; - let entities: Vec<_> = self.tabs[i] - .scene - .entity_wires() - .iter() - .filter_map(|w| { - let h = Scene::handle_from_wire_name(&w.name)?; - self.tabs[i] - .scene - .document - .get_entity(h) - .cloned() - .map(|e| (h, e)) - }) - .collect(); - let all_entities: Vec<_> = entities.into_iter().map(|(_, e)| e).collect(); - let new_cmd = ChamferCommand::new( - crate::modules::draw::defaults::get_chamfer_dist1(), - all_entities, - ); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - - "EXPLODE" | "X" => { - use crate::modules::draw::modify::explode::explode_entity; - let entities: Vec<_> = self.tabs[i].scene.selected_entities().into_iter().collect(); - if entities.is_empty() { - use crate::modules::draw::select::SelectObjectsCommand; - let cmd = SelectObjectsCommand::new("EXPLODE"); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } else { - let replacements: Vec<(acadrust::Handle, Vec)> = entities - .iter() - .filter_map(|(h, e)| { - let pieces = explode_entity(e, &self.tabs[i].scene.document); - if pieces.is_empty() { - None - } else { - Some((*h, pieces)) - } - }) - .collect(); - let exploded = replacements.len(); - if exploded > 0 { - self.push_undo_snapshot(i, "EXPLODE"); - } - for (handle, pieces) in replacements { - self.tabs[i].scene.erase_entities(&[handle]); - for piece in pieces { - self.tabs[i].scene.add_entity(piece); - } - } - if exploded > 0 { - self.tabs[i].dirty = true; - self.refresh_properties(); - self.command_line - .push_output(&format!("{exploded} object(s) exploded.")); - } else { - self.command_line - .push_info("EXPLODE: no explodable objects selected."); - } - } - } - - "OFFSET" | "O" => { - use crate::modules::draw::modify::offset::OffsetCommand; - let all_entities: Vec<_> = self.tabs[i] - .scene - .entity_wires() - .iter() - .filter_map(|w| { - let h = Scene::handle_from_wire_name(&w.name)?; - self.tabs[i].scene.document.get_entity(h).cloned() - }) - .collect(); - let new_cmd = OffsetCommand::new(all_entities); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - - "TRIM" | "TR" => { - use crate::modules::draw::modify::trim::TrimCommand; - let entities: Vec<_> = self.tabs[i] - .scene - .entity_wires() - .iter() - .filter_map(|w| { - let h = Scene::handle_from_wire_name(&w.name)?; - self.tabs[i] - .scene - .document - .get_entity(h) - .cloned() - .map(|e| (h, e)) - }) - .collect(); - let all_entities: Vec<_> = entities.into_iter().map(|(_, e)| e).collect(); - let new_cmd = TrimCommand::new(all_entities); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - - "EXTEND" | "EX" => { - use crate::modules::draw::modify::trim::ExtendCommand; - let entities: Vec<_> = self.tabs[i] - .scene - .entity_wires() - .iter() - .filter_map(|w| { - let h = Scene::handle_from_wire_name(&w.name)?; - self.tabs[i] - .scene - .document - .get_entity(h) - .cloned() - .map(|e| (h, e)) - }) - .collect(); - let all_entities: Vec<_> = entities.into_iter().map(|(_, e)| e).collect(); - let new_cmd = ExtendCommand::new(all_entities); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - - "3DORBIT" | "3O" => { - self.command_line - .push_info("3D Orbit: drag with right mouse button."); - } - - // ── Selection utilities ─────────────────────────────────────── - "SELECTALL" | "SA" => { - use crate::scene::Scene; - let handles: Vec = self.tabs[i] - .scene - .entity_wires() - .iter() - .filter_map(|w| Scene::handle_from_wire_name(&w.name)) - .collect(); - let count = handles.len(); - for h in handles { - self.tabs[i].scene.select_entity(h, false); - } - self.command_line - .push_output(&format!("SELECTALL: {} object(s) selected.", count)); - self.refresh_properties(); - } - - "DESELECT" | "DE" | "DESELALL" => { - self.tabs[i].scene.deselect_all(); - self.command_line.push_output("Deselected."); - self.refresh_properties(); - } - - "SELECTSIMILAR" | "SELSIM" => { - let added = self.tabs[i].scene.select_similar(); - self.command_line - .push_output(&format!("Select Similar: {} added.", added)); - self.refresh_properties(); - } - - "QSELECT" | "QS" => { - return Task::done(Message::QSelectOpen); - } - - // ── LIST — entity info ──────────────────────────────────────── - "LIST" | "LI" => { - let selected: Vec<_> = self.tabs[i].scene.selected_entities(); - if selected.is_empty() { - self.command_line - .push_error("LIST: no entities selected. Select entities first."); - } else { - for (handle, _) in &selected { - if let Some(entity) = self.tabs[i].scene.document.get_entity(*handle) { - let type_name = crate::entities::names::dxf_name(entity); - let common = entity.common(); - let color_str = common - .color - .index() - .map(|c| c.to_string()) - .unwrap_or_else(|| "ByLayer".to_string()); - let linetype = - if common.linetype.is_empty() || common.linetype == "ByLayer" { - "ByLayer".to_string() - } else { - common.linetype.clone() - }; - // Entity-specific details - let details = entity_list_details(entity); - self.command_line.push_output(&format!( - "{type_name} Handle:{:X} Layer:{} Color:{} LT:{}{}", - handle.value(), - common.layer, - color_str, - linetype, - if details.is_empty() { - String::new() - } else { - format!("\n {details}") - } - )); - } - } - } - } - - // ── Break / Join ───────────────────────────────────────────────── - "JOIN" | "J" => { - use crate::modules::draw::modify::join::JoinCommand; - let cmd = JoinCommand::new(); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } - - "BREAK" | "BR" => { - use crate::modules::draw::modify::break_cmd::BreakInteractiveCommand; - let cmd = BreakInteractiveCommand::new(); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } - - "BREAKATPOINT" | "BAP" => { - use crate::modules::draw::modify::break_cmd::BreakAtPointCommand; - let cmd = BreakAtPointCommand::new(); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } - - "PEDIT" | "PE" => { - use crate::modules::draw::modify::pedit::PeditCommand; - let cmd_obj = PeditCommand::new(); - self.command_line.push_info(&cmd_obj.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd_obj)); - } - - "SPLINEDIT" | "SPE" => { - use crate::modules::draw::modify::splinedit::SplineditCommand; - let cmd_obj = SplineditCommand::new(); - self.command_line.push_info(&cmd_obj.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd_obj)); - } - - "ATTEDIT" | "ATE" | "-ATTEDIT" => { - use crate::modules::draw::modify::attedit::AtteditCommand; - let cmd_obj = AtteditCommand::new(); - self.command_line.push_info(&cmd_obj.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd_obj)); - } - - // ── REFEDIT — in-place block editing ───────────────────────────── - "REFEDIT" => { - use crate::modules::draw::modify::refedit::RefEditPickCommand; - // If a session is already active, tell the user. - if self.tabs[i].refedit_session.is_some() { - self.command_line - .push_error("REFEDIT: a session is already active. Use REFCLOSE first."); - } else { - // Check if a single INSERT is already selected. - let selected: Vec<_> = - self.tabs[i].scene.selected_entities().into_iter().collect(); - if selected.len() == 1 { - if let Some(acadrust::EntityType::Insert(_)) = - selected.first().map(|(_, e)| e) - { - let handle = selected[0].0; - // Skip pick phase — jump straight to begin. - let _ = - self.dispatch_command(&format!("REFEDIT_BEGIN:{}", handle.value())); - return Task::none(); - } - } - let cmd_obj = RefEditPickCommand::new(); - self.command_line.push_info(&cmd_obj.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd_obj)); - } - } - - cmd if cmd.starts_with("REFEDIT_BEGIN:") => { - use crate::modules::draw::modify::refedit::{ - apply_insert_transform, RefEditSession, - }; - use acadrust::Handle; - - let handle_u64: u64 = cmd["REFEDIT_BEGIN:".len()..].parse().unwrap_or(0); - let insert_handle = Handle::new(handle_u64); - - // Get INSERT entity. - let insert = match self.tabs[i].scene.document.get_entity(insert_handle) { - Some(acadrust::EntityType::Insert(ins)) => ins.clone(), - _ => { - self.command_line - .push_error("REFEDIT: selected object is not an INSERT."); - return Task::none(); - } - }; - - // Build the INSERT's full placement transform (OCS + rotation + - // scale, including non-uniform / mirrored) and its inverse, so - // edits round-trip back to block-local coordinates on SAVE. - let sx = insert.x_scale(); - let sy = insert.y_scale(); - let sz = insert.z_scale(); - let forward = insert.get_transform(); - let inverse = { - use acadrust::types::{Matrix3, Matrix4, Transform}; - let ocs_t = - Matrix4::from_matrix3(Matrix3::arbitrary_axis(insert.normal).transpose()); - let t_inv = Matrix4::translation( - -insert.insert_point.x, - -insert.insert_point.y, - -insert.insert_point.z, - ); - let r_inv = Matrix4::rotation_z(-insert.rotation); - let s_inv = Matrix4::scaling(1.0 / sx, 1.0 / sy, 1.0 / sz); - // inverse(OCS·T·R·S) = S⁻¹·R⁻¹·T⁻¹·OCSᵀ - Transform::from_matrix(s_inv * r_inv * t_inv * ocs_t) - }; - - // Find the block record. - let br_handle = match self.tabs[i] - .scene - .document - .block_records - .get(&insert.block_name) - { - Some(br) => br.handle, - None => { - self.command_line.push_error(&format!( - "REFEDIT: block \"{}\" not found.", - insert.block_name - )); - return Task::none(); - } - }; - - // Collect block-local entities (skip structural Block/BlockEnd/AttDef). - let block_entities: Vec<_> = { - let br = self.tabs[i] - .scene - .document - .block_records - .get(&insert.block_name) - .unwrap(); - br.entity_handles - .iter() - .filter_map(|h| self.tabs[i].scene.document.get_entity(*h).cloned()) - .filter(|e| { - !matches!( - e, - acadrust::EntityType::Block(_) - | acadrust::EntityType::BlockEnd(_) - | acadrust::EntityType::AttributeDefinition(_) - ) - }) - .collect() - }; - - if block_entities.is_empty() { - self.command_line.push_error("REFEDIT: block is empty."); - return Task::none(); - } - - let session = RefEditSession { - block_name: insert.block_name.clone(), - br_handle, - temp_handles: vec![], - forward, - inverse, - }; - - self.push_undo_snapshot(i, "REFEDIT"); - self.tabs[i].refedit_session = Some(session.clone()); - - // Add block entities to model space with INSERT transform applied. - let mut temp_handles = Vec::new(); - for mut entity in block_entities { - apply_insert_transform(&mut entity, &session); - entity.common_mut().handle = acadrust::Handle::NULL; - entity.common_mut().owner_handle = acadrust::Handle::NULL; - let h = self.tabs[i].scene.add_entity(entity); - temp_handles.push(h); - } - self.tabs[i].refedit_session.as_mut().unwrap().temp_handles = temp_handles.clone(); - - // Fade everything except the entities being edited, so the - // surrounding drawing stays visible for context but the block's - // geometry stands out. (#136) - self.tabs[i] - .scene - .set_refedit_keep(Some(temp_handles.iter().copied().collect())); - - // Select the temp entities so user can see what they're editing. - self.tabs[i].scene.deselect_all(); - for h in &temp_handles { - self.tabs[i].scene.select_entity(*h, false); - } - self.tabs[i].dirty = true; - - // No active command — the user edits the block's geometry freely - // (move, grips, draw, erase…) and runs REFCLOSE when done. (#136) - self.tabs[i].active_cmd = None; - self.command_line.push_info(&format!( - "REFEDIT: Editing block \"{}\". Run REFCLOSE to save, REFCLOSE_DISCARD to cancel.", - insert.block_name - )); - } - - "REFCLOSE" => { - if self.tabs[i].refedit_session.is_some() { - use crate::modules::draw::modify::refedit::RefCloseCommand; - let cmd_obj = RefCloseCommand::new(); - self.command_line.push_info(&cmd_obj.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd_obj)); - } else { - self.command_line - .push_error("REFCLOSE: no REFEDIT session active."); - } - } - - "REFCLOSE_SAVE" => { - use crate::modules::draw::modify::explode::normalize_entity_for_block; - use crate::modules::draw::modify::refedit::apply_insert_inverse_transform; - - let session = match self.tabs[i].refedit_session.take() { - Some(s) => s, - None => { - self.command_line - .push_error("REFCLOSE: no REFEDIT session active."); - return Task::none(); - } - }; - - self.push_undo_snapshot(i, "REFCLOSE"); - - // Collect the edited temp entities. - let new_entities: Vec = session - .temp_handles - .iter() - .filter_map(|h| self.tabs[i].scene.document.get_entity(*h).cloned()) - .collect(); - - // Remove temp entities from model space. - self.tabs[i].scene.erase_entities(&session.temp_handles); - - // Apply inverse INSERT transform → block-local coordinates. - let new_entities: Vec<_> = new_entities - .into_iter() - .map(|mut entity| { - apply_insert_inverse_transform(&mut entity, &session); - let mut entity = normalize_entity_for_block(entity); - entity.common_mut().handle = acadrust::Handle::NULL; - entity.common_mut().owner_handle = session.br_handle; - entity - }) - .collect(); - - // Remove old block entities from the document. - let old_handles: Vec<_> = match self.tabs[i] - .scene - .document - .block_records - .get(&session.block_name) - { - Some(br) => br.entity_handles.clone(), - None => vec![], - }; - for h in &old_handles { - self.tabs[i].scene.document.remove_entity(*h); - } - // Flush the entity_handles list from the block record. - if let Some(br) = self.tabs[i] - .scene - .document - .block_records - .get_mut(&session.block_name) - { - br.entity_handles.clear(); - } - - // Add the new block entities. - for entity in new_entities { - let _ = self.tabs[i].scene.document.add_entity(entity); - } - - self.tabs[i].dirty = true; - self.command_line.push_output(&format!( - "REFCLOSE: Block \"{}\" saved. All references updated.", - session.block_name - )); - // End the edit fade before rebuilding, so the restored geometry - // recolours bright. (#136) - self.tabs[i].scene.set_refedit_keep(None); - // Rebuild hatch/image/mesh caches since block content changed. - self.tabs[i].scene.rebuild_derived_caches(); - } - - "REFCLOSE_DISCARD" => { - let session = match self.tabs[i].refedit_session.take() { - Some(s) => s, - None => { - self.command_line - .push_error("REFCLOSE: no REFEDIT session active."); - return Task::none(); - } - }; - // Remove temp entities without modifying the block. - self.tabs[i].scene.erase_entities(&session.temp_handles); - self.tabs[i].scene.deselect_all(); - // End the edit fade — restore the drawing to full brightness. - self.tabs[i].scene.set_refedit_keep(None); - self.command_line - .push_output("REFCLOSE: Changes discarded."); - } - - "ALIGN" | "AL" => { - use crate::modules::draw::modify::align::AlignCommand; - let cmd = AlignCommand::new(); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } - - "LENGTHEN" | "LEN" => { - use crate::modules::draw::modify::lengthen::LengthenCommand; - let cmd = LengthenCommand::new(); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } - - "DIVIDE" | "DIV" => { - use crate::modules::draw::inquiry::divide::DivideCommand; - let cmd = DivideCommand::new(); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } - - "MEASURE" | "ME" => { - use crate::modules::draw::inquiry::divide::MeasureCommand; - let cmd = MeasureCommand::new(); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } - - // ── Inquiry ────────────────────────────────────────────────────── - "DIST" | "DI" => { - use crate::modules::draw::inquiry::dist::DistCommand; - let cmd = DistCommand::new(); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } - - "ID" => { - use crate::modules::draw::inquiry::id::IdCommand; - let cmd = IdCommand::new(); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } - - "AREA" => { - use crate::modules::draw::inquiry::area::AreaCommand; - let cmd = AreaCommand::new(); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } - - // ── MASSPROP — area, perimeter, centroid of selected entities ──── - "MASSPROP" => { - let selected = self.tabs[i].scene.selected_entities(); - if selected.is_empty() { - self.command_line - .push_error("MASSPROP: no entities selected. Select entities first."); - } else { - for (handle, _) in &selected { - if let Some(entity) = self.tabs[i].scene.document.get_entity(*handle) { - use crate::entities::traits::EntityTypeOps; - if let Some(props) = entity.mass_props() { - self.command_line.push_output(&format!( - "{} Area={:.4} Perimeter={:.4} Centroid=({:.4},{:.4})", - crate::entities::names::dxf_name(entity), - props.area, - props.perimeter, - props.cx, - props.cy, - )); - } - } - } - } - } - - // ── FLATTEN — move selected (or all) entities to Z=0 ───────────── - "FLATTEN" => { - let handles: Vec = { - let sel = self.tabs[i].scene.selected_entities(); - if sel.is_empty() { - // Flatten all entities - self.tabs[i] - .scene - .document - .entities() - .map(|e| e.common().handle) - .collect() - } else { - sel.into_iter().map(|(h, _)| h).collect() - } - }; - if handles.is_empty() { - self.command_line.push_error("FLATTEN: no entities."); - } else { - self.push_undo_snapshot(i, "FLATTEN"); - for h in &handles { - if let Some(e) = self.tabs[i].scene.document.get_entity_mut(*h) { - flatten_entity_z(e); - } - } - self.tabs[i].dirty = true; - self.command_line.push_output(&format!( - "FLATTEN: {} entity(ies) moved to Z=0.", - handles.len() - )); - self.refresh_properties(); - } - } - - // ── QSELECT — quick-select entities by property ─────────────────── - // QSELECT TYPE — select all entities of given type - // QSELECT LAYER — select all entities on layer - // QSELECT COLOR — select all entities with color index n - // QSELECT LINETYPE — select all entities with linetype - cmd if cmd == "QSELECT" || cmd.starts_with("QSELECT ") => { - let rest = cmd.split_once(' ').map(|(_, r)| r.trim()).unwrap_or(""); - let parts: Vec<&str> = rest.splitn(2, ' ').collect(); - let prop = parts.first().map(|s| s.to_uppercase()).unwrap_or_default(); - let val = parts.get(1).map(|s| s.trim()).unwrap_or("").to_uppercase(); - - let matched: Vec = self.tabs[i] - .scene - .document - .entities() - .filter(|e| { - let c = e.common(); - match prop.as_str() { - "TYPE" => crate::entities::names::dxf_name(e).to_uppercase() == val, - "LAYER" => c.layer.to_uppercase() == val, - "COLOR" => c - .color - .index() - .map(|n| n.to_string() == val) - .unwrap_or(val == "BYLAYER"), - "LINETYPE" => c.linetype.to_uppercase() == val, - _ => false, - } - }) - .map(|e| e.common().handle) - .collect(); - - if prop.is_empty() { - self.command_line - .push_info("Usage: QSELECT TYPE|LAYER|COLOR|LINETYPE "); - } else if matched.is_empty() { - self.command_line - .push_output("QSELECT: no matching entities."); - } else { - self.tabs[i].scene.deselect_all(); - for h in &matched { - self.tabs[i].scene.select_entity(*h, false); - } - self.command_line - .push_output(&format!("QSELECT: {} entity(ies) selected.", matched.len())); - self.refresh_properties(); - } - } - - // ── COUNT — entity statistics ───────────────────────────────────── - cmd if cmd == "COUNT" || cmd.starts_with("COUNT ") => { - let filter = cmd.split_once(' ').map(|(_, r)| r.trim().to_uppercase()); - let mut counts: std::collections::BTreeMap = Default::default(); - for e in self.tabs[i].scene.document.entities() { - let layer = &e.common().layer; - let type_name = crate::entities::names::dxf_name(e); - let key = match &filter { - Some(f) if f == "LAYER" => layer.clone(), - Some(f) if f == "TYPE" => type_name.to_string(), - Some(f) => { - // Filter by layer name - if layer.to_uppercase() != *f { - continue; - } - type_name.to_string() - } - None => type_name.to_string(), - }; - *counts.entry(key).or_default() += 1; - } - let total: usize = counts.values().sum(); - for (k, n) in &counts { - self.command_line.push_output(&format!(" {k}: {n}")); - } - self.command_line - .push_output(&format!("COUNT: {total} entity(ies) total.")); - } - - "DATAEXTRACTION" | "EATTEXT" | "ATTEXT" => { - let csv = build_data_extraction_csv(&self.tabs[i].scene.document); - return Task::done(Message::DataExtractionSave(csv)); - } - - // ── Find / Replace ──────────────────────────────────────────────── - // FIND — list all Text/MText/Dimension containing - // FIND REPLACE — replace first occurrence (case-insensitive) - // FINDALL REPLACE — replace all occurrences - cmd if cmd == "FIND" - || cmd.starts_with("FIND ") - || cmd == "FINDALL" - || cmd.starts_with("FINDALL ") => - { - let all_mode = cmd.starts_with("FINDALL"); - let rest = cmd.split_once(' ').map(|(_, r)| r.trim()).unwrap_or(""); - - // Split at " REPLACE " keyword (case-insensitive) - let (search, replacement) = if let Some(pos) = rest.to_uppercase().find(" REPLACE ") - { - (&rest[..pos], Some(rest[pos + 9..].trim())) - } else { - (rest, None) - }; - - if search.is_empty() { - self.command_line.push_error("FIND: specify search text."); - } else { - let search_lc = search.to_lowercase(); - let mut count = 0usize; - let handles: Vec = self.tabs[i] - .scene - .document - .entities() - .filter_map(|e| { - use crate::entities::traits::EntityTypeOps; let txt = e.text_content()?; - if txt.to_lowercase().contains(&search_lc) { - Some(e.common().handle) - } else { - None - } - }) - .collect(); - - if let Some(rep) = replacement { - // Replace mode - let targets: Vec<_> = if all_mode { - handles.clone() - } else { - handles.iter().copied().take(1).collect() - }; - if targets.is_empty() { - self.command_line - .push_output(&format!("FIND: \"{}\" not found.", search)); - } else { - self.push_undo_snapshot(i, "FIND/REPLACE"); - for h in &targets { - if let Some(e) = self.tabs[i].scene.document.get_entity_mut(*h) { - crate::entities::traits::EntityTypeOps::replace_text(e, search, rep); - count += 1; - } - } - self.tabs[i].dirty = true; - self.command_line.push_output(&format!( - "FIND/REPLACE: replaced {} occurrence(s) of \"{}\" → \"{}\".", - count, search, rep - )); - self.refresh_properties(); - } - } else { - // List mode - if handles.is_empty() { - self.command_line - .push_output(&format!("FIND: \"{}\" not found.", search)); - } else { - for h in &handles { - if let Some(e) = self.tabs[i].scene.document.get_entity(*h) { - use crate::entities::traits::EntityTypeOps; let txt = e.text_content().unwrap_or_default(); - self.command_line.push_output(&format!( - " Handle {:X}: \"{}\"", - h.value(), - txt - )); - } - } - self.command_line.push_output(&format!( - "FIND: {} match(es) for \"{}\".", - handles.len(), - search - )); - } - } - } - } - - "HELP" | "?" => { - self.command_line.push_output( - "Draw: LINE CIRCLE ARC PLINE RECTANG(RECT) POLYGON(POLY) POINT ELLIPSE SPLINE RAY XLINE HATCH DONUT REVCLOUD WIPEOUT MLINE ATTDEF | \ - Modify: MOVE COPY ROTATE SCALE MIRROR ERASE OFFSET EXTEND FILLET CHAMFER STRETCH EXPLODE TRIM BREAK JOIN LENGTHEN ALIGN PEDIT | \ - Array: ARRAY ARRAYRECT ARRAYPOLAR ARRAYPATH | \ - Text: TEXT MTEXT LEADER MLEADER | \ - Dimension: DIMLINEAR DIMALIGNED DIMANGULAR DIMRADIUS DIMDIAMETER DIMCONTINUE DIMBASELINE | \ - Annotation: TOLERANCE | \ - Inquiry: DIST ID AREA LIST FIND FINDALL COUNT QSELECT | Draw on entity: DIVIDE MEASURE | \ - Attributes: ATTEDIT ATTDISP | \ - Utilities: FLATTEN LAYISO LAYUNISO | \ - View: ZOOM EXTENTS ZOOM WINDOW VIEW LIST/SAVE/RESTORE/DELETE | \ - Layer: LAYER LIST/NEW/ON/OFF/FREEZE/THAW/LOCK/UNLOCK/COLOR/SET | \ - Viewport: MVIEW VPLAYER VPORTS MS PS DRAWORDER | \ - Tables: STYLE DIMSTYLE LINETYPE UCS RENAME PURGE | \ - File: NEW OPEN SAVE SAVEAS PRINT PURGE UNDO REDO" - ); - } - - "DONATE" => { - crate::sys::open_url("https://patreon.com/HakanSeven12"); - self.command_line.push_info("Opening Patreon page..."); - } - - "WEBVERSION" => { - crate::sys::open_url("https://hakanseven12.github.io/OpenCADStudio/"); - self.command_line.push_info("Opening OCS Web..."); - } - - // ── DWGPROPS — print round-trip-only HeaderVariables ───────── - // No UI dialog for these yet; the command surfaces them so - // users can confirm the values that the parser populated and - // the writer will round-trip on save. - "DWGPROPS" | "DWGPROP" => { - let i = self.active_tab; - let h = &self.tabs[i].scene.document.header; - let path_label = self.tabs[i] - .current_path - .as_ref() - .map(|p| p.display().to_string()) - .unwrap_or_else(|| "(unsaved)".to_string()); - self.command_line - .push_output(&format!("Drawing: {}", path_label)); - self.command_line.push_output(&format!( - " Created (Julian): {:.6}", - h.create_date_julian - )); - self.command_line.push_output(&format!( - " Updated (Julian): {:.6}", - h.update_date_julian - )); - self.command_line.push_output(&format!( - " Total edit time: {:.4}", - h.total_editing_time - )); - self.command_line.push_output(&format!( - " User elapsed: {:.4}", - h.user_elapsed_time - )); - self.command_line.push_output(&format!( - " Last saved by: {}", - if h.last_saved_by.is_empty() { - "(unknown)" - } else { - &h.last_saved_by - } - )); - self.command_line.push_output(&format!( - " Fingerprint GUID: {}", - if h.fingerprint_guid.is_empty() { - "(none)" - } else { - &h.fingerprint_guid - } - )); - self.command_line.push_output(&format!( - " Version GUID: {}", - if h.version_guid.is_empty() { - "(none)" - } else { - &h.version_guid - } - )); - self.command_line - .push_output(&format!(" Code page: {}", h.code_page)); - self.command_line.push_output(&format!( - " Menu name: {}", - if h.menu_name.is_empty() { - "(none)" - } else { - &h.menu_name - } - )); - self.command_line.push_output(&format!( - " Hyperlink base: {}", - if h.hyperlink_base.is_empty() { - "(none)" - } else { - &h.hyperlink_base - } - )); - self.command_line.push_output(&format!( - " Project name: {}", - if h.project_name.is_empty() { - "(none)" - } else { - &h.project_name - } - )); - self.command_line.push_output(&format!( - " Stylesheet: {}", - if h.stylesheet.is_empty() { - "(none)" - } else { - &h.stylesheet - } - )); - self.command_line.push_output(&format!( - " Required versions: {:#018x}", - h.required_versions - )); - self.command_line.push_output(&format!( - " Measurement: {} ({})", - h.measurement, - if h.measurement == 1 { "Metric" } else { "Imperial" } - )); - self.command_line.push_output(&format!( - " Proxy graphics: {}", - h.proxy_graphics - )); - self.command_line - .push_output(&format!(" Tree depth: {}", h.tree_depth)); - self.command_line.push_output(&format!( - " User vars (int): {} {} {} {} {}", - h.user_int1, h.user_int2, h.user_int3, h.user_int4, h.user_int5 - )); - self.command_line.push_output(&format!( - " User vars (real): {:.6} {:.6} {:.6} {:.6} {:.6}", - h.user_real1, h.user_real2, h.user_real3, h.user_real4, h.user_real5 - )); - self.command_line.push_output(&format!( - " User timer: {}", - if h.user_timer { "On" } else { "Off" } - )); - } - - // Edit a USERI1..USERI5 / USERR1..USERR5 slot. Lets the user - // store drawing-scoped scalars (and save them through round-trip) - // even though we don't have a LISP / DIESEL runtime yet. - // USERI 1 42 → header.user_int1 = 42 - // USERR 3 1.5e-3 → header.user_real3 = 0.0015 - cmd if cmd.starts_with("USERI") || cmd.starts_with("USERR") => { - let is_real = cmd.starts_with("USERR"); - let rest = if is_real { - cmd.trim_start_matches("USERR").trim() - } else { - cmd.trim_start_matches("USERI").trim() - }; - let parts: Vec<&str> = rest.splitn(2, ' ').collect(); - let slot: Option = parts.first().and_then(|s| s.parse().ok()); - let value = parts.get(1).copied().unwrap_or("").trim(); - let i = self.active_tab; - let h = &mut self.tabs[i].scene.document.header; - match (slot, value, is_real) { - (Some(n @ 1..=5), v, true) => { - if let Ok(val) = v.parse::() { - match n { - 1 => h.user_real1 = val, - 2 => h.user_real2 = val, - 3 => h.user_real3 = val, - 4 => h.user_real4 = val, - _ => h.user_real5 = val, - } - self.tabs[i].dirty = true; - self.command_line - .push_output(&format!("USERR{n} = {val}")); - } else { - self.command_line - .push_info("Usage: USERR <1-5> "); - } - } - (Some(n @ 1..=5), v, false) => { - if let Ok(val) = v.parse::() { - match n { - 1 => h.user_int1 = val, - 2 => h.user_int2 = val, - 3 => h.user_int3 = val, - 4 => h.user_int4 = val, - _ => h.user_int5 = val, - } - self.tabs[i].dirty = true; - self.command_line - .push_output(&format!("USERI{n} = {val}")); - } else { - self.command_line - .push_info("Usage: USERI <1-5> "); - } - } - _ => self - .command_line - .push_info("Usage: USERI <1-5> | USERR <1-5> "), - } - } - - "REPORT" => { - // Pre-fill the GitHub issue body with version + platform so - // reports arrive with the basics already filled in. - let body = format!( - "\n\n\n\ - ---\n- Open CAD Studio: v{}\n- Platform: {}\n", - env!("CARGO_PKG_VERSION"), - crate::sys::platform_info(), - ); - let url = format!( - "https://github.com/HakanSeven12/OpenCADStudio/issues/new?body={}", - crate::sys::percent_encode(&body) - ); - crate::sys::open_url(&url); - self.command_line.push_info("Opening feedback page..."); - } - - "ABOUT" => { - return Task::done(Message::AboutOpen); - } - - "PLUGINS" | "PLUGINMANAGER" => { - return Task::done(Message::PluginManagerOpen); - } - - "CHANGELOG" => { - crate::sys::open_url("https://github.com/HakanSeven12/OpenCADStudio/releases"); - self.command_line.push_info("Opening release notes..."); - } - - // ── Keyboard Shortcuts panel ────────────────────────────────── - cmd if cmd == "SHORTCUTS" || cmd.starts_with("SHORTCUTS ") => { - let raw_rest = cmd.trim_start_matches("SHORTCUTS").trim(); - let parts: Vec<&str> = raw_rest.splitn(3, ' ').collect(); - let sub = parts.first().map(|s| s.to_uppercase()).unwrap_or_default(); - match sub.as_str() { - "" | "LIST" | "?" => { - return Task::done(Message::ShortcutsPanelOpen); - } - "SET" | "S" => { - // SHORTCUTS SET - // e.g. SHORTCUTS SET CTRL+D DIST - let key = parts.get(1).map(|s| s.to_uppercase()).unwrap_or_default(); - let cmd_str = parts.get(2).map(|s| s.to_uppercase()).unwrap_or_default(); - if key.is_empty() || cmd_str.is_empty() { - self.command_line.push_error("Usage: SHORTCUTS SET e.g. SHORTCUTS SET CTRL+D DIST"); - } else { - self.shortcut_overrides.insert(key.clone(), cmd_str.clone()); - self.command_line - .push_output(&format!("Shortcut set: {key} → {cmd_str}")); - } - } - "CLEAR" | "DELETE" | "REMOVE" => { - let key = parts.get(1).map(|s| s.to_uppercase()).unwrap_or_default(); - if key.is_empty() { - self.command_line.push_error("Usage: SHORTCUTS CLEAR "); - } else if self.shortcut_overrides.remove(&key).is_some() { - self.command_line - .push_output(&format!("Shortcut '{key}' removed.")); - } else { - self.command_line - .push_error(&format!("Shortcut '{key}' not found.")); - } - } - _ => { - self.command_line - .push_info("Usage: SHORTCUTS LIST | SET | CLEAR "); - } - } - } - - // ── Color Scheme / Theme selector ───────────────────────────── - cmd if cmd == "COLORSCHEME" || cmd.starts_with("COLORSCHEME ") => { - use iced::Theme; - let sub = cmd - .split_once(' ') - .map(|(_, r)| r.trim()) - .unwrap_or("") - .to_uppercase(); - // Map name to Theme variant. - let theme: Option = match sub.as_str() { - "DARK" => Some(Theme::Dark), - "LIGHT" => Some(Theme::Light), - "DRACULA" => Some(Theme::Dracula), - "NORD" => Some(Theme::Nord), - "SOLARIZED_LIGHT" | "SOLARIZEDLIGHT" => Some(Theme::SolarizedLight), - "SOLARIZED_DARK" | "SOLARIZEDDARK" => Some(Theme::SolarizedDark), - "GRUVBOX_LIGHT" | "GRUVBOXLIGHT" => Some(Theme::GruvboxLight), - "GRUVBOX_DARK" | "GRUVBOXDARK" => Some(Theme::GruvboxDark), - "TOKYONIGHT" | "TOKYO_NIGHT" => Some(Theme::TokyoNight), - "TOKYONIGHTSTORM" | "TOKYO_NIGHT_STORM" => Some(Theme::TokyoNightStorm), - "TOKYONIGHTLIGHT" | "TOKYO_NIGHT_LIGHT" => Some(Theme::TokyoNightLight), - "KANAGAWAWAVE" | "KANAGAWA_WAVE" => Some(Theme::KanagawaWave), - "KANAGAWADRAGON" | "KANAGAWA_DRAGON" => Some(Theme::KanagawaDragon), - "KANAGAWALOTUS" | "KANAGAWA_LOTUS" => Some(Theme::KanagawaLotus), - "MOONFLY" => Some(Theme::Moonfly), - "NIGHTFLY" => Some(Theme::Nightfly), - "OXOCARBON" => Some(Theme::Oxocarbon), - "FERRA" => Some(Theme::Ferra), - "" | "LIST" | "?" => { - self.command_line.push_output( - "Available themes: DARK LIGHT DRACULA NORD SOLARIZED_LIGHT SOLARIZED_DARK \ - GRUVBOX_LIGHT GRUVBOX_DARK TOKYONIGHT TOKYONIGHTSTORM TOKYONIGHTLIGHT \ - KANAGAWAWAVE KANAGAWADRAGON KANAGAWALOTUS MOONFLY NIGHTFLY OXOCARBON FERRA" - ); - return Task::none(); - } - _ => { - self.command_line.push_error(&format!( - "COLORSCHEME: unknown theme '{}'. Type COLORSCHEME LIST for options.", - sub - )); - return Task::none(); - } - }; - if let Some(t) = theme { - let name = format!("{:?}", t); - self.command_line - .push_output(&format!("Color scheme set to '{name}'.")); - return Task::done(Message::SetTheme(t)); - } - return Task::none(); - } - - // ── Layout Manager GUI ───────────────────────────────────────── - "LAYOUTMANAGER" | "LAYOUTPANEL" => { - return Task::done(Message::LayoutManagerOpen); - } - - // ── Layout / viewport ────────────────────────────────────────── - "MVIEW" | "MV" => { - if self.tabs[i].scene.current_layout == "Model" { - self.command_line - .push_error("MVIEW: switch to a paper space layout first."); - } else { - use crate::modules::layout::mview::MviewCommand; - let new_cmd = MviewCommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - } - - // ── MSPACE / PSPACE ─────────────────────────────────────────── - "MS" | "MSPACE" => { - return Task::done(Message::MspaceCommand); - } - "PSPACE" => { - return Task::done(Message::PspaceCommand); - } - - // ── VPORTS — list or create preset viewport configurations ──── - cmd if cmd == "VPORTS" || cmd.starts_with("VPORTS ") => { - let sub = cmd.split_whitespace().nth(1).unwrap_or("").to_uppercase(); - let scene = &self.tabs[i].scene; - if scene.current_layout == "Model" { - // Bare VPORTS → ask for the configuration interactively; - // the next command-line entry supplies it. - if sub.is_empty() { - self.awaiting_vports = true; - self.command_line - .push_info("VPORTS Configuration [SIngle/2H/2V/4]:"); - return self.focus_cmd_input(); - } - // Model space: split the tiled viewport layout. - use iced::Rectangle as R; - let full = R { x: 0.0, y: 0.0, width: 1.0, height: 1.0 }; - let rects: Option> = match sub.as_str() { - "SINGLE" | "SI" | "1" => Some(vec![full]), - "2H" | "2" => Some(vec![ - R { x: 0.0, y: 0.0, width: 1.0, height: 0.5 }, - R { x: 0.0, y: 0.5, width: 1.0, height: 0.5 }, - ]), - "2V" => Some(vec![ - R { x: 0.0, y: 0.0, width: 0.5, height: 1.0 }, - R { x: 0.5, y: 0.0, width: 0.5, height: 1.0 }, - ]), - "4" => Some(vec![ - R { x: 0.0, y: 0.0, width: 0.5, height: 0.5 }, - R { x: 0.5, y: 0.0, width: 0.5, height: 0.5 }, - R { x: 0.0, y: 0.5, width: 0.5, height: 0.5 }, - R { x: 0.5, y: 0.5, width: 0.5, height: 0.5 }, - ]), - _ => None, - }; - match rects { - Some(rects) => { - let n = rects.len(); - self.tabs[i].scene.set_model_tile_layout(rects); - self.tabs[i].scene.camera_generation += 1; - self.command_line - .push_output(&format!("VPORTS: {n} viewport(s).")); - } - None => { - self.command_line - .push_error("VPORTS: use SINGLE | 2H | 2V | 4."); - } - } - } else if sub.is_empty() { - // ── List existing viewports ────────────────────────── - let layout_block = scene.current_layout_block_handle_pub(); - let viewports: Vec<_> = scene - .document - .entities() - .filter_map(|e| { - if let acadrust::EntityType::Viewport(vp) = e { - if vp.id > 1 && vp.common.owner_handle == layout_block { - Some(( - vp.id, - vp.center.clone(), - vp.width, - vp.height, - crate::scene::vp_effective_scale( - vp.custom_scale, - vp.view_height, - vp.height, - ), - vp.status.is_on, - vp.status.locked, - )) - } else { - None - } - } else { - None - } - }) - .collect(); - if viewports.is_empty() { - self.command_line.push_info("No viewports. Use MVIEW to create one, or VPORTS 2H / 2V / 4 / SINGLE."); - } else { - self.command_line.push_output(&format!( - "{} viewport(s) in layout \"{}\":", - viewports.len(), - scene.current_layout - )); - for (id, center, w, h, scale, is_on, locked) in &viewports { - let state = match (is_on, locked) { - (true, true) => "On, Locked", - (true, false) => "On", - (false, _) => "Off", - }; - self.command_line.push_output(&format!( - " VP #{id}: {w:.1}×{h:.1} @ ({:.1},{:.1}) scale={scale:.4} [{state}]", - center.x, center.y - )); - } - } - } else { - // ── Preset viewport layout ─────────────────────────── - // Determine paper dimensions from PlotSettings (fallback A4 landscape). - let layout_name = scene.current_layout.clone(); - let (paper_w, paper_h) = { - use acadrust::objects::ObjectType; - let mut pw = 297.0_f64; - let mut ph = 210.0_f64; - for (_, obj) in &scene.document.objects { - if let ObjectType::PlotSettings(ps) = obj { - if ps.page_name == layout_name && ps.paper_width > 0.0 { - pw = ps.paper_width; - ph = ps.paper_height; - break; - } - } - } - (pw, ph) - }; - let margin = 5.0_f64; // mm margin around the usable area - let uw = paper_w - 2.0 * margin; // usable width - let uh = paper_h - 2.0 * margin; // usable height - // Collect rectangle specs: (cx, cz, w, h) in mm - let rects: Vec<(f64, f64, f64, f64)> = match sub.as_str() { - "2H" => { - // Two viewports side by side (horizontal split) - let vw = (uw - 2.0) / 2.0; - vec![ - (margin + vw / 2.0, margin + uh / 2.0, vw, uh), - (margin + vw + 2.0 + vw / 2.0, margin + uh / 2.0, vw, uh), - ] - } - "2V" => { - // Two viewports stacked (vertical split) - let vh = (uh - 2.0) / 2.0; - vec![ - (margin + uw / 2.0, margin + vh + 2.0 + vh / 2.0, uw, vh), - (margin + uw / 2.0, margin + vh / 2.0, uw, vh), - ] - } - "4" => { - // Four equal viewports (2×2 grid) - let vw = (uw - 2.0) / 2.0; - let vh = (uh - 2.0) / 2.0; - vec![ - (margin + vw / 2.0, margin + vh + 2.0 + vh / 2.0, vw, vh), - ( - margin + vw + 2.0 + vw / 2.0, - margin + vh + 2.0 + vh / 2.0, - vw, - vh, - ), - (margin + vw / 2.0, margin + vh / 2.0, vw, vh), - (margin + vw + 2.0 + vw / 2.0, margin + vh / 2.0, vw, vh), - ] - } - "SINGLE" | "1" => { - // Single full-page viewport - vec![(margin + uw / 2.0, margin + uh / 2.0, uw, uh)] - } - _ => { - self.command_line.push_error( - "VPORTS: unknown option. Use VPORTS 2H | 2V | 4 | SINGLE", - ); - vec![] - } - }; - if !rects.is_empty() { - // Remove existing user viewports in this layout first. - let layout_block = self.tabs[i].scene.current_layout_block_handle_pub(); - let to_erase: Vec = self.tabs[i] - .scene - .document - .entities() - .filter_map(|e| { - if let acadrust::EntityType::Viewport(vp) = e { - if vp.id > 1 && vp.common.owner_handle == layout_block { - Some(vp.common.handle) - } else { - None - } - } else { - None - } - }) - .collect(); - self.push_undo_snapshot(i, "VPORTS"); - self.tabs[i].scene.erase_entities(&to_erase); - // Create new viewports. - for (cx, cz, w, h) in &rects { - let mut vp = acadrust::entities::Viewport::new(); - vp.center = acadrust::types::Vector3::new(*cx, 0.0, *cz); - vp.width = *w; - vp.height = *h; - vp.id = 2; // commit_entity will assign unique IDs - match self.tabs[i].scene.document.add_entity_to_layout( - acadrust::EntityType::Viewport(vp), - &layout_name, - ) { - Ok(handle) => { - self.tabs[i].scene.auto_fit_viewport(handle); - } - Err(e) => { - self.command_line.push_error(&format!("VPORTS: {e}")); - } - } - } - // Re-assign unique IDs (1 + existing max per viewport). - let layout_block2 = self.tabs[i].scene.current_layout_block_handle_pub(); - let mut id_counter = 2_i16; - let handles: Vec = self.tabs[i] - .scene - .document - .entities() - .filter_map(|e| { - if let acadrust::EntityType::Viewport(vp) = e { - if vp.id >= 2 && vp.common.owner_handle == layout_block2 { - Some(vp.common.handle) - } else { - None - } - } else { - None - } - }) - .collect(); - for h in handles { - if let Some(acadrust::EntityType::Viewport(vp)) = - self.tabs[i].scene.document.get_entity_mut(h) - { - vp.id = id_counter; - id_counter += 1; - } - } - self.tabs[i].dirty = true; - self.command_line.push_output(&format!( - "VPORTS: created {} viewport(s) [{}].", - rects.len(), - sub - )); - } - } - } - - // ── VPLAYER — per-viewport layer freeze/thaw ────────────────── - "VPLAYER" => { - let scene = &self.tabs[i].scene; - if scene.current_layout == "Model" { - self.command_line - .push_error("VPLAYER: switch to a paper space layout first."); - } else if scene.active_viewport.is_none() { - self.command_line - .push_error("VPLAYER: enter a viewport first (double-click or MS)."); - } else { - use crate::modules::layout::vplayer::VplayerCommand; - let vp_handle = scene.active_viewport.unwrap(); - // Collect current frozen layer names for display. - let frozen_names: Vec = { - if let Some(acadrust::EntityType::Viewport(vp)) = - scene.document.get_entity(vp_handle) - { - vp.frozen_layers - .iter() - .filter_map(|h| { - scene - .document - .layers - .iter() - .find(|l| l.handle == *h) - .map(|l| l.name.clone()) - }) - .collect() - } else { - vec![] - } - }; - if frozen_names.is_empty() { - self.command_line - .push_info("VPLAYER: no frozen layers in active viewport."); - } else { - self.command_line.push_info(&format!( - "VPLAYER: frozen layers: {}", - frozen_names.join(", ") - )); - } - let new_cmd = VplayerCommand::new(vp_handle); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - } - - // ── Draw Order ──────────────────────────────────────────────── - cmd if cmd.starts_with("DRAWORDER") => { - use acadrust::objects::{ObjectType, SortEntitiesTable}; - let parts: Vec<&str> = cmd.split_whitespace().collect(); - let option = parts.get(1).unwrap_or(&"").to_uppercase(); - let i = self.active_tab; - let selected: Vec = self.tabs[i] - .scene - .selected_entities() - .iter() - .map(|(h, _)| *h) - .collect(); - if selected.is_empty() { - self.command_line - .push_error("DRAWORDER: select entities first."); - } else { - // Parse relative target handle for ABOVE/UNDER. - let relative_target: Option<(bool, acadrust::Handle)> = match option.as_str() { - "A" | "ABOVE" => { - let h_val = parts.get(2).and_then(|s| u64::from_str_radix(s, 16).ok()); - h_val.map(|v| (true, acadrust::Handle::new(v))) - } - "U" | "UNDER" | "BELOW" => { - let h_val = parts.get(2).and_then(|s| u64::from_str_radix(s, 16).ok()); - h_val.map(|v| (false, acadrust::Handle::new(v))) - } - _ => None, - }; - let to_front_opt = match option.as_str() { - "F" | "FRONT" => Some(true), - "B" | "BACK" => Some(false), - _ => None, - }; - - if relative_target.is_some() || to_front_opt.is_some() { - self.push_undo_snapshot(i, "DRAWORDER"); - let block_handle = self.tabs[i].scene.current_layout_block_handle_pub(); - - // For FRONT/BACK, anchor the new sort handle to the - // block's current effective draw-order range so the moved - // entities land strictly above/below every sibling — - // including ones not yet in the table, which sort by - // their own handle. (min_eff, max_eff) over siblings. - let fb_baseline: Option<(u64, u64)> = if to_front_opt.is_some() { - let selected_set: rustc_hash::FxHashSet = - selected.iter().map(|h| h.value()).collect(); - let doc_ref = &self.tabs[i].scene.document; - let overrides: rustc_hash::FxHashMap = doc_ref - .objects - .values() - .find_map(|obj| { - if let ObjectType::SortEntitiesTable(t) = obj { - if t.block_owner_handle == block_handle { - return Some( - t.entries() - .map(|e| { - ( - e.entity_handle.value(), - e.sort_handle.value(), - ) - }) - .collect(), - ); - } - } - None - }) - .unwrap_or_default(); - let mut max_eff = 0u64; - let mut min_eff = u64::MAX; - for e in doc_ref.entities() { - let c = e.common(); - let hv = c.handle.value(); - if selected_set.contains(&hv) { - continue; - } - if c.owner_handle != block_handle && !c.owner_handle.is_null() { - continue; - } - let eff = overrides.get(&hv).copied().unwrap_or(hv); - max_eff = max_eff.max(eff); - min_eff = min_eff.min(eff); - } - if min_eff == u64::MAX { - min_eff = 1; - } - Some((min_eff, max_eff)) - } else { - None - }; - - let doc = &mut self.tabs[i].scene.document; - let table_handle = doc.objects.iter().find_map(|(h, obj)| { - if let ObjectType::SortEntitiesTable(t) = obj { - if t.block_owner_handle == block_handle { - Some(*h) - } else { - None - } - } else { - None - } - }); - let get_or_create = - |doc: &mut acadrust::CadDocument, block_handle| -> acadrust::Handle { - if let Some(th) = doc.objects.iter().find_map(|(h, obj)| { - if let ObjectType::SortEntitiesTable(t) = obj { - if t.block_owner_handle == block_handle { - Some(*h) - } else { - None - } - } else { - None - } - }) { - th - } else { - let nh = acadrust::Handle::new(doc.next_handle()); - let mut table = SortEntitiesTable::for_block(block_handle); - table.handle = nh; - doc.objects.insert(nh, ObjectType::SortEntitiesTable(table)); - nh - } - }; - let th = table_handle.unwrap_or_else(|| { - let nh = acadrust::Handle::new(doc.next_handle()); - let mut table = SortEntitiesTable::for_block(block_handle); - table.handle = nh; - doc.objects.insert(nh, ObjectType::SortEntitiesTable(table)); - nh - }); - let _ = get_or_create; // suppress unused warning - if let Some(ObjectType::SortEntitiesTable(table)) = doc.objects.get_mut(&th) - { - if let Some((above, target)) = relative_target { - // move_above/move_below read the target's sort - // handle from the table and no-op when it is - // absent. A reference object that was never - // reordered isn't in the table yet, so seed it - // with its own handle as the implicit sort key. - if !table.contains(target) { - table.add_entry(target, target); - } - for h in &selected { - if above { - table.move_above(*h, target); - } else { - table.move_below(*h, target); - } - } - let rel = if above { "above" } else { "below" }; - self.command_line.push_info(&format!( - "DRAWORDER: moved {} entities {} {:x}.", - selected.len(), - rel, - target.value() - )); - } else if let Some(to_front) = to_front_opt { - let (min_eff, max_eff) = fb_baseline.unwrap_or((1, 0)); - for (k, h) in selected.iter().enumerate() { - let sort = if to_front { - max_eff.saturating_add(1 + k as u64) - } else { - min_eff.saturating_sub(1 + k as u64).max(1) - }; - table.add_entry(*h, acadrust::Handle::new(sort)); - } - let dir = if to_front { "front" } else { "back" }; - self.command_line.push_info(&format!( - "DRAWORDER: moved {} entities to {}.", - selected.len(), - dir - )); - } - } - // Sort order lives in SortEntitiesTable, which the - // render-side `sort_cache` rebuilds per geometry epoch. - // Bump it so the new draw order shows immediately - // instead of waiting for an unrelated geometry change. - self.tabs[i].scene.bump_geometry(); - self.tabs[i].dirty = true; - } else { - self.command_line.push_info( - "Usage: DRAWORDER F|FRONT | B|BACK | A|ABOVE | U|UNDER " - ); - } - } - } - - // ── LAYER management ───────────────────────────────────────── - cmd if cmd == "LAYER" || cmd.starts_with("LAYER ") || cmd.starts_with("LA ") => { - use acadrust::tables::Layer; - let raw_rest = if cmd.starts_with("LAYER ") { - cmd.trim_start_matches("LAYER ").trim() - } else if cmd.starts_with("LA ") { - cmd.trim_start_matches("LA ").trim() - } else { - "" - }; - let parts: Vec<&str> = raw_rest.split_whitespace().collect(); - let sub = parts.get(0).map(|s| s.to_uppercase()).unwrap_or_default(); - match sub.as_str() { - "" | "LIST" | "?" => { - let info: Vec = self.tabs[i] - .scene - .document - .layers - .iter() - .map(|l| { - let state = if l.flags.frozen { - "frozen" - } else if l.flags.off { - "off" - } else if l.flags.locked { - "locked" - } else { - "on" - }; - format!("{}({})", l.name, state) - }) - .collect(); - self.command_line - .push_output(&format!("Layers: {}", info.join(", "))); - } - "NEW" | "N" => { - let name = parts.get(1).map(|s| s.trim()).unwrap_or("").to_string(); - if name.is_empty() { - self.command_line.push_error("Usage: LAYER NEW "); - } else if self.tabs[i].scene.document.layers.contains(&name) { - self.command_line - .push_error(&format!("LAYER: '{}' already exists.", name)); - } else { - let mut layer = Layer::new(&name); - // Allocate a unique handle so the layer survives a - // DWG save (handle-based format; issue #67). - layer.handle = self.tabs[i].scene.document.allocate_handle(); - let _ = self.tabs[i].scene.document.layers.add(layer); - self.push_undo_snapshot(i, "LAYER NEW"); - self.tabs[i].dirty = true; - self.command_line - .push_output(&format!("LAYER: '{}' created.", name)); - } - } - "ON" => { - for name in &parts[1..] { - if let Some(l) = self.tabs[i].scene.document.layers.get_mut(name) { - l.flags.off = false; - l.flags.frozen = false; - } - } - self.push_undo_snapshot(i, "LAYER ON"); - self.tabs[i].dirty = true; - self.command_line.push_output("LAYER: layers turned on."); - } - "OFF" => { - for name in &parts[1..] { - if let Some(l) = self.tabs[i].scene.document.layers.get_mut(name) { - l.flags.off = true; - } - } - self.push_undo_snapshot(i, "LAYER OFF"); - self.tabs[i].dirty = true; - self.command_line.push_output("LAYER: layers turned off."); - } - "FREEZE" | "FR" => { - for name in &parts[1..] { - if let Some(l) = self.tabs[i].scene.document.layers.get_mut(name) { - l.flags.frozen = true; - } - } - self.push_undo_snapshot(i, "LAYER FREEZE"); - self.tabs[i].dirty = true; - self.command_line.push_output("LAYER: layers frozen."); - } - "THAW" | "TH" => { - for name in &parts[1..] { - if let Some(l) = self.tabs[i].scene.document.layers.get_mut(name) { - l.flags.frozen = false; - } - } - self.push_undo_snapshot(i, "LAYER THAW"); - self.tabs[i].dirty = true; - self.command_line.push_output("LAYER: layers thawed."); - } - "LOCK" | "LO" => { - for name in &parts[1..] { - if let Some(l) = self.tabs[i].scene.document.layers.get_mut(name) { - l.flags.locked = true; - } - } - self.push_undo_snapshot(i, "LAYER LOCK"); - self.tabs[i].dirty = true; - self.command_line.push_output("LAYER: layers locked."); - } - "UNLOCK" | "UL" => { - for name in &parts[1..] { - if let Some(l) = self.tabs[i].scene.document.layers.get_mut(name) { - l.flags.locked = false; - } - } - self.push_undo_snapshot(i, "LAYER UNLOCK"); - self.tabs[i].dirty = true; - self.command_line.push_output("LAYER: layers unlocked."); - } - "COLOR" | "C" => { - // LAYER COLOR - let layer_name = parts.get(1).map(|s| s.trim()).unwrap_or("").to_string(); - let color_str = parts.get(2).map(|s| s.trim()).unwrap_or(""); - if let Ok(idx) = color_str.parse::() { - if let Some(l) = self.tabs[i].scene.document.layers.get_mut(&layer_name) - { - l.color = acadrust::types::Color::from_index(idx); - self.push_undo_snapshot(i, "LAYER COLOR"); - self.tabs[i].dirty = true; - self.command_line.push_output(&format!( - "LAYER: '{}' color set to ACI {}.", - layer_name, idx - )); - } else { - self.command_line - .push_error(&format!("LAYER: '{}' not found.", layer_name)); - } - } else { - self.command_line - .push_error("Usage: LAYER COLOR "); - } - } - "SET" | "S" | "CURRENT" => { - let name = parts.get(1).map(|s| s.trim()).unwrap_or("").to_string(); - if self.tabs[i].scene.document.layers.contains(&name) { - self.tabs[i].layers.current_layer = name.clone(); - self.command_line - .push_output(&format!("LAYER: current layer set to '{}'.", name)); - } else { - self.command_line - .push_error(&format!("LAYER: '{}' not found.", name)); - } - } - _ => { - self.command_line.push_info( - "Usage: LAYER LIST | NEW | ON/OFF/FREEZE/THAW/LOCK/UNLOCK | COLOR | SET " - ); - } - } - } - - // Bare UCS → interactive front-end (option then value as steps), so - // `UCS Z 90` is typable in the command line and works headlessly. - // The front-end delegates back to the inline handler below. (#169) - "UCS" => { - use crate::modules::view::ucs_cmd::UcsCommand; - let cmd_obj = UcsCommand::new(); - self.command_line.push_info(&cmd_obj.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd_obj)); - } - - // ── UCS management (inline `UCS