diff --git a/src/app/commands/layerprops.rs b/src/app/commands/layerprops.rs index 464cf01fc..6a4a996f5 100644 --- a/src/app/commands/layerprops.rs +++ b/src/app/commands/layerprops.rs @@ -787,6 +787,22 @@ impl OpenCADStudio { } } + // -VIEW: the command-line form of VIEW (modules/view/view_cmd.rs). + // The command gathers the input; `-VIEW >…` carries out what it + // gathered. A whole line (`-VIEW _O _FRONT`) feeds its words to + // the command one by one, as typed. + "-VIEW" => { + use crate::command::CadCommand; + let c = crate::modules::view::view_cmd::DashViewCommand::new(self.dash_view_names(i)); + self.command_line.push_info(&c.prompt()); + self.tabs[i].active_cmd = Some(Box::new(c)); + } + cmd if cmd.starts_with("-VIEW >") => { + return Some(self.run_dash_view(&cmd["-VIEW >".len()..], i)); + } + cmd if cmd.starts_with("-VIEW ") => { + return Some(self.run_command_line(cmd)); + } "VIEW" => { use crate::command::KeywordCommand; let c = KeywordCommand::new( @@ -910,45 +926,14 @@ impl OpenCADStudio { } } } - // Standard orientation presets — snap the camera to a world - // axis view (these names take precedence over a same-named - // saved view, matching the standard orientation behaviour). - "TOP" | "FRONT" | "BACK" | "LEFT" | "RIGHT" | "BOTTOM" => { - use crate::scene::pipeline::viewcube::{ - FACE_BACK, FACE_BOTTOM, FACE_FRONT, FACE_LEFT, FACE_RIGHT, FACE_TOP, - }; - let face = match sub.as_str() { - "TOP" => FACE_TOP, - "BOTTOM" => FACE_BOTTOM, - "FRONT" => FACE_FRONT, - "BACK" => FACE_BACK, - "RIGHT" => FACE_RIGHT, - _ => FACE_LEFT, - }; - return Some(Task::done(Message::ViewCubeSnapWorld( - crate::scene::CubeRegion::Face(face), - ))); + // Standard orientation presets: the world views -VIEW sets, + // with the orthographic UCS while UCSORTHO is on. These + // names take precedence over a same-named saved view. + "TOP" | "FRONT" | "BACK" | "LEFT" | "RIGHT" | "BOTTOM" | "SWISO" | "SEISO" | "NEISO" | "NWISO" => { + return Some(self.apply_view_preset(&sub)); } - // Isometric presets snap to the matching ViewCube corner; // ISO alone is the south-west one. - "ISO" | "ISOMETRIC" | "SWISO" | "SEISO" | "NEISO" | "NWISO" => { - let (x, y) = match sub.as_str() { - "SEISO" => (1.0, -1.0), - "NEISO" => (1.0, 1.0), - "NWISO" => (-1.0, 1.0), - _ => (-1.0, -1.0), - }; - let want = glam::Vec3::new(x, y, 1.0).normalize(); - let corner = (18..26) - .map(crate::scene::CubeRegion::Corner) - .max_by(|a, b| { - a.snap_direction() - .dot(want) - .total_cmp(&b.snap_direction().dot(want)) - }) - .expect("the cube has corners"); - return Some(Task::done(Message::ViewCubeSnap(corner))); - } + "ISO" | "ISOMETRIC" => return Some(self.apply_view_preset("SWISO")), // VIEW shortcut for restore _ => { let name = sub.clone(); diff --git a/src/app/commands/mod.rs b/src/app/commands/mod.rs index c7e46b626..f78224d86 100644 --- a/src/app/commands/mod.rs +++ b/src/app/commands/mod.rs @@ -26,6 +26,7 @@ mod point_cloud; pub(crate) mod sheet_set; mod styleprops; mod view; +mod view_presets; mod xclip; mod xref_attach; @@ -925,6 +926,7 @@ inventory::submit!(crate::command::CommandRegistration { "USERI", "USERR", "VIEW", + "-VIEW", "VPORTS", "VS", "VW", diff --git a/src/app/commands/styleprops.rs b/src/app/commands/styleprops.rs index 81c0760ba..a9c37af67 100644 --- a/src/app/commands/styleprops.rs +++ b/src/app/commands/styleprops.rs @@ -1287,6 +1287,8 @@ impl OpenCADStudio { | "UOSNAP" | "FIELDDISPLAY" | "SURFACEASSOCIATIVITY" + | "UCSORTHO" + | "UCSVIEW" | "PDFIMPORTMODE" | "PDFIMPORTFILTER" | "PDFIMPORTLAYERS" @@ -1525,6 +1527,32 @@ impl OpenCADStudio { } return Some(self.finish_dispatch(cmd)); } + if matches!(name.as_str(), "UCSORTHO" | "UCSVIEW") { + use crate::modules::view::view_cmd; + let current = i16::from(if name == "UCSORTHO" { view_cmd::ucs_ortho() } else { view_cmd::ucs_view() }); + match value.as_deref().map(|v| v.trim().parse::().ok().filter(|v| (0..=1).contains(v))) { + Some(Some(mode)) => { + if current != mode { + if name == "UCSORTHO" { + view_cmd::set_ucs_ortho(mode == 1); + } else { + view_cmd::set_ucs_view(mode == 1); + } + self.save_config(); + } + } + Some(None) => { + self.command_line.push_error(crate::t!("Requires 0 or 1 only.").as_ref()); + self.command_line.push_output(&format!("Enter new value for {name} <{current}>:")); + self.pending_setvar = Some(name.clone()); + } + None => { + self.command_line.push_output(&format!("Enter new value for {name} <{current}>:")); + self.pending_setvar = Some(name.clone()); + } + } + return Some(self.finish_dispatch(cmd)); + } if name == "SURFACEASSOCIATIVITY" { use crate::scene::model::sweep_model; let current = i16::from(sweep_model::surface_associativity()); diff --git a/src/app/commands/view_presets.rs b/src/app/commands/view_presets.rs new file mode 100644 index 000000000..f5904e798 --- /dev/null +++ b/src/app/commands/view_presets.rs @@ -0,0 +1,341 @@ +//! What -VIEW carries out: the view presets (with the orthographic UCS of +//! UCSORTHO), named views saved with the UCS of UCSVIEW, and the per-view +//! settings of its sEttings option. + +use super::super::{Message, OpenCADStudio}; +use crate::command::CadCommand; +use crate::modules::view::view_cmd::{self, DashViewCommand, ViewInfo, ViewNames}; +use codec::objects::ObjectType; +use codec::types::{Handle, Vector3}; +use iced::Task; + +/// The xrecord in a view's extension dictionary holding its category and +/// layer snapshot, as the reference keeps them. +const VIEW_INFO: &str = "ADSK_XREC_VTRVIEWINFO"; + +/// The layer state a view's layer snapshot is stored under. +fn snapshot_name(view: &str) -> String { + format!("ACAD_VIEWS_{view}") +} + +impl OpenCADStudio { + /// Names -VIEW checks its input against, taken from the drawing. + pub(super) fn dash_view_names(&self, i: usize) -> ViewNames { + let document = &self.tabs[i].scene.document; + let visual_styles = crate::io::visual_styles::offered(document); + let live_sections: Vec<(String, Handle)> = document + .entities() + .filter_map(|entity| match entity { + codec::EntityType::Extended(extended) => match &extended.data { + codec::entities::ExtendedEntityData::SectionObject(section) => { + Some((section.name.clone(), extended.common.handle)) + } + _ => None, + }, + _ => None, + }) + .collect(); + let name_of = |list: &[(String, Handle)], handle: Handle| { + (!handle.is_null()) + .then(|| list.iter().find(|(_, h)| *h == handle).map(|(name, _)| name.clone())) + .flatten() + }; + let views = document + .views + .iter() + .map(|view| ViewInfo { + name: view.name.clone(), + category: (!view.handle.is_null()) + .then(|| document.xrecord(view.handle, VIEW_INFO)) + .flatten() + .and_then(|record| record.get_string(300)) + .unwrap_or_default() + .to_string(), + visual_style: name_of(&visual_styles, view.visual_style_handle), + live_section: name_of(&live_sections, view.live_section_handle), + }) + .collect(); + ViewNames { + views, + visual_styles: visual_styles.into_iter().map(|(name, _)| name).collect(), + live_sections: live_sections.into_iter().map(|(name, _)| name).collect(), + } + } + + /// Set a standard view: the world orthographic or isometric view of + /// `preset`. An orthographic one also sets its orthographic UCS while + /// UCSORTHO is on; asking for the view shown already keeps it. + pub(in crate::app) fn apply_view_preset(&mut self, preset: &str) -> Task { + use crate::scene::pipeline::viewcube::{ + FACE_BACK, FACE_BOTTOM, FACE_FRONT, FACE_LEFT, FACE_RIGHT, FACE_TOP, + }; + use crate::scene::CubeRegion; + let i = self.active_tab; + let region = match preset { + "TOP" => CubeRegion::Face(FACE_TOP), + "BOTTOM" => CubeRegion::Face(FACE_BOTTOM), + "FRONT" => CubeRegion::Face(FACE_FRONT), + "BACK" => CubeRegion::Face(FACE_BACK), + "LEFT" => CubeRegion::Face(FACE_LEFT), + "RIGHT" => CubeRegion::Face(FACE_RIGHT), + _ => { + let (x, y) = match preset { + "SEISO" => (1.0, -1.0), + "NEISO" => (1.0, 1.0), + "NWISO" => (-1.0, 1.0), + _ => (-1.0, -1.0), + }; + let want = glam::Vec3::new(x, y, 1.0).normalize(); + (18..26) + .map(CubeRegion::Corner) + .max_by(|a, b| a.snap_direction().dot(want).total_cmp(&b.snap_direction().dot(want))) + .expect("the cube has corners") + } + }; + if view_cmd::ucs_ortho() && self.tabs[i].active_block_edit.is_none() { + if let Some((name, x, y, ortho)) = view_cmd::orthographic_ucs(preset) { + // The top view's UCS is the world one. + self.tabs[i].active_ucs = (ortho != 1).then(|| { + let mut ucs = codec::tables::Ucs::new(name); + ucs.origin = Vector3::ZERO; + ucs.x_axis = Vector3::new(x[0], x[1], x[2]); + ucs.y_axis = Vector3::new(y[0], y[1], y[2]); + ucs.ortho_type = ortho; + ucs + }); + self.commit_active_ucs_change(i, "-VIEW"); + } + } + self.command_line.push_output(crate::t!("Regenerating model.").as_ref()); + self.snap_view_preset(region) + } + + /// Carry out what -VIEW gathered (`-VIEW >VERB args`). + pub(super) fn run_dash_view(&mut self, rest: &str, i: usize) -> Task { + let (verb, arg) = rest.split_once(' ').unwrap_or((rest, "")); + let mut settings_again = false; + match verb { + "END" => { + if !arg.is_empty() { + self.command_line.push_output(arg); + } + } + "PRESET" => return self.apply_view_preset(arg), + "DELETE" => { + let doomed: Vec = view_cmd::matching( + self.tabs[i].scene.document.views.iter().map(|view| view.name.as_str()), + arg, + ) + .into_iter() + .map(str::to_string) + .collect(); + if !doomed.is_empty() { + self.push_undo_snapshot(i, "-VIEW"); + for name in &doomed { + self.tabs[i].scene.document.views.remove(name); + self.tabs[i].scene.document.delete_layer_state(&snapshot_name(name)); + } + self.tabs[i].dirty = true; + } + } + "RESTORE" => { + let Some(view) = self.tabs[i].scene.document.views.get(arg).cloned() else { + return Task::none(); + }; + let before = self.tabs[i].scene.active_gaze_dir(); + self.tabs[i].scene.restore_named_view(&view); + if view.ucs_associated && self.tabs[i].active_block_edit.is_none() { + let world = view.ucs_x_axis == Vector3::new(1.0, 0.0, 0.0) + && view.ucs_y_axis == Vector3::new(0.0, 1.0, 0.0) + && view.ucs_origin == Vector3::ZERO; + self.tabs[i].active_ucs = (!world).then(|| { + let name = view_cmd::PRESETS + .iter() + .filter_map(|preset| view_cmd::orthographic_ucs(preset)) + .find(|(_, _, _, ortho)| *ortho == view.ucs_ortho_type) + .map_or("*ACTIVE*", |(name, ..)| name); + let mut ucs = codec::tables::Ucs::new(name); + ucs.origin = view.ucs_origin; + ucs.x_axis = view.ucs_x_axis; + ucs.y_axis = view.ucs_y_axis; + ucs.elevation = view.ucs_elevation; + ucs.ortho_type = view.ucs_ortho_type; + ucs + }); + self.commit_active_ucs_change(i, "-VIEW"); + } + if self.tabs[i].scene.active_gaze_dir().dot(before) < 0.9999 { + self.command_line.push_output(crate::t!("Regenerating model.").as_ref()); + } + } + "SAVE" => { + let mut view = self.tabs[i].scene.current_as_named_view(arg); + if view_cmd::ucs_view() { + self.command_line.push_output("UCSVIEW = 1 UCS will be saved with view"); + self.store_ucs_in_view(i, &mut view); + } + self.save_named_view(i, view); + } + "WINDOW" => { + let parts: Vec<&str> = arg.split('\u{1f}').collect(); + let corner = |s: &str| { + let (x, y) = s.split_once(',')?; + Some((x.trim().parse::().ok()?, y.trim().parse::().ok()?)) + }; + match (parts.first(), parts.get(1).and_then(|s| corner(s)), parts.get(2).and_then(|s| corner(s))) { + (Some(name), Some(a), Some(b)) if a.0 != b.0 && a.1 != b.1 => { + // A plan view of the window between the corners; the + // reference keeps no UCS with a window view. + let mut view = self.tabs[i].scene.current_as_named_view(name); + view.target = Vector3 { x: (a.0 + b.0) / 2.0, y: (a.1 + b.1) / 2.0, z: 0.0 }; + view.direction = Vector3 { x: 0.0, y: 0.0, z: 1.0 }; + view.width = (b.0 - a.0).abs(); + view.height = (b.1 - a.1).abs(); + self.save_named_view(i, view); + } + _ => self.command_line.push_output("Invalid window specification."), + } + } + "SETTINGS" => settings_again = true, + "UCSVIEW" => { + view_cmd::set_ucs_view(arg == "1"); + self.save_config(); + settings_again = true; + } + "SET" => { + let parts: Vec<&str> = arg.split('\u{1f}').collect(); + if let [setting, view, value] = parts[..] { + self.apply_view_setting(i, setting, view, value); + } + settings_again = true; + } + _ => {} + } + if settings_again { + let command = DashViewCommand::settings(self.dash_view_names(i)); + self.command_line.push_info(&command.prompt()); + self.tabs[i].active_cmd = Some(Box::new(command)); + } + self.finish_dispatch("-VIEW") + } + + /// Keep the current UCS with a view being saved (UCSVIEW). + fn store_ucs_in_view(&self, i: usize, view: &mut codec::tables::View) { + view.ucs_associated = true; + match &self.tabs[i].active_ucs { + Some(ucs) => { + view.ucs_origin = ucs.origin; + view.ucs_x_axis = ucs.x_axis; + view.ucs_y_axis = ucs.y_axis; + view.ucs_elevation = ucs.elevation; + view.ucs_ortho_type = ucs.ortho_type; + view.named_ucs_handle = ucs.named_ucs_handle; + } + None => { + view.ucs_origin = Vector3::ZERO; + view.ucs_x_axis = Vector3::new(1.0, 0.0, 0.0); + view.ucs_y_axis = Vector3::new(0.0, 1.0, 0.0); + view.ucs_elevation = 0.0; + view.ucs_ortho_type = 0; + } + } + } + + /// Add or replace a named view, keeping the record (and so its settings) + /// of a view saved again under the same name. + fn save_named_view(&mut self, i: usize, mut view: codec::tables::View) { + self.push_undo_snapshot(i, "-VIEW"); + let document = &mut self.tabs[i].scene.document; + match document.views.get(&view.name).cloned() { + Some(old) => { + view.name = old.name; + view.handle = old.handle; + view.visual_style_handle = old.visual_style_handle; + view.live_section_handle = old.live_section_handle; + view.background_handle = old.background_handle; + } + None => { + view.handle = document.allocate_handle(); + if view.visual_style_handle.is_null() { + view.visual_style_handle = crate::io::visual_styles::offered(document) + .into_iter() + .find(|(name, _)| name == "2D Wireframe") + .map_or(Handle::NULL, |(_, handle)| handle); + } + } + } + document.views.add_or_replace(view); + self.tabs[i].dirty = true; + } + + /// One setting of -VIEW's sEttings option on the named view. + fn apply_view_setting(&mut self, i: usize, setting: &str, name: &str, value: &str) { + self.push_undo_snapshot(i, "-VIEW"); + let document = &mut self.tabs[i].scene.document; + let Some(mut view) = document.views.get(name).cloned() else { + return; + }; + if view.handle.is_null() { + view.handle = document.allocate_handle(); + } + let find = |list: Vec<(String, Handle)>| { + list.into_iter().find(|(entry, _)| entry.eq_ignore_ascii_case(value)).map_or(Handle::NULL, |(_, h)| h) + }; + match setting { + "BACKGROUND" => view.background_handle = Handle::NULL, + "VISUALSTYLE" => view.visual_style_handle = find(crate::io::visual_styles::offered(document)), + "SECTION" => { + let sections: Vec<(String, Handle)> = document + .entities() + .filter_map(|entity| match entity { + codec::EntityType::Extended(extended) => match &extended.data { + codec::entities::ExtendedEntityData::SectionObject(section) => { + Some((section.name.clone(), extended.common.handle)) + } + _ => None, + }, + _ => None, + }) + .collect(); + view.live_section_handle = find(sections); + } + "CATEGORIZE" | "LAYER" => { + let snapshot = snapshot_name(&view.name); + let layer_name = match (setting, value) { + ("LAYER", "SAVE") => { + document.capture_layer_state(snapshot.clone(), ""); + Some(snapshot) + } + ("LAYER", _) => { + document.delete_layer_state(&snapshot); + Some(String::new()) + } + _ => None, + }; + let record = document.ensure_xrecord(view.handle, VIEW_INFO); + if let Some(ObjectType::XRecord(record)) = document.objects.get_mut(&record) { + let text = |record: &codec::objects::XRecord, code: i32| { + record.get_string(code).unwrap_or_default().to_string() + }; + let category = if setting == "CATEGORIZE" { value.to_string() } else { text(record, 300) }; + let layers = layer_name.unwrap_or_else(|| text(record, 302)); + let mut rest: Vec<_> = + record.entries.iter().filter(|entry| !matches!(entry.code, 300 | 302)).cloned().collect(); + if rest.is_empty() { + rest = vec![ + codec::objects::XRecordEntry::bool(293, false), + codec::objects::XRecordEntry::bool(294, false), + ]; + } + record.entries.clear(); + record.add_string(300, category); + record.add_string(302, layers); + record.entries.extend(rest); + } + } + _ => {} + } + document.views.add_or_replace(view); + self.tabs[i].dirty = true; + } +} diff --git a/src/app/settings.rs b/src/app/settings.rs index 4a91f665a..b62c6b072 100644 --- a/src/app/settings.rs +++ b/src/app/settings.rs @@ -419,6 +419,10 @@ pub struct UserSettings { pub field_display: bool, /// SURFACEASSOCIATIVITY: new surfaces stay associative to their sources. pub surface_associativity: bool, + /// UCSORTHO: an orthographic view preset also sets its orthographic UCS. + pub ucs_ortho: bool, + /// UCSVIEW: a saved named view keeps the current UCS. + pub ucs_view: bool, /// Nested-copy symbol handling: false inserts, true binds. pub ncopy_bind: bool, /// Last Options page; unknown saved names fall back without rejecting the config. @@ -836,6 +840,8 @@ impl Default for UserSettings { attdef_on_screen: true, field_display: true, surface_associativity: true, + ucs_ortho: true, + ucs_view: true, ncopy_bind: false, cursor_type: CursorType::Crosshair, crosshair_color: None, diff --git a/src/app/update/file.rs b/src/app/update/file.rs index d37dc2a01..86164f45b 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -794,6 +794,8 @@ impl OpenCADStudio { attdef_on_screen: crate::modules::draw::draw::attdef::session().on_screen, field_display: crate::entities::field::display(), surface_associativity: crate::scene::model::sweep_model::surface_associativity(), + ucs_ortho: crate::modules::view::view_cmd::ucs_ortho(), + ucs_view: crate::modules::view::view_cmd::ucs_view(), ncopy_bind: self.ncopy_bind, cursor_type: self.cursor_type, crosshair_color: self.crosshair_color, @@ -907,6 +909,8 @@ impl OpenCADStudio { crate::modules::draw::draw::attdef::session().on_screen = s.attdef_on_screen; crate::entities::field::set_display(s.field_display); crate::scene::model::sweep_model::set_surface_associativity(s.surface_associativity); + crate::modules::view::view_cmd::set_ucs_ortho(s.ucs_ortho); + crate::modules::view::view_cmd::set_ucs_view(s.ucs_view); self.ncopy_bind = s.ncopy_bind; self.cursor_type = s.cursor_type; self.crosshair_color = s.crosshair_color; diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index 10b270e96..2f81f6abe 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -5976,15 +5976,25 @@ properties={:.1}ms picked={}", } fn snap_view_region(&mut self, region: CubeRegion, r_ucs: glam::Mat4) -> Task { + self.snap_view_region_as(region, r_ucs, true) + } + + /// A standard view preset (-VIEW): the world view itself, kept when it + /// is shown already, with no cube message. + pub(in crate::app) fn snap_view_preset(&mut self, region: CubeRegion) -> Task { + self.snap_view_region_as(region, glam::Mat4::IDENTITY, false) + } + + fn snap_view_region_as(&mut self, region: CubeRegion, r_ucs: glam::Mat4, cube: bool) -> Task { let i = self.active_tab; self.clear_navigation_hover(i); self.tabs[i].scene.remember_current_view(); let mut region = region; - // "Already there → flip to opposite" check: compare the - // current gaze direction with the region's target gaze. + // "Already there → flip to opposite" check for a cube click: compare + // the current gaze direction with the region's target gaze. let target_dir = r_ucs.transform_vector3(region.snap_direction()); let cur_dir = self.tabs[i].scene.active_gaze_dir(); - if cur_dir.dot(target_dir) > 0.9999 { + if cube && cur_dir.dot(target_dir) > 0.9999 { region = region.opposite(); } let eye_dir = r_ucs.transform_vector3(region.snap_direction()); @@ -6014,8 +6024,10 @@ properties={:.1}ms picked={}", } } self.tabs[i].scene.camera_generation += 1; - self.command_line - .push_output(crate::tf!("View: {}", crate::t!(region.label())).as_ref()); + if cube { + self.command_line + .push_output(crate::tf!("View: {}", crate::t!(region.label())).as_ref()); + } Task::none() } diff --git a/src/command.rs b/src/command.rs index eaf478412..a03e3e77f 100644 --- a/src/command.rs +++ b/src/command.rs @@ -734,6 +734,8 @@ impl CadCommand for KeywordCommand { // Consumed inputs that keep prompting return `Some(NeedPoint)` — // `None` would hand the same text to the command a second time. None => { + // A leading underscore marks the English keyword, as in scripts. + let t = t.strip_prefix('_').unwrap_or(t); let Some((_, keyword, value_prompt)) = match_cmd_option(&self.options, t) else { // Unknown verb — keep prompting rather than dispatch garbage. return Some(CmdResult::NeedPoint); diff --git a/src/io/mod.rs b/src/io/mod.rs index 722ce3d89..c41259a22 100644 --- a/src/io/mod.rs +++ b/src/io/mod.rs @@ -24,6 +24,7 @@ pub mod step; pub mod stl; pub mod thumbnail; pub mod update_check; +pub mod visual_styles; #[cfg(target_arch = "wasm32")] pub(crate) mod web_recent; #[cfg(target_arch = "wasm32")] diff --git a/src/io/visual_styles.rs b/src/io/visual_styles.rs new file mode 100644 index 000000000..95ab105a9 --- /dev/null +++ b/src/io/visual_styles.rs @@ -0,0 +1,148 @@ +//! The visual styles a new drawing carries, as the reference's new drawing +//! has them: name, kind and settings, then each property value in file +//! order. Tokens: `L` long, `S` short, `D` real, `B0`/`B1` flag, `I` color +//! index, `R` true color, `CL`/`CB`/`CN` by layer / by block / none, `T` text; +//! a trailing `!` marks a property that is not enabled. + +use codec::objects::{ObjectType, VisualStyle, VisualStyleProperty, VisualStylePropertyValue}; +use codec::types::Color; + +/// (name, type, face lighting model, face lighting quality, face color +/// mode, face modifier, edge model, edge style, internal only, extended +/// lighting model, properties) +type Row = (&'static str, i16, i16, i16, i16, i32, i32, i32, bool, i16, &'static str); + +const STYLES: &[Row] = &[ + ("2dWireframe", 4, 0, 2, 0, 0, 1, 4, false, 3, + "L0 L2 L0 L0 D0.6 D30.0 R255,255,255 L1 L4 I7 CN L1 L1 D1.0 L0 CN D1.0 L1 L6 L2 I7 L5 L0 L0 B0 L1 D0.0 L0 B1 B1 B0 B0 B0 B0 B0 B0 B0 L50 D0.0 D1.0 L0 R0,0,0 L50 L3 R0,0,255 B0 L50 L50 L50 B0 L50 CL! D1.0! L2 Tstrokes_ogs.tif B0 D1.0 D1.0"), + ("Basic", 7, 1, 0, 1, 0, 0, 4, true, 3, + "L1 L0 L1 L0 D0.6 D30.0 R255,255,255 L0 L4 I7 CN L1 L1 D1.0 L8 I7 D1.0 L1 L6 L2 I7 L5 L0 L0 B0 L1 D0.0 L0 B0 B1 B1 B0 B0 B0 B0 B0 B0 L50 D0.0 D1.0 L0 R0,0,0 L50 L3 R0,0,255 B0 L50 L50 L50 B0 L50 CL! D1.0! L2 Tstrokes_ogs.tif B0 D1.0 D1.0"), + ("Brighten", 12, 2, 2, 0, 0, 1, 4, true, 3, + "L2 L2 L0 L0 D0.6 D30.0 R255,255,255 L1 L4 I7 CN L1 L1 D1.0 L8 I7 D1.0 L1 L6 L2 I7 L5 L0 L0 B0 L1 D50.0 L0 B0 B1 B1 B0 B0 B0 B0 B0 B0 L50 D0.0 D1.0 L0 R0,0,0 L50 L3 R0,0,255 B0 L50 L50 L50 B0 L50 CL! D1.0! L2 Tstrokes_ogs.tif B0 D1.0 D1.0"), + ("ColorChange", 16, 2, 2, 3, 0, 1, 4, true, 3, + "L2 L2 L3 L0 D0.6 D30.0 R128,128,128 L1 L4 I7 CN L1 L1 D1.0 L8 R128,128,128 D1.0 L1 L6 L2 I7 L5 L0 L0 B0 L1 D0.0 L0 B0 B1 B1 B0 B0 B0 B0 B0 B0 L50 D0.0 D1.0 L0 R0,0,0 L50 L3 R0,0,255 B0 L50 L50 L50 B0 L50 CL! D1.0! L2 Tstrokes_ogs.tif B0 D1.0 D1.0"), + ("Conceptual", 9, 3, 2, 0, 0, 2, 2, false, 3, + "L3 L2 L0 L0 D0.6 D30.0 R255,255,255 L2 L2 I7 CN L1 L1 D179.0 L8 I7 D1.0 L1 L6 L2 I7 L3 L0 L0 B0 L1 D0.0 L0 B0 B1 B1 B0 B0 B0 B0 B0 B0 L50 D0.0 D1.0 L0 R0,0,0 L50 L3 R0,0,255 B0 L50 L50 L50 B0 L50 CL! D1.0! L2 Tstrokes_ogs.tif B0 D1.0 D1.0"), + ("Dim", 11, 2, 2, 0, 0, 1, 4, true, 3, + "L2 L2 L0 L0 D0.6 D30.0 R255,255,255 L1 L4 I7 CN L1 L1 D1.0 L8 I7 D1.0 L1 L6 L2 I7 L5 L0 L0 B0 L1 D-50.0 L0 B0 B1 B1 B0 B0 B0 B0 B0 B0 L50 D0.0 D1.0 L0 R0,0,0 L50 L3 R0,0,255 B0 L50 L50 L50 B0 L50 CL! D1.0! L2 Tstrokes_ogs.tif B0 D1.0 D1.0"), + ("EdgeColorOff", 22, 2, 2, 0, 0, 1, 4, true, 3, + "L2! L2! L0! L0! D0.6! D30.0! R255,255,255! L1! L4! I7! CN! L1! L1! D1.0! L8 I7! D1.0! L1! L6! L2! I7! L5! L0! L0! B0! L1! D0.0! L0! B0! B1! B1! B0! B0! B0! B0! B0! B0! L50! D0.0! D1.0! L0! R0,0,0! L50! L3! R0,0,255! B0! L50! L50! L50! B0! L50! CL! D1.0! L2! Tstrokes_ogs.tif! B0! D1.0! D1.0!"), + ("Facepattern", 15, 2, 2, 0, 0, 1, 4, true, 3, + "L2 L2 L0 L0 D0.6 D30.0 R255,255,255 L1 L4 I7 CN L1 L1 D1.0 L8 I7 D1.0 L1 L6 L2 I7 L5 L0 L0 B0 L1 D0.0 L0 B0 B1 B1 B0 B0 B0 B0 B0 B0 L50 D0.0 D1.0 L0 R0,0,0 L50 L3 R0,0,255 B0 L50 L50 L50 B0 L50 CL! D1.0! L2 Tstrokes_ogs.tif B0 D1.0 D1.0"), + ("Flat", 0, 2, 1, 1, 2, 0, 0, true, 3, + "L2 L1 L1 L2 D0.6 D30.0 R255,255,255 L0 L0 I7 CN L1 L1 D1.0 L8 I7 D1.0 L1 L6 L2 I7 L5 L0 L0 B0 L13 D0.0 L0 B0 B1 B1 B0 B0 B0 B0 B0 B0 L50 D0.0 D1.0 L0 R0,0,0 L50 L3 R0,0,255 B0 L50 L50 L50 B0 L50 CL! D1.0! L2 Tstrokes_ogs.tif B0 D1.0 D1.0"), + ("FlatWithEdges", 1, 2, 1, 1, 2, 1, 0, true, 3, + "L2 L1 L1 L2 D0.6 D30.0 R255,255,255 L1 L0 I7 CN L1 L1 D1.0 L0 CN D1.0 L1 L6 L2 I7 L5 L0 L0 B0 L13 D0.0 L0 B0 B1 B1 B0 B0 B0 B0 B0 B0 L50 D0.0 D1.0 L0 R0,0,0 L50 L3 R0,0,255 B0 L50 L50 L50 B0 L50 CL! D1.0! L2 Tstrokes_ogs.tif B0 D1.0 D1.0"), + ("Gouraud", 2, 2, 2, 1, 2, 0, 0, true, 3, + "L2 L2 L1 L2 D0.6 D30.0 R255,255,255 L0 L0 I7 CN L1 L1 D1.0 L0 I7 D1.0 L1 L6 L2 I7 L5 L0 L0 B0 L13 D0.0 L0 B0 B1 B1 B0 B0 B0 B0 B0 B0 L50 D0.0 D1.0 L0 R0,0,0 L50 L3 R0,0,255 B0 L50 L50 L50 B0 L50 CL! D1.0! L2 Tstrokes_ogs.tif B0 D1.0 D1.0"), + ("GouraudWithEdges", 3, 2, 2, 1, 2, 1, 0, true, 3, + "L2 L2 L1 L2 D0.6 D30.0 R255,255,255 L1 L0 I7 CN L1 L1 D1.0 L0 CN D1.0 L1 L6 L2 I7 L5 L0 L0 B0 L13 D0.0 L0 B0 B1 B1 B0 B0 B0 B0 B0 B0 L50 D0.0 D1.0 L0 R0,0,0 L50 L3 R0,0,255 B0 L50 L50 L50 B0 L50 CL! D1.0! L2 Tstrokes_ogs.tif B0 D1.0 D1.0"), + ("Hidden", 6, 1, 2, 2, 0, 2, 2, false, 3, + "L1 L2 L2 L0 D0.6 D30.0 R255,255,255 L2 L2 I7 CN L2 L1 D40.0 L0 CN D1.0 L1 L6 L2 I7 L3 L0 L0 B0 L1 D0.0 L0 B0 B1 B1 B0 B0 B0 B0 B0 B0 L50 D0.0 D1.0 L0 R0,0,0 L50 L3 R0,0,255 B0 L50 L50 L50 B0 L50 CL! D1.0! L2 Tstrokes_ogs.tif B0 D1.0 D1.0"), + ("JitterOff", 20, 2, 2, 0, 0, 1, 4, true, 3, + "L2! L2! L0! L0! D0.6! D30.0! R255,255,255! L1! L4! I7! CN! L1! L1! D1.0! L10 I7! D1.0! L1! L6! L2! I7! L5! L0! L0! B0! L1! D0.0! L0! B0! B1! B1! B0! B0! B0! B0! B0! B0! L50! D0.0! D1.0! L0! R0,0,0! L50! L3! R0,0,255! B0! L50! L50! L50! B0! L50! CL! D1.0! L2! Tstrokes_ogs.tif! B0! D1.0! D1.0!"), + ("Linepattern", 14, 2, 2, 0, 0, 1, 4, true, 3, + "L2 L2 L0 L0 D0.6 D30.0 R255,255,255 L1 L4 I7 CN L7 L7 D1.0 L8 I7 D1.0 L1 L6 L2 I7 L5 L0 L0 B0 L1 D0.0 L0 B0 B1 B1 B0 B0 B0 B0 B0 B0 L50 D0.0 D1.0 L0 R0,0,0 L50 L3 R0,0,255 B0 L50 L50 L50 B0 L50 CL! D1.0! L2 Tstrokes_ogs.tif B0 D1.0 D1.0"), + ("OverhangOff", 21, 2, 2, 0, 0, 1, 4, true, 3, + "L2! L2! L0! L0! D0.6! D30.0! R255,255,255! L1! L4! I7! CN! L1! L1! D1.0! L9 I7! D1.0! L1! L6! L2! I7! L5! L0! L0! B0! L1! D0.0! L0! B0! B1! B1! B0! B0! B0! B0! B0! B0! L50! D0.0! D1.0! L0! R0,0,0! L50! L3! R0,0,255! B0! L50! L50! L50! B0! L50! CL! D1.0! L2! Tstrokes_ogs.tif! B0! D1.0! D1.0!"), + ("Realistic", 8, 2, 3, 0, 2, 0, 0, false, 3, + "L2 L3 L0 L2 D0.6 D30.0 R255,255,255 L0 L0 I7 CN L1 L1 D1.0 L8 CN D1.0 L1 L6 L2 I7 L3 L0 L0 B0 L13 D0.0 L0 B0 B1 B1 B0 B0 B0 B0 B0 B0 L50 D0.0 D1.0 L0 R0,0,0 L50 L3 R0,0,255 B0 L50 L50 L50 B0 L50 CL! D1.0! L2 Tstrokes_ogs.tif B0 D1.0 D1.0"), + ("Shaded", 27, 2, 2, 1, 2, 0, 4, false, 3, + "L2 L2 L1 L2 D0.6 D30.0 R255,255,255 L0 L4 I7 CN L1 L1 D1.0 L8 CN D1.0 L1 L6 L2 R120,120,120 L3 L0 L0 B0 L5 D0.0 L0 B0 B1 B1 B0 B0 B0 B0 B0 B0 L50 D0.0 D1.0 L0 R0,0,0 L50 L3 R0,0,255 B0 L50 L50 L50 B0 L50 CL! D1.0! L2 Tstrokes_ogs.tif B0 D1.0 D1.0"), + ("Shaded with edges", 26, 2, 2, 1, 2, 1, 2, false, 3, + "L2 L2 L1 L2 D0.6 D30.0 R255,255,255 L1 L2 I7 CN L2 L1 D1.0 L8 CN D1.0 L1 L6 L2 I7 L3 L0 L0 B0 L5 D0.0 L0 B0 B1 B1 B0 B0 B0 B0 B0 B0 L50 D0.0 D1.0 L0 R0,0,0 L50 L3 R0,0,255 B0 L50 L50 L50 B0 L50 CL! D1.0! L2 Tstrokes_ogs.tif B0 D1.0 D1.0"), + ("Shades of Gray", 23, 2, 2, 3, 0, 2, 2, false, 3, + "L2 L2 L3 L0 D0.6 D30.0 R255,255,255 L2 L2 I7 I7 L1 L1 D40.0 L8 I7 D1.0 L1 L6 L2 I7 L3 L0 L0 B0 L1 D0.0 L0 B0 B1 B1 B0 B0 B0 B0 B0 B0 L50 D0.0 D1.0 L0 R0,0,0 L50 L3 R0,0,255 B0 L50 L50 L50 B0 L50 CL! D1.0! L2 Tstrokes_ogs.tif B0 D1.0 D1.0"), + ("Sketchy", 24, 1, 2, 2, 0, 2, 2, false, 3, + "L1 L2 L2 L0 D0.6 D30.0 R255,255,255 L2 L2 I7 I7 L1 L1 D40.0 L11 I7 D1.0 L1 L6 L2 I7 L6 L0 L0 B0 L1 D0.0 L0 B0 B1 B1 B0 B0 B0 B0 B0 B0 L50 D0.0 D1.0 L0 R0,0,0 L50 L3 R0,0,255 B0 L50 L50 L50 B0 L50 CL! D1.0! L2 Tstrokes_ogs.tif B0 D1.0 D1.0"), + ("Thicken", 13, 2, 2, 0, 0, 1, 4, true, 3, + "L2 L2 L0 L0 D0.6 D30.0 R255,255,255 L1 L4 I7 CN L1 L1 D1.0 L12 I7 D1.0 L1 L6 L2 I7 L5 L0 L0 B0 L1 D0.0 L0 B0 B1 B1 B0 B0 B0 B0 B0 B0 L50 D0.0 D1.0 L0 R0,0,0 L50 L3 R0,0,255 B0 L50 L50 L50 B0 L50 CL! D1.0! L2 Tstrokes_ogs.tif B0 D1.0 D1.0"), + ("Wireframe", 5, 0, 2, 0, 0, 1, 4, false, 3, + "L0 L2 L0 L0 D0.6 D30.0 R255,255,255 L1 L4 I7 CN L1 L1 D1.0 L0 CN D1.0 L1 L6 L2 I7 L3 L0 L0 B0 L1 D0.0 L0 B0 B1 B1 B0 B0 B0 B0 B0 B0 L50 D0.0 D1.0 L0 R0,0,0 L50 L3 R0,0,255 B0 L50 L50 L50 B0 L50 CL! D1.0! L2 Tstrokes_ogs.tif B0 D1.0 D1.0"), + ("X-Ray", 25, 2, 2, 1, 1, 1, 0, false, 3, + "L2 L2 L1 L1 D0.5 D30.0 R255,255,255 L1 L0 I7 CN L1 L1 D1.0 L8 I7 D1.0 L1 L6 L2 I7 L3 L0 L0 B0 L13 D0.0 L0 B0 B1 B1 B0 B0 B0 B0 B0 B0 L50 D0.0 D1.0 L0 R0,0,0 L50 L3 R0,0,255 B0 L50 L50 L50 B0 L50 CL! D1.0! L2 Tstrokes_ogs.tif B0 D1.0 D1.0"), +]; + +fn property(token: &str) -> Option { + let (token, enabled) = match token.strip_suffix('!') { + Some(token) => (token, 0), + None => (token, 1), + }; + let value = match token.split_at(1) { + ("L", v) => VisualStylePropertyValue::Long(v.parse().ok()?), + ("S", v) => VisualStylePropertyValue::Short(v.parse().ok()?), + ("D", v) => VisualStylePropertyValue::Double(v.parse().ok()?), + ("B", v) => VisualStylePropertyValue::Bool(v == "1"), + ("I", v) => VisualStylePropertyValue::Color(Color::Index(v.parse().ok()?)), + ("R", v) => { + let mut parts = v.split(',').map(|part| part.parse::().ok()); + let (r, g, b) = (parts.next()??, parts.next()??, parts.next()??); + VisualStylePropertyValue::Color(Color::Rgb { r, g, b }) + } + ("C", "L") => VisualStylePropertyValue::Color(Color::ByLayer), + ("C", "B") => VisualStylePropertyValue::Color(Color::ByBlock), + ("C", "N") => VisualStylePropertyValue::Color(Color::None), + ("T", v) => VisualStylePropertyValue::Text(v.to_string()), + _ => return None, + }; + Some(VisualStyleProperty { value, enabled }) +} + +/// Adds the reference's visual styles to the drawing's ACAD_VISUALSTYLE +/// dictionary, keeping a style of the same name that is there already. +pub fn populate_document(document: &mut codec::CadDocument) { + let dictionary = document.header.acad_visualstyle_dict_handle; + if dictionary.is_null() { + return; + } + for &(name, style_type, flm, flq, fcm, fm, em, es, internal, elm, props) in STYLES { + let present = match document.objects.get(&dictionary) { + Some(ObjectType::Dictionary(entries)) => entries.get(name).is_some(), + _ => return, + }; + if present { + continue; + } + let Some(properties) = props.split(' ').map(property).collect::>>() else { continue }; + let handle = document.allocate_handle(); + let mut style = VisualStyle::new(); + style.handle = handle; + style.owner = dictionary; + style.reactors = vec![dictionary]; + style.description = name.to_string(); + style.style_type = style_type; + style.face_lighting_model = flm; + style.face_lighting_quality = flq; + style.face_color_mode = fcm; + style.face_modifier = fm; + style.edge_model = em; + style.edge_style = es; + style.internal_use_only = internal; + style.extended_lighting_model = elm; + style.properties = properties; + document.objects.insert(handle, ObjectType::VisualStyle(style)); + if let Some(ObjectType::Dictionary(entries)) = document.objects.get_mut(&dictionary) { + entries.add_entry(name, handle); + } + } +} + +/// The visual styles offered by name (not the internal ones), as the +/// reference lists them: the 2D wireframe style shows as "2D Wireframe". +pub fn offered(document: &codec::CadDocument) -> Vec<(String, codec::types::Handle)> { + let Some(ObjectType::Dictionary(entries)) = document.objects.get(&document.header.acad_visualstyle_dict_handle) else { + return Vec::new(); + }; + entries + .entries + .iter() + .filter(|(_, handle)| match document.objects.get(handle) { + Some(ObjectType::VisualStyle(style)) => !style.internal_use_only, + _ => false, + }) + .map(|(name, handle)| { + let shown = if name.eq_ignore_ascii_case("2dWireframe") { "2D Wireframe".to_string() } else { name.clone() }; + (shown, *handle) + }) + .collect() +} diff --git a/src/modules/view/mod.rs b/src/modules/view/mod.rs index a21468791..39387f4fa 100644 --- a/src/modules/view/mod.rs +++ b/src/modules/view/mod.rs @@ -17,6 +17,7 @@ mod tile_horiz; mod tile_vert; mod tool_palettes; pub mod ucs_cmd; +pub mod view_cmd; mod ucs_icon; mod view_front; mod view_iso; diff --git a/src/modules/view/view_cmd.rs b/src/modules/view/view_cmd.rs new file mode 100644 index 000000000..c29d9164b --- /dev/null +++ b/src/modules/view/view_cmd.rs @@ -0,0 +1,527 @@ +//! -VIEW: the command-line form of VIEW. +//! +//! The prompts follow the reference command line word for word. The command +//! only gathers input and checks the names it is given against the drawing +//! (a snapshot taken when it starts); whatever changes the drawing or the +//! camera is handed to the inline `-VIEW >…` handler through +//! [`CmdResult::Dispatch`], which re-enters the settings loop where the +//! reference does. + +use crate::command::{CadCommand, CmdResult}; +use glam::DVec3; +use std::sync::atomic::{AtomicBool, Ordering}; + +static UCS_ORTHO: AtomicBool = AtomicBool::new(true); +static UCS_VIEW: AtomicBool = AtomicBool::new(true); + +/// UCSORTHO: an orthographic view preset also sets the matching orthographic UCS. +pub fn ucs_ortho() -> bool { + UCS_ORTHO.load(Ordering::Relaxed) +} + +pub fn set_ucs_ortho(on: bool) { + UCS_ORTHO.store(on, Ordering::Relaxed); +} + +/// UCSVIEW: a saved named view keeps the current UCS. +pub fn ucs_view() -> bool { + UCS_VIEW.load(Ordering::Relaxed) +} + +pub fn set_ucs_view(on: bool) { + UCS_VIEW.store(on, Ordering::Relaxed); +} + +/// The standard orthographic and isometric view presets. +pub const PRESETS: [&str; 10] = + ["TOP", "BOTTOM", "FRONT", "BACK", "LEFT", "RIGHT", "SWISO", "SEISO", "NEISO", "NWISO"]; + +/// The orthographic UCS an orthographic preset sets while UCSORTHO is on: +/// its name, X and Y axes and ORTHOGRAPHIC type, as the reference sets them. +pub fn orthographic_ucs(preset: &str) -> Option<(&'static str, [f64; 3], [f64; 3], i16)> { + Some(match preset { + "TOP" => ("*TOP*", [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], 1), + "BOTTOM" => ("*BOTTOM*", [-1.0, 0.0, 0.0], [0.0, 1.0, 0.0], 2), + "FRONT" => ("*FRONT*", [1.0, 0.0, 0.0], [0.0, 0.0, 1.0], 3), + "BACK" => ("*BACK*", [-1.0, 0.0, 0.0], [0.0, 0.0, 1.0], 4), + "LEFT" => ("*LEFT*", [0.0, -1.0, 0.0], [0.0, 0.0, 1.0], 5), + "RIGHT" => ("*RIGHT*", [0.0, 1.0, 0.0], [0.0, 0.0, 1.0], 6), + _ => return None, + }) +} + +/// One named view, as -VIEW's settings need to know it. +#[derive(Clone, Default)] +pub struct ViewInfo { + pub name: String, + pub category: String, + pub visual_style: Option, + pub live_section: Option, +} + +/// What -VIEW checks typed names against. +#[derive(Clone, Default)] +pub struct ViewNames { + pub views: Vec, + pub visual_styles: Vec, + pub live_sections: Vec, +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Setting { + Background, + Categorize, + LayerSnapshot, + LiveSection, + VisualStyle, +} + +impl Setting { + /// The word the inline handler takes. + pub fn verb(self) -> &'static str { + match self { + Setting::Background => "BACKGROUND", + Setting::Categorize => "CATEGORIZE", + Setting::LayerSnapshot => "LAYER", + Setting::LiveSection => "SECTION", + Setting::VisualStyle => "VISUALSTYLE", + } + } + + fn view_prompt(self) -> &'static str { + match self { + Setting::Background => "Enter view name to edit Background or [?]:", + Setting::Categorize => "Enter view name to Categorize or [?]:", + Setting::LayerSnapshot => "Enter view name to edit Layer snapshot or [?]:", + Setting::LiveSection => "Enter view name to edit live section or [?]:", + Setting::VisualStyle => "Enter view name to edit visual style or [?]:", + } + } +} + +#[derive(Clone, PartialEq, Debug)] +enum Step { + Main, + Orthographic, + List, + Delete, + Restore, + Save, + WindowName, + WindowFirst(String), + WindowSecond(String, DVec3), + Settings, + SettingView(Setting), + SettingViewList(Setting), + SettingValue(Setting, String), + SettingValueList(Setting, String), + Ucs, +} + +pub struct DashViewCommand { + names: ViewNames, + step: Step, +} + +/// Typed `input` matched against the options of a prompt. An option's +/// keyword is its word holding capitals (`live Section` → `Section`); the +/// capitals alone are its abbreviation (`sEttings` → `E`), and any start of +/// the keyword that reaches its last capital is taken too (`SE`, `SETTINGS`). +/// A leading `_` is the international form scripts use. +fn keyword(input: &str, options: &[&'static str]) -> Option<&'static str> { + let input = input.trim().trim_start_matches('_').to_ascii_uppercase(); + if input.is_empty() { + return None; + } + options.iter().copied().find(|option| { + if *option == "?" { + return input == "?"; + } + let word = option + .split(' ') + .find(|word| word.chars().any(|c| c.is_ascii_uppercase())) + .unwrap_or(option); + let capitals: String = word.chars().filter(char::is_ascii_uppercase).collect(); + let reach = word.rfind(|c: char| c.is_ascii_uppercase()).map_or(1, |at| at + 1); + input == capitals || (word.to_ascii_uppercase().starts_with(&input) && input.len() >= reach) + }) +} + +/// The names in `names` that one of the comma-separated wildcard patterns matches. +pub fn matching<'a>(names: impl IntoIterator, patterns: &str) -> Vec<&'a str> { + let patterns: Vec<&str> = patterns.split(',').map(str::trim).filter(|p| !p.is_empty()).collect(); + names + .into_iter() + .filter(|name| patterns.iter().any(|pattern| crate::io::xref_model::wildcard_match(name, pattern))) + .collect() +} + +/// The reference's listing of saved views: `"NAME"` padded to 35 columns +/// followed by the space letter. +pub fn view_listing(views: &[(String, bool)]) -> String { + let mut out = String::from("Saved views:"); + if views.is_empty() { + out.push_str("\n No matching views found."); + return out; + } + out.push_str("\nView name Space"); + for (name, paper) in views { + out.push_str(&format!("\n{:<35}{}", format!("\"{name}\""), if *paper { "P" } else { "M" })); + } + out +} + +impl DashViewCommand { + pub fn new(names: ViewNames) -> Self { + Self { names, step: Step::Main } + } + + /// The command back at its settings prompt, after a setting was applied. + pub fn settings(names: ViewNames) -> Self { + Self { names, step: Step::Settings } + } + + fn view(&self, name: &str) -> Option<&ViewInfo> { + self.names.views.iter().find(|view| view.name.eq_ignore_ascii_case(name)) + } + + fn end(message: &str) -> CmdResult { + CmdResult::Dispatch(format!("-VIEW >END {message}")) + } + + fn list_views(&self, pattern: &str) -> String { + let pattern = if pattern.trim().is_empty() { "*" } else { pattern }; + let names: Vec<&str> = self.names.views.iter().map(|view| view.name.as_str()).collect(); + let found: Vec<(String, bool)> = + matching(names, pattern).into_iter().map(|name| (name.to_string(), false)).collect(); + view_listing(&found) + } + + fn value_prompt(&self, setting: Setting, view: &str) -> String { + let info = self.view(view).cloned().unwrap_or_default(); + match setting { + Setting::Background => "Specify background type [Color/Gradient/Image/None]:".into(), + Setting::Categorize => { + format!("Enter category name or * for none, or [?]: <\"{}\">", info.category) + } + Setting::LayerSnapshot => "Enter an option [Save/Delete] :".into(), + Setting::LiveSection => format!( + "Enter live section name or * for none, or [?]: <{}>:", + info.live_section.as_deref().unwrap_or("*") + ), + Setting::VisualStyle => format!( + "Enter visual style name or * for none, or [?]: <{}>:", + info.visual_style.as_deref().unwrap_or("2D Wireframe") + ), + } + } + + /// Hand a setting to the inline handler, which applies it and starts the + /// settings prompt again. + fn apply(setting: Setting, view: &str, value: &str) -> CmdResult { + CmdResult::Dispatch(format!("-VIEW >SET {}\u{1f}{view}\u{1f}{value}", setting.verb())) + } + + fn on_setting_value(&mut self, setting: Setting, view: String, text: &str) -> CmdResult { + let t = text.trim(); + match setting { + Setting::Background => match keyword(t, &["Color", "Gradient", "Image", "None"]) { + // The other background types are chosen in their own dialogs. + Some("None") => Self::apply(setting, &view, "NONE"), + Some(_) => CmdResult::Dispatch("-VIEW >SETTINGS".into()), + None => { + self.step = Step::SettingValue(setting, view); + CmdResult::ReportError("Invalid option keyword.".into()) + } + }, + Setting::LayerSnapshot => match keyword(t, &["Save", "Delete"]) { + Some("Save") => Self::apply(setting, &view, "SAVE"), + Some(_) => Self::apply(setting, &view, "DELETE"), + None => { + self.step = Step::SettingValue(setting, view); + CmdResult::ReportError("Invalid option keyword.".into()) + } + }, + Setting::Categorize => { + if t == "?" { + self.step = Step::SettingValueList(setting, view); + return CmdResult::NeedPoint; + } + let value = if t == "*" { "" } else { t }; + Self::apply(setting, &view, value) + } + Setting::LiveSection | Setting::VisualStyle => { + if t == "?" { + self.step = Step::SettingValueList(setting, view); + return CmdResult::NeedPoint; + } + if t == "*" { + return Self::apply(setting, &view, ""); + } + let known = if setting == Setting::LiveSection { + &self.names.live_sections + } else { + &self.names.visual_styles + }; + match known.iter().find(|name| name.eq_ignore_ascii_case(t)) { + Some(name) => { + let name = name.clone(); + Self::apply(setting, &view, &name) + } + None => { + self.step = Step::SettingValue(setting, view); + CmdResult::ReportError( + if setting == Setting::LiveSection { + "Invalid live section name." + } else { + "Invalid visual style name." + } + .into(), + ) + } + } + } + } + } + + fn value_listing(&self, setting: Setting, pattern: &str) -> String { + let pattern = if pattern.trim().is_empty() { "*" } else { pattern }; + match setting { + Setting::Categorize => { + let mut categories: Vec<&str> = self + .names + .views + .iter() + .map(|view| view.category.as_str()) + .filter(|category| !category.is_empty()) + .collect(); + categories.sort_unstable_by_key(|name| name.to_ascii_uppercase()); + categories.dedup_by(|a, b| a.eq_ignore_ascii_case(b)); + let found = matching(categories, pattern); + let mut out = String::from("Saved categories:"); + if !found.is_empty() { + out.push_str("\nCategory name"); + for name in found { + out.push('\n'); + out.push_str(name); + } + } + out + } + Setting::VisualStyle | Setting::LiveSection => { + let known = if setting == Setting::VisualStyle { + &self.names.visual_styles + } else { + &self.names.live_sections + }; + let mut found = matching(known.iter().map(String::as_str), pattern); + found.sort_unstable_by_key(|name| name.to_ascii_uppercase()); + if found.is_empty() { + if setting == Setting::VisualStyle { + " No matching visual styles found.".into() + } else { + " No matching live sections found.".into() + } + } else { + found.join("\n") + } + } + _ => String::new(), + } + } +} + +impl CadCommand for DashViewCommand { + fn name(&self) -> &'static str { + "-VIEW" + } + + fn prompt(&self) -> String { + match &self.step { + Step::Main => "Enter an option [?/Delete/Orthographic/Restore/Save/sEttings/Window]:".into(), + Step::Orthographic => "Enter an option [Top/Bottom/Front/BAck/Left/Right]:".into(), + Step::List | Step::SettingViewList(_) => "Enter view name(s) to list <*>:".into(), + Step::Delete => "Enter view name(s) to delete:".into(), + Step::Restore => "Enter view name to restore:".into(), + Step::Save | Step::WindowName => "Enter view name to save:".into(), + Step::WindowFirst(_) => "Specify first corner:".into(), + Step::WindowSecond(..) => "Specify opposite corner:".into(), + Step::Settings => { + "Enter an option [Background/Categorize/Layer snapshot/live Section/Ucs/Visual style]:".into() + } + Step::SettingView(setting) => setting.view_prompt().into(), + Step::SettingValue(setting, view) => self.value_prompt(*setting, view), + Step::SettingValueList(Setting::Categorize, _) => "Enter category name(s) to list <*>:".into(), + Step::SettingValueList(Setting::LiveSection, _) => "Enter live section name(s) to list <*>:".into(), + Step::SettingValueList(_, _) => "Enter visual style name(s) to list <*>:".into(), + Step::Ucs => format!( + "Save current UCS with named views?[Yes/No]<{}>:", + if ucs_view() { "Yes" } else { "No" } + ), + } + } + + fn wants_text_input(&self) -> bool { + true + } + + fn on_text_input(&mut self, text: &str) -> Option { + let t = text.trim(); + // The window corners take typed coordinates as points. + if matches!(self.step, Step::WindowFirst(_) | Step::WindowSecond(..)) + && crate::app::helpers::parse_coord(t).is_some() + { + return None; + } + if t.is_empty() { + return Some(self.on_enter()); + } + let step = std::mem::replace(&mut self.step, Step::Main); + Some(match step { + Step::Main => { + let word = t.trim_start_matches('_').to_ascii_uppercase(); + if let Some(preset) = PRESETS.iter().find(|preset| **preset == word) { + return Some(CmdResult::Dispatch(format!("-VIEW >PRESET {preset}"))); + } + match keyword(t, &["?", "Delete", "Orthographic", "Restore", "Save", "sEttings", "Window"]) { + Some("?") => self.step = Step::List, + Some("Delete") => self.step = Step::Delete, + Some("Orthographic") => self.step = Step::Orthographic, + Some("Restore") => self.step = Step::Restore, + Some("Save") => self.step = Step::Save, + Some("sEttings") => self.step = Step::Settings, + Some(_) => self.step = Step::WindowName, + None => return Some(CmdResult::ReportError("Invalid option keyword.".into())), + } + CmdResult::NeedPoint + } + Step::Orthographic => { + match keyword(t, &["Top", "Bottom", "Front", "BAck", "Left", "Right"]) { + Some(face) => CmdResult::Dispatch(format!("-VIEW >PRESET {}", face.to_ascii_uppercase())), + None => { + self.step = Step::Orthographic; + CmdResult::ReportError("Invalid option keyword.".into()) + } + } + } + Step::List => CmdResult::Measurement(self.list_views(t)), + Step::Delete => { + let names: Vec<&str> = self.names.views.iter().map(|view| view.name.as_str()).collect(); + if matching(names, t).is_empty() { + Self::end("No matching view names found.") + } else { + CmdResult::Dispatch(format!("-VIEW >DELETE {t}")) + } + } + Step::Restore => match self.view(t) { + Some(view) => CmdResult::Dispatch(format!("-VIEW >RESTORE {}", view.name)), + None => Self::end(&format!("Cannot find view \"{t}\".")), + }, + Step::Save => CmdResult::Dispatch(format!("-VIEW >SAVE {t}")), + Step::WindowName => { + self.step = Step::WindowFirst(t.to_string()); + CmdResult::NeedPoint + } + Step::WindowFirst(_) | Step::WindowSecond(..) => Self::end("Invalid window specification."), + Step::Settings => { + match keyword( + t, + &["Background", "Categorize", "Layer snapshot", "live Section", "Ucs", "Visual style"], + ) { + Some("Background") => self.step = Step::SettingView(Setting::Background), + Some("Categorize") => self.step = Step::SettingView(Setting::Categorize), + Some("Layer snapshot") => self.step = Step::SettingView(Setting::LayerSnapshot), + Some("live Section") => self.step = Step::SettingView(Setting::LiveSection), + Some("Ucs") => self.step = Step::Ucs, + Some(_) => self.step = Step::SettingView(Setting::VisualStyle), + None => { + self.step = Step::Settings; + return Some(CmdResult::ReportError("Invalid option keyword.".into())); + } + } + CmdResult::NeedPoint + } + Step::SettingView(setting) => { + if t == "?" { + self.step = Step::SettingViewList(setting); + return Some(CmdResult::NeedPoint); + } + match self.view(t).map(|view| view.name.clone()) { + Some(view) => { + self.step = Step::SettingValue(setting, view); + CmdResult::NeedPoint + } + None => { + self.step = Step::SettingView(setting); + CmdResult::ReportError("No matching view names found.".into()) + } + } + } + Step::SettingViewList(setting) => { + self.step = Step::SettingView(setting); + CmdResult::ReportMeasurement(self.list_views(t)) + } + Step::SettingValue(setting, view) => self.on_setting_value(setting, view, t), + Step::SettingValueList(setting, view) => { + let listing = self.value_listing(setting, t); + self.step = Step::SettingValue(setting, view); + CmdResult::ReportMeasurement(listing) + } + Step::Ucs => match keyword(t, &["Yes", "No"]) { + Some(answer) => CmdResult::Dispatch(format!("-VIEW >UCSVIEW {}", u8::from(answer == "Yes"))), + None => { + self.step = Step::Ucs; + CmdResult::ReportError("Invalid option keyword.".into()) + } + }, + }) + } + + fn on_point(&mut self, pt: DVec3) -> CmdResult { + match std::mem::replace(&mut self.step, Step::Main) { + Step::WindowFirst(name) => { + self.step = Step::WindowSecond(name, pt); + CmdResult::NeedPoint + } + Step::WindowSecond(name, first) => CmdResult::Dispatch(format!( + "-VIEW >WINDOW {}\u{1f}{},{}\u{1f}{},{}", + name, first.x, first.y, pt.x, pt.y + )), + step => { + self.step = step; + CmdResult::NeedPoint + } + } + } + + fn on_enter(&mut self) -> CmdResult { + match std::mem::replace(&mut self.step, Step::Main) { + Step::Main | Step::Delete | Step::Restore | Step::Save | Step::WindowName => { + CmdResult::Dispatch("-VIEW >END".into()) + } + Step::Orthographic => CmdResult::Dispatch("-VIEW >PRESET TOP".into()), + Step::List => CmdResult::Measurement(self.list_views("*")), + Step::WindowFirst(_) | Step::WindowSecond(..) => Self::end("Invalid window specification."), + // Enter at the settings prompt goes back to the main prompt. + Step::Settings => CmdResult::NeedPoint, + Step::SettingView(_) | Step::Ucs => { + self.step = Step::Settings; + CmdResult::NeedPoint + } + Step::SettingViewList(setting) => { + self.step = Step::SettingView(setting); + CmdResult::ReportMeasurement(self.list_views("*")) + } + // Enter keeps the value shown as the default. + Step::SettingValue(..) => { + self.step = Step::Settings; + CmdResult::NeedPoint + } + Step::SettingValueList(setting, view) => { + let listing = self.value_listing(setting, "*"); + self.step = Step::SettingValue(setting, view); + CmdResult::ReportMeasurement(listing) + } + } + } +} diff --git a/src/scene/entity.rs b/src/scene/entity.rs index f34b33826..c1419b4e6 100644 --- a/src/scene/entity.rs +++ b/src/scene/entity.rs @@ -3040,6 +3040,8 @@ impl Scene { /// standard linetypes and a compatible page setup on every paper layout. pub fn populate_new_drawing_defaults(&mut self) { crate::io::linetypes::populate_document(&mut self.document); + // The visual styles of the reference's new drawing. + crate::io::visual_styles::populate_document(&mut self.document); // A new drawing is metric, as the reference's new drawing: metre // insertion units, metric measurement, and the ISO-25 dimension style // current. Standard stays available for imperial work.