use super::*; impl OpenCADStudio { pub(super) fn dispatch_view(&mut self, cmd: &str, i: usize) -> Option> { match cmd { "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 Some(Task::done(Message::AboutOpen)); } "PLUGINS" | "PLUGINMANAGER" => { return Some(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 Some(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 Some(Task::none()); } _ => { self.command_line.push_error(&format!( "COLORSCHEME: unknown theme '{}'. Type COLORSCHEME LIST for options.", sub )); return Some(Task::none()); } }; if let Some(t) = theme { let name = format!("{:?}", t); self.command_line .push_output(&format!("Color scheme set to '{name}'.")); return Some(Task::done(Message::SetTheme(t))); } return Some(Task::none()); } // ── Layout Manager GUI ───────────────────────────────────────── "LAYOUTMANAGER" | "LAYOUTPANEL" => { return Some(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 Some(Task::done(Message::MspaceCommand)); } "PSPACE" => { return Some(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 Some(self.focus_cmd_input()); } // Model space: split the tiled viewport layout via pane_grid. use iced::widget::pane_grid::{Axis, Configuration as C}; let split = |axis, a, b| C::Split { axis, ratio: 0.5, a: Box::new(a), b: Box::new(b), }; let config: Option<(C, usize)> = match sub.as_str() { "SINGLE" | "SI" | "1" => Some((C::Pane(0), 1)), "2H" | "2" => { Some((split(Axis::Horizontal, C::Pane(0), C::Pane(1)), 2)) } "2V" => Some((split(Axis::Vertical, C::Pane(0), C::Pane(1)), 2)), "4" => Some(( split( Axis::Vertical, split(Axis::Horizontal, C::Pane(0), C::Pane(2)), split(Axis::Horizontal, C::Pane(1), C::Pane(3)), ), 4, )), _ => None, }; match config { Some((config, n)) => { self.tabs[i].scene.set_model_panes(config); 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 " ); } } } _ => return None, } Some(self.finish_dispatch(cmd)) } } // ── Draw Order: interactive reference-object pick ────────────────────────── /// Moves a captured selection above or below a reference object the user /// picks in the viewport. On pick it relaunches `DRAWORDER A|U ` /// with the captured handles reinstalled as the selection, so the existing /// command path performs the actual reorder. pub(crate) struct DrawOrderRefCommand { to_move: Vec, above: bool, } impl DrawOrderRefCommand { pub(crate) fn new(to_move: Vec, above: bool) -> Self { Self { to_move, above } } } impl CadCommand for DrawOrderRefCommand { fn name(&self) -> &'static str { "DRAWORDER" } fn prompt(&self) -> String { if self.above { "DRAWORDER Select reference object (move selection above):".into() } else { "DRAWORDER Select reference object (move selection under):".into() } } fn needs_entity_pick(&self) -> bool { true } fn on_entity_pick( &mut self, handle: acadrust::Handle, _pt: glam::DVec3, ) -> crate::command::CmdResult { if handle.is_null() { return crate::command::CmdResult::NeedPoint; } let opt = if self.above { "A" } else { "U" }; let cmd = format!("DRAWORDER {} {:x}", opt, handle.value()); crate::command::CmdResult::Relaunch(cmd, std::mem::take(&mut self.to_move)) } fn on_point(&mut self, _pt: glam::DVec3) -> crate::command::CmdResult { crate::command::CmdResult::NeedPoint } fn on_enter(&mut self) -> crate::command::CmdResult { crate::command::CmdResult::Cancel } }