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.
This commit is contained in:
Matus Mastena committed 2026-09-17 16:54:11 +02:00
1 parent 1aa299e204
commit d1c3106283
6 files changed
+232 -2

No files matched your search

+4
View File
@@ -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(),
+4
View File
@@ -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,
}
}
+7
View File
@@ -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<Element<'_, Message>> {
@@ -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 {
+11 -2
View File
@@ -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));
}
}
+205
View File
@@ -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()
}
+1
View File
@@ -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;