From d1c31062839e0b4d51e13dbdfb3505432bf55daa Mon Sep 17 00:00:00 2001 From: Matus Mastena Date: Sat, 12 Sep 2026 17:58:58 +0200 Subject: [PATCH] Add the Browser panel A docked outline of the drawing: the three origin planes, the open sketch, and every solid labelled with the operation that made it, recovered through primitive_property_operation. Rows run ordinary commands, so clicking a plane starts a sketch on it and clicking a body selects it. It is a table of contents, not a feature tree. The DWG history records how each solid was built but nothing re-runs it, so an earlier feature cannot be edited to rebuild the ones after it. Presenting the list as editable history would promise exactly that, so the panel states what it is instead. Off by default and opened with BROWSER, leaving the existing dock layout alone for anyone who does not want it. --- src/app/mod.rs | 4 + src/app/update/dialog.rs | 4 + src/app/view/mod.rs | 7 ++ src/ui/dock.rs | 13 ++- src/ui/window/browser.rs | 205 +++++++++++++++++++++++++++++++++++++++ src/ui/window/mod.rs | 1 + 6 files changed, 232 insertions(+), 2 deletions(-) create mode 100644 src/ui/window/browser.rs 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;