diff --git a/src/app/mod.rs b/src/app/mod.rs index e2f103d81..9204daedb 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -733,6 +733,9 @@ pub(super) struct OpenCADStudio { pub(crate) show_block_palette: bool, /// Docked External References panel visibility (EXTERNALREFERENCES). pub(crate) show_external_references: bool, + /// Whether the Browser panel is shown. Off until BROWSER opens it, so + /// the default layout is unchanged for existing users. + pub(crate) show_browser: bool, /// General edge-stack dock layout for the side panels. pub(crate) dock: crate::ui::dock::DockState, /// Which panel is currently floated at full height (hovered, or a pinned @@ -3929,6 +3932,7 @@ impl OpenCADStudio { show_properties: true, show_block_palette: false, show_external_references: false, + show_browser: false, block_palette: Default::default(), xref_manager: Default::default(), dock: Default::default(), diff --git a/src/app/update/dialog.rs b/src/app/update/dialog.rs index e7eac4b80..7c20cb776 100644 --- a/src/app/update/dialog.rs +++ b/src/app/update/dialog.rs @@ -602,6 +602,9 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven PanelId::ExternalReferences => { self.show_external_references = false; } + PanelId::Browser => { + self.show_browser = false; + } PanelId::Properties => { self.show_properties = false; self.ribbon.set_properties(false); @@ -704,6 +707,7 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven PanelId::Properties => self.show_properties, PanelId::BlockPalette => self.show_block_palette, PanelId::ExternalReferences => self.show_external_references, + PanelId::Browser => self.show_browser, } } diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index 8ca29a3a2..7fefbba3c 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -1820,6 +1820,7 @@ bg={bg_ms:.1}ms n={view_count}" crate::ui::dock::PanelId::Properties => self.show_properties, crate::ui::dock::PanelId::BlockPalette => self.show_block_palette, crate::ui::dock::PanelId::ExternalReferences => self.show_external_references, + crate::ui::dock::PanelId::Browser => self.show_browser, } }; let edge_stack = |side: crate::app::config::DockSide| -> Option> { @@ -2881,6 +2882,12 @@ impl OpenCADStudio { tab.xref_missing, &tab.scene.document, ), + crate::ui::dock::PanelId::Browser => crate::ui::window::browser::view( + &tab.scene.document, + tab.sketch_session.as_ref().map(|session| session.name.as_str()), + width, + auto_collapse, + ), }; let divider = dock_divider(id); match side { diff --git a/src/ui/dock.rs b/src/ui/dock.rs index 475fa0023..68b49c015 100644 --- a/src/ui/dock.rs +++ b/src/ui/dock.rs @@ -42,6 +42,8 @@ pub enum PanelId { Properties, BlockPalette, ExternalReferences, + /// Outline of the drawing's origin planes, open sketch and solid bodies. + Browser, } impl PanelId { @@ -51,6 +53,7 @@ impl PanelId { PanelId::Properties => "Properties", PanelId::BlockPalette => "Block Palette", PanelId::ExternalReferences => "External References", + PanelId::Browser => "Browser", } } @@ -60,6 +63,7 @@ impl PanelId { PanelId::Properties => 250.0, PanelId::BlockPalette => 260.0, PanelId::ExternalReferences => 460.0, + PanelId::Browser => 230.0, } } @@ -117,7 +121,7 @@ impl Default for DockState { fn default() -> Self { Self { left: vec![PanelId::Properties], - right: vec![PanelId::BlockPalette], + right: vec![PanelId::Browser, PanelId::BlockPalette], panels: BTreeMap::new(), } } @@ -143,7 +147,12 @@ impl DockState { /// resize never hit a missing configuration. Also a cheap heal for configs /// written by an older version. pub fn ensure_settings(&mut self) { - for id in [PanelId::Properties, PanelId::BlockPalette, PanelId::ExternalReferences] { + for id in [ + PanelId::Properties, + PanelId::BlockPalette, + PanelId::ExternalReferences, + PanelId::Browser, + ] { self.panels.entry(id).or_insert_with(|| DockPanel::for_id(id)); } } diff --git a/src/ui/window/browser.rs b/src/ui/window/browser.rs new file mode 100644 index 000000000..c14dd4d54 --- /dev/null +++ b/src/ui/window/browser.rs @@ -0,0 +1,205 @@ +//! The Browser: a standing outline of what the drawing is built from. +//! +//! Three sections, each read straight from the document rather than from a +//! parallel model the panel would have to keep in sync: +//! +//! * **Origin planes** — the three world planes, as somewhere to start a +//! sketch when the drawing is still empty and there is no face to pick. +//! * **Sketch** — the open sketch, if there is one. +//! * **Bodies** — every `Solid3D`, labelled with the operation that made it, +//! recovered from the DWG solid history. +//! +//! What this panel deliberately is *not* is a parametric feature tree. The +//! history records how each solid was built, but nothing re-runs it, so +//! editing an earlier feature cannot rebuild the ones after it. Showing the +//! list as if it were editable history would promise exactly that. It is a +//! table of contents, and it is labelled as one. + +use crate::app::Message; +use crate::ui::dock::{DockMsg, PanelId}; +use acadrust::objects::SolidHistoryOperation; +use acadrust::{CadDocument, EntityType, Handle}; +use iced::widget::{button, column, container, mouse_area, row, scrollable, text, tooltip}; +use iced::{Background, Border, Element, Fill, Length, Theme}; + +/// The three world planes, with the argument `CREATESKETCH` takes for each. +const ORIGIN_PLANES: [(&str, &str); 3] = [("XY", "XY"), ("XZ", "XZ"), ("YZ", "YZ")]; + +/// Human name for the operation that produced a solid. `None` when the +/// drawing carries no history for it — a solid imported as plain geometry, +/// or one whose history was stripped on save. +fn operation_label(operation: &SolidHistoryOperation) -> &'static str { + match operation { + SolidHistoryOperation::Box(_) => "Box", + SolidHistoryOperation::Wedge(_) => "Wedge", + SolidHistoryOperation::Sphere(_) => "Sphere", + SolidHistoryOperation::Cone(_) => "Cone", + SolidHistoryOperation::Cylinder(_) => "Cylinder", + SolidHistoryOperation::Torus(_) => "Torus", + SolidHistoryOperation::Pyramid(_) => "Pyramid", + SolidHistoryOperation::Sweep(_) => "Sweep", + SolidHistoryOperation::Loft(_) => "Loft", + SolidHistoryOperation::Extrusion(_) => "Extrude", + SolidHistoryOperation::Revolve(_) => "Revolve", + SolidHistoryOperation::Fillet(_) => "Fillet", + SolidHistoryOperation::Chamfer(_) => "Chamfer", + _ => "Solid", + } +} + +/// Every `Solid3D` in model space, with the label to show for it. +fn bodies(document: &CadDocument) -> Vec<(Handle, String)> { + document + .entities() + .filter(|entity| matches!(entity, EntityType::Solid3D(_))) + .map(|entity| { + let handle = entity.common().handle; + let label = crate::scene::model::solid_history::primitive_property_operation( + document, handle, + ) + .as_ref() + .map_or("Solid", operation_label); + (handle, format!("{label} {:X}", handle.value())) + }) + .collect() +} + +fn section_header(label: String) -> Element<'static, Message> { + container(text(label).size(11)) + .width(Fill) + .padding([3, 6]) + .style(|theme: &Theme| container::Style { + background: Some(Background::Color(theme.palette().background.weak.color)), + ..Default::default() + }) + .into() +} + +/// A clickable row that runs `command`. +fn entry_row(label: String, command: String) -> Element<'static, Message> { + button(text(label).size(12)) + .on_press(Message::Command(command)) + .width(Fill) + .padding([3, 10]) + .style(button::subtle) + .into() +} + +/// A row that states something rather than doing something. +fn note_row(label: String) -> Element<'static, Message> { + container(text(label).size(11).style(crate::ui::style::common::muted_style)) + .width(Fill) + .padding([3, 10]) + .into() +} + +pub fn view<'a>( + document: &'a CadDocument, + open_sketch: Option<&'a str>, + width: f32, + auto_collapse: bool, +) -> Element<'a, Message> { + // ── Dock chrome (title, pin, close) — matches the other panels ──────── + let pin_icon = if auto_collapse { + crate::ui::icons::themed_primary_weak_text(crate::ui::icons::PIN, 12.0) + } else { + crate::ui::icons::themed_secondary(crate::ui::icons::PIN, 12.0) + }; + let pin = button(pin_icon) + .on_press(Message::Dock(DockMsg::AutoCollapseToggle(PanelId::Browser))) + .style(move |theme: &Theme, status| { + let mut style = button::subtle(theme, status); + if auto_collapse { + let palette = theme.palette(); + style.background = Some(Background::Color(palette.primary.weak.color)); + style.text_color = palette.primary.weak.text; + style.border.color = palette.primary.base.color; + style.border.width = 1.0; + } + style + }) + .padding([3, 5]); + let pin = tooltip(pin, text(crate::t!("Auto")).size(10), tooltip::Position::Bottom).gap(4); + + let close = button(crate::ui::icons::themed_secondary(crate::ui::icons::CLOSE, 12.0)) + .on_press(Message::Dock(DockMsg::Close(PanelId::Browser))) + .style(button::subtle) + .padding([3, 5]); + let close = tooltip(close, text(crate::t!("Close")).size(10), tooltip::Position::Bottom).gap(4); + + let title_bar = mouse_area( + container( + row![ + text(crate::t!("Browser")).size(12), + iced::widget::Space::new().width(Fill), + pin, + close, + ] + .spacing(3) + .align_y(iced::Center), + ) + .style(|theme: &Theme| container::Style { + background: Some(Background::Color(theme.palette().background.weak.color)), + ..Default::default() + }) + .width(Fill) + .padding([3, 6]), + ) + .on_press(Message::Dock(DockMsg::DockGrab(PanelId::Browser))) + .interaction(iced::mouse::Interaction::Grab); + + let mut tree = column![].spacing(1); + + tree = tree.push(section_header(crate::t!("Origin planes").into_owned())); + for (label, argument) in ORIGIN_PLANES { + tree = tree.push(entry_row( + label.to_string(), + format!("CREATESKETCH {argument}"), + )); + } + + tree = tree.push(section_header(crate::t!("Sketch").into_owned())); + match open_sketch { + Some(name) => { + tree = tree.push(entry_row(name.to_string(), "FINISHSKETCH".to_string())); + } + None => { + tree = tree.push(note_row(crate::t!("No sketch open").into_owned())); + } + } + + let bodies = bodies(document); + tree = tree.push(section_header(crate::tf!("Bodies ({})", bodies.len()).into_owned())); + if bodies.is_empty() { + tree = tree.push(note_row(crate::t!("No solids yet").into_owned())); + } else { + for (handle, label) in bodies { + // Selecting by handle is what SELECT already takes, so clicking a + // row reuses the ordinary selection path. + tree = tree.push(entry_row(label, format!("SELECT {:X}", handle.value()))); + } + } + + let body = scrollable(container(tree).padding(iced::Padding { + top: 0.0, + right: 8.0, + bottom: 6.0, + left: 0.0, + })) + .width(Fill) + .height(Fill); + + container(column![title_bar, body].spacing(6).padding(6)) + .width(Length::Fixed(width)) + .height(Fill) + .style(|theme: &Theme| container::Style { + background: Some(Background::Color(theme.palette().background.base.color)), + border: Border { + color: theme.palette().background.neutral.color, + width: 1.0, + radius: 0.0.into(), + }, + ..Default::default() + }) + .into() +} diff --git a/src/ui/window/mod.rs b/src/ui/window/mod.rs index ac3352c5c..85c39ac3c 100644 --- a/src/ui/window/mod.rs +++ b/src/ui/window/mod.rs @@ -1,5 +1,6 @@ pub mod about; pub mod block_palette; +pub mod browser; pub mod layout_manager; pub mod layer_state_manager; pub mod drawing_units;