use super::*; impl OpenCADStudio { pub(super) fn dispatch_display(&mut self, cmd: &str, i: usize) -> Option> { match cmd { // Interactive pan: left-drag pans the view until Esc. The only pan // path when there is no middle mouse button (trackpad / web). "PAN" => { self.tabs[i].pan_mode = true; self.command_line .push_output("PAN: drag with the left mouse button. Press Esc to exit."); } // ── TABLE cell editing ───────────────────────────────────────────── // TABLE CELL — set text for a cell in the selected Table cmd if cmd.starts_with("TABLE ") => { let rest = cmd.trim_start_matches("TABLE").trim(); let sub_up = rest.split_whitespace().next().unwrap_or("").to_uppercase(); if sub_up == "CELL" { let parts: Vec<&str> = rest.splitn(4, char::is_whitespace).collect(); // parts: ["CELL", "", "", ""] let row_res = parts.get(1).and_then(|s| s.parse::().ok()); let col_res = parts.get(2).and_then(|s| s.parse::().ok()); let text = parts.get(3).copied().unwrap_or(""); match (row_res, col_res) { (Some(row), Some(col)) => { let selected_handles: Vec = self.tabs[i] .scene .selected_entities() .iter() .map(|(h, _)| *h) .collect(); let mut found = false; for sh in &selected_handles { if let Some(acadrust::EntityType::Table(tbl)) = self.tabs[i] .scene .document .entities_mut() .find(|e| e.common().handle == *sh) { if tbl.set_cell_text(row, col, text) { found = true; } } } if found { self.push_undo_snapshot(i, "TABLE CELL"); self.tabs[i].dirty = true; self.command_line.push_output(&format!( "TABLE CELL: set [{row},{col}] = \"{text}\"." )); } else { self.command_line.push_error( "TABLE CELL: select a Table entity first, or row/col out of range." ); } } _ => { self.command_line .push_info("Usage: TABLE CELL "); } } } else { self.command_line.push_info( "Usage: TABLE (creates new table) or TABLE CELL ", ); } } // ── UCSICON — toggle UCS icon visibility on all viewports ──────────── // UCSICON ON — show UCS icon in all viewports // UCSICON OFF — hide UCS icon in all viewports // UCSICON NOORIGIN — show icon but not at origin (show at corner) // UCSICON ORIGIN — show icon at UCS origin cmd if cmd == "UCSICON" || cmd.starts_with("UCSICON ") => { let sub = cmd.split_whitespace().nth(1).unwrap_or("").to_uppercase(); match sub.as_str() { "ON" | "OFF" | "NOORIGIN" | "ORIGIN" => { self.push_undo_snapshot(i, "UCSICON"); let visible = sub != "OFF"; let at_origin = sub == "ORIGIN"; // Update model-space icon flags. self.show_ucs_icon = visible; self.ribbon.set_ucs_icon(visible); if sub == "NOORIGIN" || sub == "ORIGIN" { self.ucs_icon_at_origin = at_origin; } let mut count = 0usize; for entity in self.tabs[i].scene.document.entities_mut() { if let acadrust::EntityType::Viewport(vp) = entity { vp.status.ucs_icon_visible = visible; if sub == "NOORIGIN" || sub == "ORIGIN" { vp.status.ucs_icon_at_origin = at_origin; } count += 1; } } self.tabs[i].dirty = true; self.command_line.push_output(&format!( "UCSICON {sub}: updated {count} viewport(s) + model space." )); } "" => { // Bare UCSICON toggles visibility. self.push_undo_snapshot(i, "UCSICON"); let visible = !self.show_ucs_icon; self.show_ucs_icon = visible; self.ribbon.set_ucs_icon(visible); for entity in self.tabs[i].scene.document.entities_mut() { if let acadrust::EntityType::Viewport(vp) = entity { vp.status.ucs_icon_visible = visible; } } self.tabs[i].dirty = true; let state = if visible { "ON" } else { "OFF" }; self.command_line.push_output(&format!("UCSICON {state}")); } _ => { self.command_line .push_info("Usage: UCSICON ON | OFF | NOORIGIN | ORIGIN"); } } } // ── NAVVCUBE — toggle ViewCube visibility ──────────────────────────── "NAVVCUBE" => { return Some(Task::done(Message::ToggleViewCube)); } // ── PROPERTIES — toggle Properties panel visibility ────────────────── "PROPERTIES" | "PROPS" => { return Some(Task::done(Message::ToggleProperties)); } // ── FILETAB — toggle file/document tabs ────────────────────────────── "FILETAB" => { return Some(Task::done(Message::ToggleFileTabs)); } // ── LAYOUTTAB — toggle layout/paper-space tabs ─────────────────────── "LAYOUTTAB" => { return Some(Task::done(Message::ToggleLayoutTabs)); } // ── Drafting aids — same toggles the status-bar pills drive, also // reachable by name from the command line. ───────────────────────── // GRID — show / hide the reference grid. "GRID" => { return Some(Task::done(Message::ToggleGrid)); } // SNAP — toggle cursor snapping to the grid. "SNAP" => { return Some(Task::done(Message::ToggleGridSnap)); } // POLAR — toggle polar tracking. "POLAR" => { return Some(Task::done(Message::TogglePolar)); } // DSETTINGS / OSNAP — open the drafting-settings popup, which is OCS's // settings surface (the persisted DYN/ORTHO/POLAR/OSNAP prefs). "DSETTINGS" | "OSNAP" => { return Some(Task::done(Message::ToggleSnapPopup)); } // UNITS — open the drawing-units picker (linear / angular format). "UNITS" | "DDUNITS" => { return Some(Task::done(Message::ToggleUnitsPopup)); } // ── CLEANSCREEN — collapse the surrounding panels for a full canvas ── "CLEANSCREEN" => { return Some(Task::done(Message::ToggleCleanScreen)); } // ── QUICKPROPERTIES — toggle the floating quick-properties readout ─── "QUICKPROPERTIES" => { return Some(Task::done(Message::ToggleQuickProperties)); } // ── TOOLPALETTES — not yet implemented ─────────────────────────────── "TOOLPALETTES" => { self.command_line .push_info("TOOLPALETTES: Tool Palettes not yet implemented."); } // ── SHEETSET — not yet implemented ─────────────────────────────────── "SHEETSET" => { self.command_line .push_info("SHEETSET: Sheet Set Manager not yet implemented."); } // ── XDATA — read/write extended entity data ────────────────────────── // XDATA LIST — show all xdata records on selected entities // XDATA SET — append a string xdata value for // XDATA CLEAR — remove all xdata from selected entities // XDATA CLEAR — remove xdata for a specific application cmd if cmd == "XDATA" || cmd.starts_with("XDATA ") => { use acadrust::xdata::{ExtendedDataRecord, XDataValue}; let rest = cmd.trim_start_matches("XDATA").trim(); let parts: Vec<&str> = rest.splitn(3, char::is_whitespace).collect(); let sub = parts.first().map(|s| s.to_uppercase()).unwrap_or_default(); 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("XDATA: select entities first."); } else { match sub.as_str() { "LIST" | "" => { for sh in &selected_handles { if let Some(entity) = self.tabs[i].scene.document.get_entity(*sh) { let xd = &entity.common().extended_data; if xd.is_empty() { self.command_line .push_output(&format!(" {:x}: no xdata.", sh.value())); } else { for rec in xd.records() { self.command_line.push_output(&format!( " {:x} [{}]: {} value(s)", sh.value(), rec.application_name, rec.values.len() )); for v in &rec.values { self.command_line .push_output(&format!(" {:?}", v)); } } } } } } "SET" => { let app = parts.get(1).copied().unwrap_or("OpenCADStudio"); let val = parts.get(2).copied().unwrap_or(""); self.push_undo_snapshot(i, "XDATA SET"); for sh in &selected_handles { if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(*sh) { let mut rec = ExtendedDataRecord::new(app); rec.add_value(XDataValue::String(val.to_string())); entity.common_mut().extended_data.add_record(rec); } } self.tabs[i].dirty = true; self.command_line.push_output(&format!( "XDATA: set [{app}] = \"{val}\" on {} entity/entities.", selected_handles.len() )); } "CLEAR" => { let app_filter = parts.get(1).copied(); self.push_undo_snapshot(i, "XDATA CLEAR"); for sh in &selected_handles { if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(*sh) { let xd = &mut entity.common_mut().extended_data; if let Some(app) = app_filter { // Rebuild without the matching app. let kept: Vec<_> = xd .records() .iter() .filter(|r| r.application_name != app) .cloned() .collect(); xd.clear(); for r in kept { xd.add_record(r); } } else { xd.clear(); } } } self.tabs[i].dirty = true; self.command_line.push_output("XDATA: cleared."); } _ => { self.command_line .push_info("Usage: XDATA LIST | SET | CLEAR [app]"); } } } } // BOX / SPHERE / CYLINDER / CONE / WEDGE / TORUS are handled by the // Model-tab primitive command above (with truck boolean caching). // ── EXTRUDE ──────────────────────────────────────────────────── // PRESSPULL on a closed boundary creates a solid by extruding it to a // height — the same operation as EXTRUDE. THICKEN turns a closed planar // profile into a solid of the given thickness, which is also an extrude. "EXTRUDE" | "PRESSPULL" | "THICKEN" => { use crate::modules::insert::solid3d_cmds::ExtrudeCommand; // If a single entity is already selected, skip the pick step. let selected: Vec<_> = self.tabs[i].scene.selected_entities().into_iter().collect(); let color = self.tabs[i].scene.layer_color(&self.tabs[i].active_layer); if selected.len() == 1 { let handle = selected[0].0; let mut cmd = ExtrudeCommand::new(color); cmd.on_entity_pick(handle, glam::DVec3::ZERO); self.command_line.push_info(&cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(cmd)); } else { let cmd = ExtrudeCommand::new(color); self.command_line.push_info(&cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(cmd)); } } // ── REVOLVE ──────────────────────────────────────────────────── "REVOLVE" => { use crate::modules::insert::solid3d_cmds::RevolveCommand; let color = self.tabs[i].scene.layer_color(&self.tabs[i].active_layer); let cmd = RevolveCommand::new(color); self.command_line.push_info(&cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(cmd)); } // ── SWEEP ────────────────────────────────────────────────────── "SWEEP" => { use crate::modules::insert::solid3d_cmds::SweepCommand; let color = self.tabs[i].scene.layer_color(&self.tabs[i].active_layer); let cmd = SweepCommand::new(color); self.command_line.push_info(&cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(cmd)); } // ── LOFT ─────────────────────────────────────────────────────── "LOFT" => { use crate::modules::insert::solid3d_cmds::LoftCommand; let color = self.tabs[i].scene.layer_color(&self.tabs[i].active_layer); let cmd = LoftCommand::new(color); self.command_line.push_info(&cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(cmd)); } // ── OBJ import ─────────────────────────────────────────────── "IMPORTOBJ" | "OBJIMPORT" => { return Some(Task::done(Message::ObjImport)); } // ── STL export ──────────────────────────────────────────────── "STLOUT" | "EXPORTSTL" => { return Some(Task::done(Message::StlExport)); } // STEPOUT — export 3D meshes to STEP AP203 format "STEPOUT" | "EXPORTSTEP" | "STPOUT" => { return Some(Task::done(Message::StepExport)); } // ── Plot Style Editor GUI ───────────────────────────────────── "PLOTSTYLEPANEL" | "PLOTSTYLEEDITOR" | "STYLESMANAGER" => { return Some(Task::done(Message::PlotStylePanelOpen)); } // ── Plot / Page Setup ────────────────────────────────────────── // PLOT / PRINT open the full plot dialog (printer, paper, scale, // options); EXPORT / EXPORTPDF stay a direct PDF export. "PLOT" | "PRINT" => { return Some(Task::done(Message::PlotDialogOpen)); } // With no argument these open the save dialog; with a path they // export straight to it. The direct form is also the workaround for // systems where the native save dialog cannot open at all (broken // XDG portal / missing zenity on Linux) — there the dialog future // resolves to None and no export would happen. (#369) cmd if cmd == "EXPORT" || cmd == "EXPORTPDF" || cmd.starts_with("EXPORT ") || cmd.starts_with("EXPORTPDF ") => { // Strip the keyword actually typed — EXPORTPDF first, since // EXPORT is its prefix (see #295 for the failure mode). let arg = cmd .strip_prefix("EXPORTPDF") .or_else(|| cmd.strip_prefix("EXPORT")) .unwrap_or("") .trim() .trim_matches('"'); if arg.is_empty() { return Some(Task::done(Message::PlotExport)); } let mut path = std::path::PathBuf::from(arg); if path.extension().is_none() { path.set_extension("pdf"); } // A bare filename lands next to the drawing when it has a home. if path.is_relative() { if let Some(dir) = self.tabs[i].current_path.as_deref().and_then(|p| p.parent()) { path = dir.join(path); } } return Some(self.on_plot_export_path_some(path)); } // PLOTSTYLE — load or clear CTB/STB plot style table cmd if cmd == "PLOTSTYLE" || cmd.starts_with("PLOTSTYLE ") => { let sub = cmd .split_once(' ') .map(|(_, r)| r.trim().to_uppercase()) .unwrap_or_default(); match sub.as_str() { "CLEAR" | "NONE" => { return Some(Task::done(Message::PlotStyleClear)); } "" | "LOAD" => { let active = self .active_plot_style .as_ref() .map(|t| format!("Active: {}", t.name)) .unwrap_or_else(|| "No plot style loaded.".into()); self.command_line.push_info(&active); return Some(Task::done(Message::PlotStyleLoad)); } "?" | "STATUS" => { let msg = self .active_plot_style .as_ref() .map(|t| { format!( "Plot style: {} ({} color overrides)", t.name, t.aci_entries.iter().filter(|e| e.color.is_some()).count() ) }) .unwrap_or_else(|| "No plot style table loaded.".into()); self.command_line.push_output(&msg); } _ => { self.command_line .push_error("Usage: PLOTSTYLE [LOAD | CLEAR | STATUS]"); } } } // UNDERLAY — edit properties of selected PDF/DWF/DGN underlay entities. // Usage: // UNDERLAY FADE <0-80> // UNDERLAY CONTRAST <0-100> // UNDERLAY ON | OFF // UNDERLAY CLIP ON | OFF // UNDERLAY MONO ON | OFF cmd if cmd == "UNDERLAY" || cmd.starts_with("UNDERLAY ") => { let sub = cmd .split_once(' ') .map(|(_, r)| r.trim().to_uppercase()) .unwrap_or_default(); let handles: Vec = self.tabs[i] .scene .selected_entities() .iter() .map(|(h, _)| *h) .collect(); if handles.is_empty() { self.command_line .push_error("UNDERLAY: select underlay entities first."); } else { let parts: Vec<&str> = sub.splitn(2, char::is_whitespace).collect(); let action = parts.first().copied().unwrap_or(""); let arg = parts.get(1).copied().unwrap_or("").trim(); let mut changed = 0usize; self.push_undo_snapshot(i, "UNDERLAY"); for h in &handles { if let Some(acadrust::EntityType::Underlay(ul)) = self.tabs[i] .scene .document .entities_mut() .find(|e| e.common().handle == *h) { match action { "FADE" => { if let Ok(v) = arg.parse::() { ul.set_fade(v); changed += 1; } } "CONTRAST" => { if let Ok(v) = arg.parse::() { ul.set_contrast(v); changed += 1; } } "ON" => { ul.set_on(true); changed += 1; } "OFF" => { ul.set_on(false); changed += 1; } "CLIP" => match arg { "ON" => { ul.flags |= acadrust::entities::UnderlayDisplayFlags::CLIPPING; changed += 1; } "OFF" => { ul.clear_clip(); changed += 1; } _ => {} }, "MONO" => match arg { "ON" => { ul.set_monochrome(true); changed += 1; } "OFF" => { ul.set_monochrome(false); changed += 1; } _ => {} }, _ => { // No sub-command: print status. self.command_line.push_output(&format!( "Underlay {:x}: fade={}, contrast={}, on={}, clip={}, mono={}", h.value(), ul.fade, ul.contrast, ul.is_on(), ul.is_clipping(), ul.is_monochrome(), )); } } } } if changed > 0 { self.tabs[i].dirty = true; self.command_line .push_info(&format!("Updated {changed} underlay(s).")); } else if !action.is_empty() { self.command_line.push_error( "Usage: UNDERLAY [FADE |CONTRAST |ON|OFF|CLIP ON|OFF|MONO ON|OFF]" ); } } } // PAGESETUP is folded into the unified plot dialog. "PAGESETUP" => { return Some(Task::done(Message::PlotDialogOpen)); } // ── Recognized commands whose full implementation is pending ───────── // These verbs are surfaced by the ribbon / menus but their feature is // still being built. Acknowledge them with an honest status so the // button responds instead of reporting an unknown command; each is // replaced by its real handler as the feature lands. // OBJECTSCALE — mark the selected objects annotative by attaching the // AcAnnotativeData XData record the tessellator already honours, so // they scale with the current annotation scale. cmd if cmd == "OBJECTSCALE" || cmd.starts_with("OBJECTSCALE ") => { use acadrust::xdata::{ExtendedDataRecord, XDataValue}; let handles: Vec = self.tabs[i] .scene .selected_entities() .iter() .map(|(h, _)| *h) .collect(); if handles.is_empty() { self.command_line .push_error("OBJECTSCALE: select objects first."); return Some(Task::none()); } self.push_undo_snapshot(i, "OBJECTSCALE"); let mut n = 0usize; for h in &handles { if let Some(e) = self.tabs[i].scene.document.get_entity_mut(*h) { let xd = &mut e.common_mut().extended_data; if xd.get_record("AcAnnotativeData").is_none() { let mut rec = ExtendedDataRecord::new("AcAnnotativeData"); rec.add_value(XDataValue::String("1".to_string())); xd.add_record(rec); } n += 1; } } self.tabs[i].scene.bump_geometry(); self.tabs[i].dirty = true; self.command_line.push_output(&format!( "OBJECTSCALE: marked {n} object(s) annotative (they scale with the annotation scale)." )); return Some(Task::none()); } // HYPERLINK — attach a hyperlink to the selected objects, stored // in the standard PE_URL XData record so it round-trips in the file. cmd if cmd == "HYPERLINK" || cmd.starts_with("HYPERLINK ") => { use acadrust::xdata::{ExtendedDataRecord, XDataValue}; let url = cmd.strip_prefix("HYPERLINK").unwrap_or("").trim().to_string(); if url.is_empty() { self.command_line.push_info("Usage: HYPERLINK (select objects first)"); return Some(Task::none()); } let handles: Vec = self.tabs[i].scene.selected_entities().iter().map(|(h, _)| *h).collect(); if handles.is_empty() { self.command_line.push_error("HYPERLINK: select objects first."); return Some(Task::none()); } self.push_undo_snapshot(i, "HYPERLINK"); let mut n = 0usize; for h in &handles { if let Some(e) = self.tabs[i].scene.document.get_entity_mut(*h) { let xd = &mut e.common_mut().extended_data; let mut rec = ExtendedDataRecord::new("PE_URL"); rec.add_value(XDataValue::String(url.clone())); xd.add_record(rec); n += 1; } } self.tabs[i].dirty = true; self.command_line .push_output(&format!("HYPERLINK: attached to {n} object(s).")); return Some(Task::none()); } // ADJUST — set brightness / contrast / fade on selected raster images // ADJUST BRIGHTNESS|CONTRAST|FADE <0-100> cmd if cmd == "ADJUST" || cmd.starts_with("ADJUST ") => { let rest = cmd.trim_start_matches("ADJUST").trim(); let parts: Vec<&str> = rest.splitn(2, char::is_whitespace).collect(); let action = parts.first().map(|s| s.to_uppercase()).unwrap_or_default(); let arg = parts.get(1).copied().unwrap_or("").trim(); let handles: Vec = self.tabs[i] .scene .selected_entities() .iter() .map(|(h, _)| *h) .collect(); if handles.is_empty() { self.command_line .push_error("ADJUST: select raster image(s) first."); } else if action.is_empty() { self.command_line .push_info("Usage: ADJUST BRIGHTNESS|CONTRAST|FADE <0-100>"); } else if let Ok(v) = arg.parse::() { let v = v.min(100); self.push_undo_snapshot(i, "ADJUST"); let mut changed = 0usize; for h in &handles { if let Some(acadrust::EntityType::RasterImage(img)) = self.tabs[i] .scene .document .entities_mut() .find(|e| e.common().handle == *h) { match action.as_str() { "BRIGHTNESS" => { img.brightness = v; changed += 1; } "CONTRAST" => { img.contrast = v; changed += 1; } "FADE" => { img.fade = v; changed += 1; } _ => {} } } } if changed > 0 { self.tabs[i].dirty = true; self.tabs[i].scene.bump_geometry(); self.command_line .push_output(&format!("ADJUST: {action} = {v} on {changed} image(s).")); } else { self.command_line.push_error( "ADJUST: no raster images selected, or unknown property (use BRIGHTNESS|CONTRAST|FADE).", ); } } else { self.command_line.push_error("ADJUST: value must be 0-100."); } } // ANNOSCALE / CANNOSCALE — set the current annotation scale // (e.g. 1:50, 2:1, or a plain factor). Drives annotative-object size // in model space and is written to the drawing header. cmd if cmd == "ANNOSCALE" || cmd == "CANNOSCALE" || cmd.starts_with("ANNOSCALE ") || cmd.starts_with("CANNOSCALE ") => { let arg = cmd .split_whitespace() .nth(1) .unwrap_or("") .trim() .to_string(); if arg.is_empty() { let name = self.tabs[i] .scene .document .header .current_annotation_scale .clone(); self.command_line .push_output(&format!("Current annotation scale: {name}")); return Some(Task::none()); } // anno multiplier = denominator / numerator: 1:50 → 50, 2:1 → 0.5. let anno = if let Some((a, b)) = arg.split_once(':') { match (a.trim().parse::(), b.trim().parse::()) { (Ok(a), Ok(b)) if a != 0.0 => Some((b / a) as f32), _ => None, } } else { arg.parse::().ok() }; match anno { Some(v) if v > 0.0 => { self.tabs[i].scene.annotation_scale = v; let hdr = &mut self.tabs[i].scene.document.header; hdr.current_annotation_scale = arg.clone(); hdr.annotation_scale_value = 1.0 / v as f64; self.tabs[i].scene.bump_geometry(); self.tabs[i].dirty = true; self.command_line .push_output(&format!("Annotation scale: {arg}")); } _ => self .command_line .push_error("Usage: ANNOSCALE e.g. 1:50, 2:1, or a factor"), } } // SCALELISTEDIT — list / add / delete the drawing's annotation scales. // SCALELISTEDIT list // SCALELISTEDIT ADD 1:50 add (name is a paper:drawing ratio) // SCALELISTEDIT DELETE 1:50 remove (not the current scale) cmd if cmd == "SCALELISTEDIT" || cmd.starts_with("SCALELISTEDIT ") => { let rest = cmd.trim_start_matches("SCALELISTEDIT").trim(); let mut parts = rest.splitn(2, char::is_whitespace); let sub = parts.next().unwrap_or("").to_uppercase(); let arg = parts.next().unwrap_or("").trim(); match sub.as_str() { "ADD" => match arg.split_once(':') { Some((p, d)) => match (p.trim().parse::(), d.trim().parse::()) { (Ok(paper), Ok(drawing)) if paper > 0.0 && drawing > 0.0 => { self.push_undo_snapshot(i, "SCALELISTEDIT"); if self.tabs[i].scene.add_scale(arg, paper, drawing) { self.tabs[i].dirty = true; self.command_line .push_output(&format!("Added annotation scale {arg}.")); } else { self.command_line .push_info(&format!("Scale {arg} already exists.")); } } _ => self .command_line .push_error("SCALELISTEDIT ADD: use a ratio like 1:50."), }, None => self .command_line .push_error("SCALELISTEDIT ADD: use a ratio like 1:50."), }, "DELETE" | "REMOVE" => { let current = self.tabs[i] .scene .document .header .current_annotation_scale .clone(); if arg.is_empty() { self.command_line.push_info("Usage: SCALELISTEDIT DELETE "); } else if arg.eq_ignore_ascii_case(¤t) { self.command_line.push_error(&format!( "Cannot delete the current annotation scale ({arg})." )); } else { self.push_undo_snapshot(i, "SCALELISTEDIT"); if self.tabs[i].scene.remove_scale(arg) { self.tabs[i].dirty = true; self.command_line .push_output(&format!("Removed annotation scale {arg}.")); } else { self.command_line .push_info(&format!("No annotation scale named {arg}.")); } } } "" => { let names: Vec = self.tabs[i] .scene .scale_list() .into_iter() .map(|(n, _, _)| n) .collect(); if names.is_empty() { self.command_line.push_info("No annotation scales defined."); } else { self.command_line .push_output(&format!("Annotation scales: {}", names.join(", "))); } } _ => self .command_line .push_info("Usage: SCALELISTEDIT [ADD 1:50 | DELETE 1:50]"), } } // OBJECTSCALE — open the Annotation Object Scale dialog for the // selected object (add / remove its per-object scale representations). "OBJECTSCALE" => { return Some(Task::done(Message::AnnoObjectScaleOpen)); } // DATALINK — import a CSV file into a table placed at the // origin (one-time import; a live re-reading link is future work). cmd if cmd == "DATALINK" || cmd.starts_with("DATALINK ") => { let path = cmd.trim_start_matches("DATALINK").trim(); if path.is_empty() { self.command_line.push_info( "Usage: DATALINK — imports the CSV into a table at the origin.", ); return Some(Task::none()); } match std::fs::read_to_string(path) { Ok(text) => { let rows_data: Vec> = text .lines() .filter(|l| !l.trim().is_empty()) .map(|line| line.split(',').map(|s| s.trim().to_string()).collect()) .collect(); let nrows = rows_data.len(); let ncols = rows_data.iter().map(|r| r.len()).max().unwrap_or(0); if nrows == 0 || ncols == 0 { self.command_line .push_error("DATALINK: the CSV file is empty."); return Some(Task::none()); } use acadrust::entities::TableBuilder; use acadrust::types::Vector3; let mut table = TableBuilder::new(nrows, ncols) .at(Vector3::new(0.0, 0.0, 0.0)) .row_height(0.5) .column_width(2.0) .build(); for (r, row) in rows_data.iter().enumerate() { for (c, cell) in row.iter().enumerate() { table.set_cell_text(r, c, cell); } } self.push_undo_snapshot(i, "DATALINK"); self.tabs[i] .scene .add_entity_clone(acadrust::EntityType::Table(table)); self.tabs[i].scene.bump_geometry(); self.tabs[i].dirty = true; self.command_line.push_output(&format!( "DATALINK: imported {nrows}×{ncols} cells into a table at the origin." )); } Err(e) => { self.command_line .push_error(&format!("DATALINK: cannot read \"{path}\": {e}")); } } } // LANDXMLIMPORT — import survey points (LandXML // elements) as Point objects. Reads the coordinate text content // (northing easting elevation) → Point at (easting, northing, elev). cmd if cmd == "LANDXMLIMPORT" || cmd.starts_with("LANDXMLIMPORT ") => { let path = cmd.trim_start_matches("LANDXMLIMPORT").trim(); if path.is_empty() { self.command_line.push_info( "Usage: LANDXMLIMPORT (imports CgPoint survey points)", ); return Some(Task::none()); } match std::fs::read_to_string(path) { Ok(xml) => { let pts = parse_landxml_cgpoints(&xml); if pts.is_empty() { self.command_line .push_info("LANDXMLIMPORT: no survey points found."); return Some(Task::none()); } self.push_undo_snapshot(i, "LANDXMLIMPORT"); for [x, y, z] in &pts { let mut p = acadrust::entities::Point::new(); p.location = acadrust::types::Vector3::new(*x, *y, *z); self.tabs[i] .scene .add_entity_clone(acadrust::EntityType::Point(p)); } self.tabs[i].scene.bump_geometry(); self.tabs[i].dirty = true; self.command_line.push_output(&format!( "LANDXMLIMPORT: imported {} survey point(s). Use ZOOM EXTENTS to view.", pts.len() )); } Err(e) => self .command_line .push_error(&format!("LANDXMLIMPORT: cannot read \"{path}\": {e}")), } } "POINTCLOUDATTACH" | "RECAP" | "SYNCPVIEWPORTS" | "UNDERLAYLAYERS" | "UOSNAP" => { self.command_line .push_info(&format!("{cmd}: not yet implemented.")); } _ => return None, } Some(self.finish_dispatch(cmd)) } } // Scan LandXML text for survey points. Each element's text content is // "northing easting elevation"; returned as [easting, northing, elevation] so it // maps to a Point at (X=easting, Y=northing, Z=elevation). Tolerant manual scan // (no XML dependency); handles the standard text-content form. // (landxml cgpoint scan) fn parse_landxml_cgpoints(xml: &str) -> Vec<[f64; 3]> { let mut out = Vec::new(); let mut rest = xml; while let Some(open) = rest.find("". if !matches!( after.chars().next(), Some(' ') | Some('>') | Some('\t') | Some('\n') | Some('\r') ) { rest = after; continue; } let Some(gt) = after.find('>') else { break }; let body = &after[gt + 1..]; let Some(close) = body.find("") else { break; }; let text = &body[..close]; let nums: Vec = text .split_whitespace() .filter_map(|s| s.parse().ok()) .collect(); if nums.len() >= 3 { out.push([nums[1], nums[0], nums[2]]); } rest = &body[close + "".len()..]; } out }