diff --git a/src/app/mod.rs b/src/app/mod.rs index fa57f74c9..ce734aaa9 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -498,6 +498,11 @@ pub(super) struct OpenCADStudio { /// next frame to decide whether the ViewCube still has room beside it — so /// the two corner widgets adapt to the bar's real width, not an estimate. render_bar_w: std::sync::Arc, + /// Whether the visual-style flyout beside the active viewport is open. + render_mode_menu_open: bool, + /// Mode whose sample is shown while the pointer moves through the flyout. + /// This does not alter the drawing until the corresponding row is clicked. + render_mode_preview: Option, /// Whether the Properties panel is shown on the left (PROPERTIES). show_properties: bool, /// Whether the document file tabs are shown at the top (FILETAB). @@ -1708,6 +1713,12 @@ pub enum Message { /// styles). Replaces the binary `SetWireframe` over time; the older /// message stays for ribbon/CLI back-compat and forwards. SetRenderMode(acadrust::entities::ViewportRenderMode), + /// Open or close the active viewport's visual-style flyout. + ToggleRenderModeMenu(acadrust::entities::ViewportRenderMode), + /// Close the visual-style flyout after Escape or an outside click. + DismissRenderModeMenu, + /// Change only the sample shown beside the visual-style list. + PreviewRenderMode(acadrust::entities::ViewportRenderMode), /// Switch camera projection: true = Orthographic, false = Perspective. SetProjection(bool), /// Select a ribbon module tab by index. @@ -2933,6 +2944,8 @@ impl OpenCADStudio { ucs_icon_at_origin: true, show_viewcube: true, render_bar_w: std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0)), + render_mode_menu_open: false, + render_mode_preview: None, show_properties: true, show_file_tabs: true, show_layout_tabs: true, diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index 87711bed7..75cdecace 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -1191,7 +1191,30 @@ impl OpenCADStudio { Task::done(Message::SetRenderMode(mode)) } - Message::SetRenderMode(mode) => self.on_set_render_mode(mode), + Message::SetRenderMode(mode) => { + self.render_mode_menu_open = false; + self.render_mode_preview = None; + self.on_set_render_mode(mode) + } + + Message::ToggleRenderModeMenu(mode) => { + self.render_mode_menu_open = !self.render_mode_menu_open; + self.render_mode_preview = self.render_mode_menu_open.then_some(mode); + Task::none() + } + + Message::DismissRenderModeMenu => { + self.render_mode_menu_open = false; + self.render_mode_preview = None; + Task::none() + } + + Message::PreviewRenderMode(mode) => { + if self.render_mode_menu_open { + self.render_mode_preview = Some(mode); + } + Task::none() + } Message::SetProjection(ortho) => { use crate::scene::Projection; diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index 76bb5d758..f660f1118 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -1757,6 +1757,7 @@ impl OpenCADStudio { world_width: 0.0, depth_override: None, fill_is_3d: false, + fill_is_2d_solid: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/app/view/controls.rs b/src/app/view/controls.rs index 8b877c03b..c847d2dc4 100644 --- a/src/app/view/controls.rs +++ b/src/app/view/controls.rs @@ -1,8 +1,8 @@ use super::*; use super::super::document::{DynComponent, DynFieldEntry}; use super::super::Message; -use iced::widget::{button, container, mouse_area, row, tooltip}; -use iced::{Background, Border, Element, Theme}; +use iced::widget::{button, canvas, column, container, mouse_area, row, text, tooltip}; +use iced::{Background, Border, Color, Element, Length, Point, Rectangle, Theme}; use std::time::Duration; fn viewport_tooltip<'a>( @@ -22,15 +22,200 @@ fn viewport_tooltip<'a>( .into() } +#[derive(Clone, Copy)] +struct RenderModePreview { + mode: acadrust::entities::ViewportRenderMode, +} + +impl canvas::Program for RenderModePreview { + type State = (); + + fn draw( + &self, + _state: &(), + renderer: &iced::Renderer, + theme: &Theme, + bounds: Rectangle, + _cursor: iced::mouse::Cursor, + ) -> Vec { + use acadrust::entities::ViewportRenderMode as M; + + let mut frame = canvas::Frame::new(renderer, bounds.size()); + let palette = theme.palette(); + let ink = palette.background.base.text.scale_alpha(0.86); + let faint = ink.scale_alpha(0.34); + let accent = palette.primary.base.color; + let face = palette.primary.weak.color.scale_alpha(0.72); + let face_alt = palette.primary.strong.color.scale_alpha(0.64); + let edge = if self.mode == M::Wireframe3D { accent } else { ink }; + let shaded = matches!( + self.mode, + M::FlatShaded + | M::GouraudShaded + | M::FlatShadedWithEdges + | M::GouraudShadedWithEdges + ); + let flat = matches!(self.mode, M::FlatShaded | M::FlatShadedWithEdges); + let smooth = matches!(self.mode, M::GouraudShaded | M::GouraudShadedWithEdges); + let with_edges = matches!( + self.mode, + M::Wireframe2D + | M::Wireframe3D + | M::HiddenLine + | M::FlatShadedWithEdges + | M::GouraudShadedWithEdges + ); + + // A small cube makes hidden/back edges and the shaded-edge variants + // immediately distinguishable without depending on the open drawing. + let a = Point::new(25.0, 35.0); + let b = Point::new(87.0, 35.0); + let c = Point::new(87.0, 94.0); + let d = Point::new(25.0, 94.0); + let off = iced::Vector::new(22.0, -17.0); + let e = a + off; + let f = b + off; + let g = c + off; + let h = d + off; + let quad = |points: [Point; 4]| { + canvas::Path::new(|path| { + path.move_to(points[0]); + path.line_to(points[1]); + path.line_to(points[2]); + path.line_to(points[3]); + path.close(); + }) + }; + if shaded || self.mode == M::HiddenLine { + frame.fill(&quad([a, b, f, e]), face_alt); + frame.fill(&quad([b, c, g, f]), face.scale_alpha(0.82)); + frame.fill(&quad([a, d, c, b]), face); + } + let stroke_line = |frame: &mut canvas::Frame, from: Point, to: Point, color: Color| { + frame.stroke( + &canvas::Path::line(from, to), + canvas::Stroke::default().with_color(color).with_width(1.2), + ); + }; + if matches!(self.mode, M::Wireframe2D | M::Wireframe3D) { + for (from, to) in [(e, f), (f, g), (g, h), (h, e)] { + stroke_line(&mut frame, from, to, faint); + } + } + if with_edges { + for (from, to) in [ + (a, b), (b, c), (c, d), (d, a), + (a, e), (b, f), (c, g), (d, h), + (e, f), (f, g), + ] { + stroke_line(&mut frame, from, to, edge); + } + } + + // Curved sample: faceted tones for Flat, concentric highlight for + // Gouraud, and latitude/longitude lines for the wireframe modes. + let center = Point::new((bounds.width - 55.0).max(145.0), 59.0); + let radius = 32.0; + if flat { + for i in 0..12 { + let a0 = i as f32 * std::f32::consts::TAU / 12.0; + let a1 = (i + 1) as f32 * std::f32::consts::TAU / 12.0; + let wedge = canvas::Path::new(|path| { + path.move_to(center); + path.line_to(Point::new(center.x + radius * a0.cos(), center.y + radius * a0.sin())); + path.line_to(Point::new(center.x + radius * a1.cos(), center.y + radius * a1.sin())); + path.close(); + }); + frame.fill(&wedge, if i % 3 == 0 { face_alt } else { face }); + } + } else if smooth { + frame.fill(&canvas::Path::circle(center, radius), face_alt); + frame.fill( + &canvas::Path::circle(Point::new(center.x - 7.0, center.y - 8.0), radius * 0.72), + face, + ); + frame.fill( + &canvas::Path::circle(Point::new(center.x - 12.0, center.y - 13.0), radius * 0.34), + palette.primary.weak.text.scale_alpha(0.38), + ); + } + if with_edges || self.mode == M::HiddenLine { + frame.stroke( + &canvas::Path::circle(center, radius), + canvas::Stroke::default().with_color(edge).with_width(1.2), + ); + } + if matches!(self.mode, M::Wireframe2D | M::Wireframe3D) { + for scale in [0.38_f32, 0.72] { + let y = radius * scale; + let half = (radius * radius - y * y).sqrt(); + stroke_line( + &mut frame, + Point::new(center.x - half, center.y - y), + Point::new(center.x + half, center.y - y), + faint, + ); + stroke_line( + &mut frame, + Point::new(center.x - half, center.y + y), + Point::new(center.x + half, center.y + y), + faint, + ); + } + stroke_line( + &mut frame, + Point::new(center.x, center.y - radius), + Point::new(center.x, center.y + radius), + faint, + ); + } + + // Bottom samples show the intentional Wireframe 2D/3D distinction: + // the legacy planar solid loses only its interior in the 3D style, + // while a hatch-like pattern remains visible in both. + let solid = quad([ + Point::new(18.0, 118.0), + Point::new(74.0, 112.0), + Point::new(69.0, 140.0), + Point::new(23.0, 143.0), + ]); + if self.mode != M::Wireframe3D { + frame.fill(&solid, accent.scale_alpha(0.48)); + } + frame.stroke( + &solid, + canvas::Stroke::default().with_color(edge).with_width(1.0), + ); + let hatch_box = canvas::Path::rectangle(Point::new(105.0, 112.0), iced::Size::new(82.0, 31.0)); + frame.stroke( + &hatch_box, + canvas::Stroke::default().with_color(ink).with_width(1.0), + ); + for x in (94..188).step_by(9) { + let x = x as f32; + stroke_line( + &mut frame, + Point::new(x.max(105.0), 143.0), + Point::new((x + 24.0).min(187.0), 112.0), + faint, + ); + } + + vec![frame.into_geometry()] + } +} + pub(super) fn viewport_controls<'a>( render_mode: acadrust::entities::ViewportRenderMode, show_grid: bool, snap_on: bool, include_split: bool, tile_count: usize, + render_mode_menu_open: bool, + render_mode_preview: Option, ) -> Element<'a, Message> { use acadrust::entities::ViewportRenderMode as M; - let render_modes: Vec = vec![ + let render_modes = [ RenderModeChoice(M::Wireframe2D), RenderModeChoice(M::Wireframe3D), RenderModeChoice(M::HiddenLine), @@ -101,34 +286,135 @@ pub(super) fn viewport_controls<'a>( viewport_tooltip(button, title, command) }; - // Render-mode picker, restyled borderless so the outer chip frames it. - let picker = iced::widget::pick_list( - Some(RenderModeChoice(render_mode)), - render_modes, - |value| value.to_string(), + // The standard picker cannot show a live sample beside its rows. This + // flyout keeps the current style unchanged while hovering; a click commits. + let picker_head = button( + row![ + text(RenderModeChoice(render_mode).to_string()).size(11), + crate::ui::icons::themed_arrow_toggle(render_mode_menu_open, 9.0), + ] + .spacing(6) + .align_y(iced::Center), ) - .on_select(|c| Message::SetRenderMode(c.0)) - .text_size(11) + .on_press(Message::ToggleRenderModeMenu(render_mode)) .padding([4, 6]) - .style(move |theme: &Theme, _| { - let text = theme.palette().background.base.text; - iced::widget::pick_list::Style { - background: Background::Color(iced::Color::TRANSPARENT), - border: Border { - radius: 3.0.into(), + .style(move |theme: &Theme, status| { + let palette = theme.palette(); + let active = render_mode_menu_open + || matches!(status, button::Status::Hovered | button::Status::Pressed); + button::Style { + background: active.then_some(Background::Color(palette.background.strong.color)), + text_color: if active { + palette.background.strong.text + } else { + palette.background.base.text + }, + border: Border { + radius: 3.0.into(), + ..Default::default() + }, ..Default::default() - }, - text_color: text, - placeholder_color: text.scale_alpha(0.68), - handle_color: text, } }); - let picker = viewport_tooltip( - picker, + let picker_head = viewport_tooltip( + picker_head, crate::t!("Visual Style").into_owned(), "VISUALSTYLES", ); + let preview_mode = render_mode_preview.unwrap_or(render_mode); + let mut choices = column![].spacing(2).width(Length::Fixed(174.0)); + for choice in render_modes { + let highlighted = choice.0 == preview_mode; + let selected = choice.0 == render_mode; + let option = container(text(choice.to_string()).size(11).width(Length::Fill)) + .padding([6, 8]) + .width(Length::Fill) + .style(move |theme: &Theme| { + let palette = theme.palette(); + let active = highlighted; + container::Style { + background: active.then_some(Background::Color( + if selected { palette.primary.weak.color } else { palette.background.strong.color }, + )), + text_color: if selected { + Some(palette.primary.weak.text) + } else if active { + Some(palette.background.strong.text) + } else { + Some(palette.background.base.text) + }, + border: Border { + radius: 3.0.into(), + ..Default::default() + }, + ..Default::default() + } + }); + choices = choices.push(mouse_area(option) + .interaction(iced::mouse::Interaction::Pointer) + .on_enter(Message::PreviewRenderMode(choice.0)) + .on_press(Message::SetRenderMode(choice.0))); + } + let preview = container( + column![ + container( + text(RenderModeChoice(preview_mode).to_string()) + .size(12) + .wrapping(iced::advanced::text::Wrapping::None), + ) + .width(Length::Fixed(215.0)) + .height(Length::Fixed(16.0)) + .align_y(iced::alignment::Vertical::Center), + canvas(RenderModePreview { mode: preview_mode }) + .width(Length::Fixed(215.0)) + .height(Length::Fixed(154.0)), + ] + .spacing(6), + ) + .padding(8) + .style(|theme: &Theme| { + let palette = theme.palette(); + container::Style { + background: Some(Background::Color(palette.background.base.color)), + border: Border { + color: palette.background.neutral.color, + width: 1.0, + radius: 3.0.into(), + }, + text_color: Some(palette.background.base.text), + ..Default::default() + } + }); + let popup = container( + row![choices, preview] + .spacing(6) + .align_y(iced::alignment::Vertical::Top), + ) + .padding(6) + .style(|theme: &Theme| { + let palette = theme.palette(); + container::Style { + background: Some(Background::Color(palette.background.weak.color)), + border: Border { + color: palette.background.neutral.color, + width: 1.0, + radius: 4.0.into(), + }, + text_color: Some(palette.background.weak.text), + ..Default::default() + } + }); + let picker: Element<'a, Message> = iced_aw::DropDown::new( + picker_head, + popup, + render_mode_menu_open, + ) + .alignment(iced_aw::drop_down::Alignment::Bottom) + .offset(3.0) + .on_dismiss(Message::DismissRenderModeMenu) + .into(); + // Thin vertical divider between control groups. let sep = || { container(iced::widget::Space::new().width(1.0).height(16.0)).style(|theme: &Theme| { diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index ba7bc0426..d703d45ce 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -199,6 +199,21 @@ impl OpenCADStudio { theme_text.a, ]; let is_paper = tab.scene.current_layout != "Model"; + let committed_render_mode = if is_paper { + tab.scene + .active_viewport_render_mode() + .unwrap_or(tab.render_mode) + } else { + tab.render_mode + }; + // Gallery hover is a non-destructive live preview: only the shader's + // input changes. Dismissal restores the committed mode, while clicking + // a row follows the normal SetRenderMode path and persists it. + let viewport_render_mode = if self.render_mode_menu_open { + self.render_mode_preview.unwrap_or(committed_render_mode) + } else { + committed_render_mode + }; // Adaptive corner widgets: the ViewCube shows only while the active // viewport is wide enough to hold it *beside* the render-mode bar, whose // real width is measured each frame by its `DensitySwap` and read back @@ -250,7 +265,7 @@ impl OpenCADStudio { shader(ViewportPane::model( &tab.scene, viewcube_visible, - tab.render_mode, + viewport_render_mode, viewcube_text_color, )) .width(Fill) @@ -268,7 +283,7 @@ impl OpenCADStudio { // mouse_areas' hover state and drops their move events). let scene = &tab.scene; let show_viewcube = viewcube_visible; - let render_mode = tab.render_mode; + let render_mode = viewport_render_mode; let size_probe: Element<'_, Message> = responsive(move |size| { { let mut sel = scene.selection.borrow_mut(); @@ -856,6 +871,8 @@ impl OpenCADStudio { self.snapper.grid_snap(), true, tab.scene.model_tiles.borrow().len(), + self.render_mode_menu_open, + self.render_mode_preview, ); // Adaptive: DensitySwap measures the bar's real width every frame // (reported into `render_bar_w`, which the ViewCube reads to decide @@ -946,6 +963,8 @@ impl OpenCADStudio { self.snapper.grid_snap(), false, 0, + self.render_mode_menu_open, + self.render_mode_preview, ); let adaptive: Element<'_, Message> = DensitySwap::new(vec![ iced::widget::opaque(bar), diff --git a/src/entities/dimension.rs b/src/entities/dimension.rs index 93ccdab6d..3569f0cdc 100644 --- a/src/entities/dimension.rs +++ b/src/entities/dimension.rs @@ -1825,6 +1825,7 @@ fn tessellate_dimension_inner( world_width: 0.0, depth_override: None, fill_is_3d: false, + fill_is_2d_solid: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, @@ -1854,6 +1855,7 @@ fn tessellate_dimension_inner( world_width: 0.0, depth_override: None, fill_is_3d: false, + fill_is_2d_solid: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, @@ -1883,6 +1885,7 @@ fn tessellate_dimension_inner( world_width: 0.0, depth_override: None, fill_is_3d: false, + fill_is_2d_solid: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, @@ -1913,6 +1916,7 @@ fn tessellate_dimension_inner( world_width: 0.0, depth_override: None, fill_is_3d: false, + fill_is_2d_solid: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, @@ -1959,6 +1963,7 @@ fn tessellate_dimension_inner( world_width: 0.0, depth_override: None, fill_is_3d: false, + fill_is_2d_solid: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/entities/leader.rs b/src/entities/leader.rs index d5575b2ec..450ec9796 100644 --- a/src/entities/leader.rs +++ b/src/entities/leader.rs @@ -624,6 +624,7 @@ impl LeaderTess for Leader { world_width: 0.0, depth_override: None, fill_is_3d: false, + fill_is_2d_solid: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, @@ -798,6 +799,7 @@ impl LeaderTess for Leader { world_width: 0.0, depth_override: None, fill_is_3d: false, + fill_is_2d_solid: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/entities/multileader.rs b/src/entities/multileader.rs index 960345b61..5c8e7ce3b 100644 --- a/src/entities/multileader.rs +++ b/src/entities/multileader.rs @@ -1507,6 +1507,7 @@ impl MultiLeaderTess for MultiLeader { world_width: 0.0, depth_override: None, fill_is_3d: false, + fill_is_2d_solid: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, @@ -1829,6 +1830,7 @@ impl MultiLeaderTess for MultiLeader { world_width: 0.0, depth_override: None, fill_is_3d: false, + fill_is_2d_solid: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, @@ -1897,6 +1899,7 @@ impl MultiLeaderTess for MultiLeader { world_width: 0.0, depth_override: None, fill_is_3d: false, + fill_is_2d_solid: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, @@ -1980,6 +1983,7 @@ impl MultiLeaderTess for MultiLeader { world_width: 0.0, depth_override: None, fill_is_3d: false, + fill_is_2d_solid: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, @@ -2018,6 +2022,7 @@ impl MultiLeaderTess for MultiLeader { world_width: 0.0, depth_override: None, fill_is_3d: false, + fill_is_2d_solid: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/entities/table.rs b/src/entities/table.rs index ac4ec5ffe..e899febb8 100644 --- a/src/entities/table.rs +++ b/src/entities/table.rs @@ -1671,6 +1671,7 @@ pub fn tessellate_table( world_width: 0.0, depth_override: None, fill_is_3d: false, + fill_is_2d_solid: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/modules/annotate/aligned_dim.rs b/src/modules/annotate/aligned_dim.rs index 8682000f8..6de0b5545 100644 --- a/src/modules/annotate/aligned_dim.rs +++ b/src/modules/annotate/aligned_dim.rs @@ -191,6 +191,7 @@ impl CadCommand for AlignedDimensionCommand { world_width: 0.0, depth_override: None, fill_is_3d: false, + fill_is_2d_solid: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, @@ -245,6 +246,7 @@ fn preview_aligned(p1: DVec3, p2: DVec3, dim_pt: DVec3) -> WireModel { world_width: 0.0, depth_override: None, fill_is_3d: false, + fill_is_2d_solid: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/modules/annotate/angular_dim.rs b/src/modules/annotate/angular_dim.rs index 4d50cdd6b..86c3cf780 100644 --- a/src/modules/annotate/angular_dim.rs +++ b/src/modules/annotate/angular_dim.rs @@ -139,6 +139,7 @@ fn preview_wire(points: Vec) -> WireModel { world_width: 0.0, depth_override: None, fill_is_3d: false, + fill_is_2d_solid: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/modules/annotate/diameter_dim.rs b/src/modules/annotate/diameter_dim.rs index 6a9f6ec9d..19f140fb1 100644 --- a/src/modules/annotate/diameter_dim.rs +++ b/src/modules/annotate/diameter_dim.rs @@ -185,6 +185,7 @@ fn preview_line(a: Vec3, b: Vec3) -> WireModel { world_width: 0.0, depth_override: None, fill_is_3d: false, + fill_is_2d_solid: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/modules/annotate/dim_baseline.rs b/src/modules/annotate/dim_baseline.rs index 6679910d5..03310422f 100644 --- a/src/modules/annotate/dim_baseline.rs +++ b/src/modules/annotate/dim_baseline.rs @@ -184,6 +184,7 @@ impl CadCommand for DimBaselineCommand { world_width: 0.0, depth_override: None, fill_is_3d: false, + fill_is_2d_solid: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/modules/annotate/dim_continue.rs b/src/modules/annotate/dim_continue.rs index 8eb13890a..20b1f024c 100644 --- a/src/modules/annotate/dim_continue.rs +++ b/src/modules/annotate/dim_continue.rs @@ -164,6 +164,7 @@ impl CadCommand for DimContinueCommand { world_width: 0.0, depth_override: None, fill_is_3d: false, + fill_is_2d_solid: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/modules/annotate/dimjogline.rs b/src/modules/annotate/dimjogline.rs index 92be2162b..1ca1306b5 100644 --- a/src/modules/annotate/dimjogline.rs +++ b/src/modules/annotate/dimjogline.rs @@ -90,6 +90,7 @@ impl CadCommand for DimJogLineCommand { world_width: 0.0, depth_override: None, fill_is_3d: false, + fill_is_2d_solid: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/modules/annotate/dimtedit.rs b/src/modules/annotate/dimtedit.rs index ae00d3192..13f166468 100644 --- a/src/modules/annotate/dimtedit.rs +++ b/src/modules/annotate/dimtedit.rs @@ -107,6 +107,7 @@ impl CadCommand for DimTeditCommand { world_width: 0.0, depth_override: None, fill_is_3d: false, + fill_is_2d_solid: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/modules/annotate/leader_cmd.rs b/src/modules/annotate/leader_cmd.rs index 78a5c5c4a..f75b0a9ca 100644 --- a/src/modules/annotate/leader_cmd.rs +++ b/src/modules/annotate/leader_cmd.rs @@ -233,6 +233,7 @@ fn preview_wire(pts: &[Vec3], arrow_size: f32) -> WireModel { world_width: 0.0, depth_override: None, fill_is_3d: false, + fill_is_2d_solid: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/modules/annotate/linear_dim.rs b/src/modules/annotate/linear_dim.rs index 849cb179d..4d76c7927 100644 --- a/src/modules/annotate/linear_dim.rs +++ b/src/modules/annotate/linear_dim.rs @@ -223,6 +223,7 @@ fn preview_wire(points: Vec) -> WireModel { world_width: 0.0, depth_override: None, fill_is_3d: false, + fill_is_2d_solid: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/modules/annotate/mleader_cmd.rs b/src/modules/annotate/mleader_cmd.rs index 18f906566..87e4adff6 100644 --- a/src/modules/annotate/mleader_cmd.rs +++ b/src/modules/annotate/mleader_cmd.rs @@ -205,6 +205,7 @@ fn preview_wire(pts: &[Vec3], arrow_size: f32) -> WireModel { world_width: 0.0, depth_override: None, fill_is_3d: false, + fill_is_2d_solid: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/modules/annotate/mleader_edit.rs b/src/modules/annotate/mleader_edit.rs index fd8a4a3b0..610de3aee 100644 --- a/src/modules/annotate/mleader_edit.rs +++ b/src/modules/annotate/mleader_edit.rs @@ -466,6 +466,7 @@ fn preview_wire(pts: &[Vec3]) -> WireModel { world_width: 0.0, depth_override: None, fill_is_3d: false, + fill_is_2d_solid: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/modules/annotate/ordinate_dim.rs b/src/modules/annotate/ordinate_dim.rs index 9dc83233d..77fac16ab 100644 --- a/src/modules/annotate/ordinate_dim.rs +++ b/src/modules/annotate/ordinate_dim.rs @@ -126,6 +126,7 @@ fn preview_wire(points: Vec) -> WireModel { world_width: 0.0, depth_override: None, fill_is_3d: false, + fill_is_2d_solid: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/modules/annotate/qdim.rs b/src/modules/annotate/qdim.rs index 038df193b..e4b376071 100644 --- a/src/modules/annotate/qdim.rs +++ b/src/modules/annotate/qdim.rs @@ -97,6 +97,7 @@ impl CadCommand for QdimCommand { world_width: 0.0, depth_override: None, fill_is_3d: false, + fill_is_2d_solid: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/modules/annotate/radius_dim.rs b/src/modules/annotate/radius_dim.rs index 452ab343b..b41d64f9c 100644 --- a/src/modules/annotate/radius_dim.rs +++ b/src/modules/annotate/radius_dim.rs @@ -190,6 +190,7 @@ fn preview_wire(points: Vec) -> WireModel { world_width: 0.0, depth_override: None, fill_is_3d: false, + fill_is_2d_solid: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/modules/annotate/table_cmd.rs b/src/modules/annotate/table_cmd.rs index 4652e9979..8bedcbff0 100644 --- a/src/modules/annotate/table_cmd.rs +++ b/src/modules/annotate/table_cmd.rs @@ -189,6 +189,7 @@ impl CadCommand for TableCommand { world_width: 0.0, depth_override: None, fill_is_3d: false, + fill_is_2d_solid: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/modules/annotate/tolerance_cmd.rs b/src/modules/annotate/tolerance_cmd.rs index 99c0e69ad..1dc566f50 100644 --- a/src/modules/annotate/tolerance_cmd.rs +++ b/src/modules/annotate/tolerance_cmd.rs @@ -91,6 +91,7 @@ impl CadCommand for ToleranceCommand { world_width: 0.0, depth_override: None, fill_is_3d: false, + fill_is_2d_solid: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/modules/draw/draw/attdef.rs b/src/modules/draw/draw/attdef.rs index 591b6ac46..8f5128270 100644 --- a/src/modules/draw/draw/attdef.rs +++ b/src/modules/draw/draw/attdef.rs @@ -188,6 +188,7 @@ impl CadCommand for AttdefCommand { world_width: 0.0, depth_override: None, fill_is_3d: false, + fill_is_2d_solid: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/modules/draw/draw/mline.rs b/src/modules/draw/draw/mline.rs index 674c93b68..664651d25 100644 --- a/src/modules/draw/draw/mline.rs +++ b/src/modules/draw/draw/mline.rs @@ -170,6 +170,7 @@ impl CadCommand for MlineCommand { world_width: 0.0, depth_override: None, fill_is_3d: false, + fill_is_2d_solid: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/modules/draw/draw/raster_image.rs b/src/modules/draw/draw/raster_image.rs index 63f1b070e..3f7868c94 100644 --- a/src/modules/draw/draw/raster_image.rs +++ b/src/modules/draw/draw/raster_image.rs @@ -114,6 +114,7 @@ impl CadCommand for ImageCommand { world_width: 0.0, depth_override: None, fill_is_3d: false, + fill_is_2d_solid: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/modules/draw/draw/ray.rs b/src/modules/draw/draw/ray.rs index 6725ea111..2753b3a82 100644 --- a/src/modules/draw/draw/ray.rs +++ b/src/modules/draw/draw/ray.rs @@ -89,6 +89,7 @@ impl CadCommand for RayCommand { world_width: 0.0, depth_override: None, fill_is_3d: false, + fill_is_2d_solid: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, @@ -187,6 +188,7 @@ impl CadCommand for XLineCommand { world_width: 0.0, depth_override: None, fill_is_3d: false, + fill_is_2d_solid: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/modules/draw/draw/revcloud.rs b/src/modules/draw/draw/revcloud.rs index f152d93f7..83cfb182d 100644 --- a/src/modules/draw/draw/revcloud.rs +++ b/src/modules/draw/draw/revcloud.rs @@ -96,6 +96,7 @@ impl CadCommand for RevCloudCommand { world_width: 0.0, depth_override: None, fill_is_3d: false, + fill_is_2d_solid: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/modules/draw/inquiry/area.rs b/src/modules/draw/inquiry/area.rs index 997549207..2c961c096 100644 --- a/src/modules/draw/inquiry/area.rs +++ b/src/modules/draw/inquiry/area.rs @@ -414,6 +414,7 @@ impl CadCommand for AreaCommand { world_width: 0.0, depth_override: None, fill_is_3d: false, + fill_is_2d_solid: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/modules/draw/inquiry/dist.rs b/src/modules/draw/inquiry/dist.rs index 88222ed9d..4008c1db7 100644 --- a/src/modules/draw/inquiry/dist.rs +++ b/src/modules/draw/inquiry/dist.rs @@ -81,6 +81,7 @@ impl CadCommand for DistCommand { world_width: 0.0, depth_override: None, fill_is_3d: false, + fill_is_2d_solid: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/modules/draw/inquiry/measuregeom.rs b/src/modules/draw/inquiry/measuregeom.rs index afa2632f7..209b16711 100644 --- a/src/modules/draw/inquiry/measuregeom.rs +++ b/src/modules/draw/inquiry/measuregeom.rs @@ -72,6 +72,7 @@ impl MeasureGeomCommand { world_width: 0.0, depth_override: None, fill_is_3d: false, + fill_is_2d_solid: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/modules/draw/modify/trim.rs b/src/modules/draw/modify/trim.rs index a5087c209..7a2fbd100 100644 --- a/src/modules/draw/modify/trim.rs +++ b/src/modules/draw/modify/trim.rs @@ -4570,6 +4570,7 @@ fn preview_wire(points: Vec<[f32; 3]>, color: [f32; 4], name: &str) -> WireModel world_width: 0.0, depth_override: None, fill_is_3d: false, + fill_is_2d_solid: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/modules/model/primitive_cmd.rs b/src/modules/model/primitive_cmd.rs index fb897988a..eff03f3a3 100644 --- a/src/modules/model/primitive_cmd.rs +++ b/src/modules/model/primitive_cmd.rs @@ -310,6 +310,7 @@ fn wire(name: &str, points: Vec<[f32; 3]>) -> WireModel { world_width: 0.0, depth_override: None, fill_is_3d: false, + fill_is_2d_solid: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/modules/view/plot_window.rs b/src/modules/view/plot_window.rs index 27103177c..c610e6778 100644 --- a/src/modules/view/plot_window.rs +++ b/src/modules/view/plot_window.rs @@ -58,6 +58,7 @@ impl CadCommand for PlotWindowCommand { world_width: 0.0, depth_override: None, fill_is_3d: false, + fill_is_2d_solid: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/modules/view/zoom_window.rs b/src/modules/view/zoom_window.rs index 0bfd4e6d0..f6d311bb7 100644 --- a/src/modules/view/zoom_window.rs +++ b/src/modules/view/zoom_window.rs @@ -77,6 +77,7 @@ impl CadCommand for ZoomWindowCommand { world_width: 0.0, depth_override: None, fill_is_3d: false, + fill_is_2d_solid: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/scene/cache/block_cache.rs b/src/scene/cache/block_cache.rs index 7685ecad1..4610b38bb 100644 --- a/src/scene/cache/block_cache.rs +++ b/src/scene/cache/block_cache.rs @@ -48,6 +48,9 @@ pub struct LocalWire { pub tangent_geoms: Vec, pub fill_tris: Vec<[f32; 3]>, pub fill_tris_low: Vec<[f32; 3]>, + /// Preserves the planar SOLID classification through block expansion so + /// it never gets merged with unrelated annotation fills of the same style. + pub fill_is_2d_solid: bool, /// Thickness-wall pick geometry, transformed by the insert like `points` /// so a block child's extruded wall stays selectable at the instance's /// place and scale. Never reaches the GPU. @@ -736,6 +739,7 @@ fn tessellate_sub_local( tangent_geoms: wire.tangent_geoms, fill_tris: wire.fill_tris, fill_tris_low: wire.fill_tris_low, + fill_is_2d_solid: wire.fill_is_2d_solid, pick_tris: wire.pick_tris, pick_tris_low: wire.pick_tris_low, color: wire.color, @@ -1191,6 +1195,9 @@ struct StyleKey { /// discriminator, so greek fills must stay in their own batches even /// when their color/style would otherwise collide with regular wires. is_fill_only: bool, + /// Part of the batch key so planar SOLID fills remain independently + /// switchable after block geometry is merged by style. + fill_is_2d_solid: bool, /// Bit-cast composed block-local depth for band wires (`0` = no override). /// Keeps bands of different in-block draw ranks in separate batches so /// each finalized WireModel carries one correct `depth_override`. @@ -1220,6 +1227,7 @@ struct BatchEntry { /// reconstructs `high + low`). Without it absolute f32 fills quantize to /// ~0.5 m and the greek-text rectangles shear. fill_tris_low: Vec<[f32; 3]>, + fill_is_2d_solid: bool, /// Accumulated thickness-wall pick geometry, paired high/low like /// `fill_tris`. Pick-only — no GPU batch reads this. pick_tris: Vec<[f32; 3]>, @@ -1261,6 +1269,7 @@ impl BatchEntry { aci: u8, plinegen: bool, _is_fill_only: bool, + fill_is_2d_solid: bool, ) -> Self { // `is_fill_only` is part of the StyleKey hash so greek fills never // share a batch with regular wires (otherwise the finalized @@ -1275,6 +1284,7 @@ impl BatchEntry { world_width, aci, plinegen, + fill_is_2d_solid, min_x: f32::INFINITY, min_y: f32::INFINITY, max_x: f32::NEG_INFINITY, @@ -1308,6 +1318,7 @@ impl Batches { world_width: b.world_width, depth_override: b.local_depth, fill_is_3d: false, + fill_is_2d_solid: b.fill_is_2d_solid, pick_tris: b.pick_tris, pick_tris_low: b.pick_tris_low, dash_from_start: false, @@ -1352,6 +1363,7 @@ fn style_key( aci: u8, plinegen: bool, is_fill_only: bool, + fill_is_2d_solid: bool, local_depth: Option, ) -> StyleKey { StyleKey { @@ -1377,6 +1389,7 @@ fn style_key( aci, plinegen, is_fill_only, + fill_is_2d_solid, depth_bits: local_depth.map_or(0, f32::to_bits), } } @@ -1708,6 +1721,7 @@ fn emit_wire( lw.aci, lw.plinegen, lw.is_fill_only, + lw.fill_is_2d_solid, local_depth, ); @@ -1730,6 +1744,7 @@ fn emit_wire( lw.aci, lw.plinegen, lw.is_fill_only, + lw.fill_is_2d_solid, ) }); entry.local_depth = local_depth; diff --git a/src/scene/convert/tess.rs b/src/scene/convert/tess.rs index 67f092099..0ab9d86e2 100644 --- a/src/scene/convert/tess.rs +++ b/src/scene/convert/tess.rs @@ -712,6 +712,7 @@ pub(crate) fn tessellate_entity( world_width: 0.0, depth_override: None, fill_is_3d: false, + fill_is_2d_solid: false, pick_tris, pick_tris_low, dash_from_start: false, @@ -961,6 +962,7 @@ pub(crate) fn tessellate_entity( world_width: 0.0, depth_override: None, fill_is_3d: false, + fill_is_2d_solid: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, @@ -1402,6 +1404,7 @@ fn lod_stub_wire( world_width: 0.0, depth_override: None, fill_is_3d: false, + fill_is_2d_solid: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, @@ -1491,6 +1494,7 @@ fn lod_stub_wire_3d( world_width: 0.0, depth_override: None, fill_is_3d: false, + fill_is_2d_solid: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/scene/convert/tessellate.rs b/src/scene/convert/tessellate.rs index 5d1583cce..59203c886 100644 --- a/src/scene/convert/tessellate.rs +++ b/src/scene/convert/tessellate.rs @@ -376,6 +376,7 @@ pub fn tessellate( world_width: 0.0, depth_override: None, fill_is_3d: false, + fill_is_2d_solid: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), // MLINE dashes: A-type aligned, but the end-dash length is @@ -674,6 +675,7 @@ pub fn tessellate( world_width: 0.0, depth_override: None, fill_is_3d: false, + fill_is_2d_solid: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, @@ -714,6 +716,7 @@ pub fn tessellate( world_width: 0.0, depth_override: None, fill_is_3d: false, + fill_is_2d_solid: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, @@ -761,6 +764,7 @@ pub fn tessellate( world_width: 0.0, depth_override: None, fill_is_3d: false, + fill_is_2d_solid: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, @@ -789,6 +793,7 @@ pub fn tessellate( world_width: 0.0, depth_override: None, fill_is_3d: false, + fill_is_2d_solid: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, @@ -847,6 +852,7 @@ pub fn tessellate( world_width: 0.0, depth_override: None, fill_is_3d: false, + fill_is_2d_solid: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, @@ -887,6 +893,7 @@ pub fn tessellate( world_width: 0.0, depth_override: None, fill_is_3d: false, + fill_is_2d_solid: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, @@ -924,6 +931,7 @@ pub fn tessellate( world_width: 0.0, depth_override: None, fill_is_3d: false, + fill_is_2d_solid: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, @@ -954,6 +962,7 @@ pub fn tessellate( world_width: 0.0, depth_override: None, fill_is_3d: false, + fill_is_2d_solid: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, @@ -1006,6 +1015,7 @@ pub fn tessellate( world_width: 0.0, depth_override: None, fill_is_3d: false, + fill_is_2d_solid: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, @@ -1057,6 +1067,7 @@ pub fn tessellate( world_width: 0.0, depth_override: None, fill_is_3d: false, + fill_is_2d_solid: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, @@ -1101,6 +1112,7 @@ pub fn tessellate( world_width: polyline_band_width(entity), depth_override: None, fill_is_3d: false, + fill_is_2d_solid: false, pick_tris, pick_tris_low, dash_from_start: false, @@ -1194,6 +1206,7 @@ pub fn tessellate( world_width: 0.0, depth_override: None, fill_is_3d: false, + fill_is_2d_solid: false, pick_tris, pick_tris_low, dash_from_start: false, @@ -1253,6 +1266,7 @@ pub fn tessellate( fill_tris, fill_tris_low, fill_is_3d, + fill_is_2d_solid: matches!(entity, EntityType::Solid(_)), depth_override: None, }); } @@ -1263,6 +1277,7 @@ pub fn tessellate( world_width: 0.0, depth_override: None, fill_is_3d: false, + fill_is_2d_solid: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, @@ -1306,6 +1321,7 @@ pub fn tessellate( world_width: polyline_band_width(entity), depth_override: None, fill_is_3d: false, + fill_is_2d_solid: false, pick_tris, pick_tris_low, dash_from_start: false, @@ -1350,6 +1366,7 @@ pub fn tessellate( world_width, depth_override: None, fill_is_3d: false, + fill_is_2d_solid: false, pick_tris, pick_tris_low, dash_from_start: false, @@ -1482,6 +1499,7 @@ pub fn tessellate( world_width: 0.0, depth_override: None, fill_is_3d: false, + fill_is_2d_solid: false, pick_tris, pick_tris_low, dash_from_start: false, diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 5c7db96e1..7d5107c9b 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -1649,6 +1649,9 @@ pub struct Scene { /// constants. `[depth, half]` retains a fixed child sub-range for block /// composition. Full sort/layout/block changes rebuild the labels. draw_depth_cache: RefCell>, + /// Shared empty map for 3-D wireframe, where true depth wins instead of + /// the entity submission order used by the optimized 2-D style. + no_draw_depths: Arc>, /// Cached hatch fill models, keyed by target block and geometry_epoch. View culling /// is handled at draw time via `hatch_skip_flags` in the pipeline, /// not at build time — that lets the GPU buffer stay stable across @@ -1960,6 +1963,7 @@ impl Scene { interaction_handle_index_cache: RefCell::new(None), sort_cache: RefCell::new(None), draw_depth_cache: RefCell::new(None), + no_draw_depths: Arc::new(HashMap::default()), hatch_cache: RefCell::new(HashMap::default()), wipeout_cache: RefCell::new(HashMap::default()), image_cache: RefCell::new(HashMap::default()), diff --git a/src/scene/model/wire_model.rs b/src/scene/model/wire_model.rs index 80ef3c33f..7bcd31fe9 100644 --- a/src/scene/model/wire_model.rs +++ b/src/scene/model/wire_model.rs @@ -145,6 +145,9 @@ pub struct WireModel { /// every view mode. The render pass can't infer this from `fill_tris_low` /// alone — a 2-D fill at UTM scale carries a low residual too. pub fill_is_3d: bool, + /// `true` only for a planar SOLID entity's interior. Wireframe 3D omits + /// this fill while preserving its perimeter and every other 2-D overlay. + pub fill_is_2d_solid: bool, } impl WireModel { @@ -196,6 +199,7 @@ impl WireModel { world_width: 0.0, depth_override: None, fill_is_3d: false, + fill_is_2d_solid: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), text_verts: Vec::new(), @@ -430,6 +434,7 @@ impl Default for WireModel { fill_tris_low: Vec::new(), depth_override: None, fill_is_3d: false, + fill_is_2d_solid: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), } diff --git a/src/scene/paper.rs b/src/scene/paper.rs index 111b36f18..3e0e371a3 100644 --- a/src/scene/paper.rs +++ b/src/scene/paper.rs @@ -82,8 +82,10 @@ impl Scene { /// scene camera (tiled splits will append more later). `model_mode` /// supplies its render mode (held on the tab, not the scene). /// - **Paper layout**: one instance per content viewport entity - /// (`id > 1`, owned by the current layout block, switched on), - /// using each viewport's own camera and render mode. + /// (`id > 1`, owned by the current layout block, switched on), using each + /// viewport's own camera and render mode. `model_mode` temporarily + /// overrides the active viewport so the visual-style gallery can preview + /// on hover without modifying the document. pub fn active_viewports( &self, canvas_w: f32, @@ -182,7 +184,11 @@ impl Scene { tile_idx: None, screen_rect, camera, - render_mode: vp.render_mode, + render_mode: if self.active_viewport == Some(h) { + model_mode + } else { + vp.render_mode + }, active: self.active_viewport == Some(h), grid_on: vp.status.grid_on, paper_sheet: false, diff --git a/src/scene/pick/xclip.rs b/src/scene/pick/xclip.rs index 70175278e..f20e29395 100644 --- a/src/scene/pick/xclip.rs +++ b/src/scene/pick/xclip.rs @@ -263,6 +263,7 @@ pub fn frame_wire( world_width: 0.0, depth_override: None, fill_is_3d: false, + fill_is_2d_solid: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, @@ -783,4 +784,3 @@ mod tests { assert!(out.iter().all(|p| p[0] <= 10.0 + 1e-3)); } } - diff --git a/src/scene/pipeline/face3d_gpu.rs b/src/scene/pipeline/face3d_gpu.rs index f4d55d668..e33f37edd 100644 --- a/src/scene/pipeline/face3d_gpu.rs +++ b/src/scene/pipeline/face3d_gpu.rs @@ -108,12 +108,15 @@ impl Face3DGpu { /// true depth; fills with empty `fill_tris_low` (2D fills — text greek, /// MultiLeader / dimension backgrounds) feed the 2D draw-order buffer. /// - `keep_3d_mesh_fills`: when false (wireframe modes), the 3D side - /// is left empty; the 2D side is always populated. + /// is left empty. + /// - `show_2d_solid_fills`: false only for Wireframe 3D; removes the + /// legacy planar SOLID interior while keeping other 2-D overlays. pub fn from_wires( device: &wgpu::Device, face3d_wires: &[WireModel], all_wires: &[WireModel], keep_3d_mesh_fills: bool, + show_2d_solid_fills: bool, depth_map: &rustc_hash::FxHashMap, ) -> Self { let depth_of = @@ -168,13 +171,17 @@ impl Face3DGpu { // with both fill_tris and points (mesh edges + faces) keep the dim // so PolyfaceMesh / PolygonMesh still look 3-D-shaded. // - // 2-D fills always go to `verts_2d` (visible in every mode). + // 2-D fills go to `verts_2d`; only legacy planar SOLID interiors are + // omitted by Wireframe 3D. // 3-D mesh face data goes to `verts_3d` only when // `keep_3d_mesh_fills` is true. for wire in all_wires { if wire.fill_tris.is_empty() { continue; } + if !show_2d_solid_fills && wire.fill_is_2d_solid { + continue; + } // A real 3-D surface fill (PolyfaceMesh / PolygonMesh) carries a // double-single low residual paired with `fill_tris` — it lives at // true world coordinates and must keep its real depth. 2-D fills diff --git a/src/scene/pipeline/mod.rs b/src/scene/pipeline/mod.rs index 48fb66fe7..2bbb009f2 100644 --- a/src/scene/pipeline/mod.rs +++ b/src/scene/pipeline/mod.rs @@ -296,11 +296,12 @@ pub struct Pipeline { /// a pick bumps only `selection_generation`, refreshing the overlay without /// touching the main wire buffers. pub cached_selection: (u64, u64), - /// `(wire_content_id, face3d_fill_active)` the Face3D edge/fill buffers were - /// uploaded for. A stable content id avoids retaining the resident wire Arc: + /// `(wire_content_id, face3d_fill_active, show_2d_solid_fills)` the Face3D + /// edge/fill buffers were uploaded for. A stable content id avoids retaining + /// the resident wire Arc: /// that Arc must stay uniquely owned by Scene so a small edit can splice it /// in place instead of rebuilding the whole drawing. - pub cached_face3d_key: (u64, bool), + pub cached_face3d_key: (u64, bool, bool), /// Handle → indices into the resident wire set, built once per wire upload /// (when `cached_wire_id` changes). Lets the selection/hover xray overlay /// gather just the highlighted entity's wires (`O(highlighted)`) instead of @@ -1710,7 +1711,7 @@ impl Pipeline { cached_epoch: (u64::MAX, u64::MAX, u64::MAX), cached_wire_id: u64::MAX, cached_selection: (u64::MAX, u64::MAX), - cached_face3d_key: (u64::MAX, false), + cached_face3d_key: (u64::MAX, false, false), wire_handle_index: std::sync::Arc::new(rustc_hash::FxHashMap::default()), render_sig: u64::MAX, skip_geometry: false, @@ -2269,6 +2270,7 @@ impl Pipeline { face3d_wires: &[WireModel], all_wires: &[WireModel], wireframe_only: bool, + show_2d_solid_fills: bool, depth_map: &rustc_hash::FxHashMap, ) { let perf_started = crate::perf::enabled().then(iced::time::Instant::now); @@ -2279,11 +2281,15 @@ impl Pipeline { // Fill buffer split: 3D quads + PolyfaceMesh / PolygonMesh face // tris go to `chunks_3d` (gated by `keep_3d_mesh_fills`); // 2D fills (text-LOD greek, MultiLeader background) go to - // `chunks_2d` and are visible in every mode. + // `chunks_2d`. The 3-D wireframe additionally removes only legacy + // planar SOLID interiors; HATCH is handled by a separate pass. let keep_3d_mesh_fills = !wireframe_only; + let solid_fill_hidden = |wire: &WireModel| { + !show_2d_solid_fills && wire.fill_is_2d_solid + }; let has_any_2d_fill = all_wires .iter() - .any(|w| !w.fill_tris.is_empty() && w.points.is_empty()); + .any(|w| !w.fill_tris.is_empty() && w.points.is_empty() && !solid_fill_hidden(w)); let has_any_3d_fill = !face3d_wires.is_empty() || all_wires .iter() @@ -2297,6 +2303,7 @@ impl Pipeline { face3d_wires, all_wires, keep_3d_mesh_fills, + show_2d_solid_fills, depth_map, )); } diff --git a/src/scene/text/complex_lt.rs b/src/scene/text/complex_lt.rs index 15f88d667..06b67ed0f 100644 --- a/src/scene/text/complex_lt.rs +++ b/src/scene/text/complex_lt.rs @@ -313,6 +313,7 @@ pub fn apply_along( world_width: 0.0, depth_override: None, fill_is_3d: false, + fill_is_2d_solid: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, @@ -355,6 +356,7 @@ pub fn apply_along( world_width: 0.0, depth_override: None, fill_is_3d: false, + fill_is_2d_solid: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/scene/view/render.rs b/src/scene/view/render.rs index 0ec42d94d..7a53003d4 100644 --- a/src/scene/view/render.rs +++ b/src/scene/view/render.rs @@ -100,6 +100,10 @@ pub struct ViewportData { /// Hatch / wipeout uploads are deliberately *not* gated by this flag — /// the user toggle should only affect 3D solids, not 2D fills. pub(in crate::scene) view_wireframe: bool, + /// Legacy planar SOLID interiors remain filled in the optimized 2-D + /// wireframe, but become outlines in the 3-D wireframe. HATCH uses its + /// own pipeline and is intentionally unaffected. + pub(in crate::scene) show_2d_solid_fills: bool, /// Whether the active render mode wants 3D mesh fills uploaded. Off /// in `Wireframe2D` / `Wireframe3D`; on for every shaded variant. Set /// at the same point `view_wireframe` is computed so the two stay in @@ -169,7 +173,9 @@ pub struct Primitive { /// Flags the render pipeline consumes, derived from /// [`acadrust::entities::ViewportRenderMode`]. Each shaded variant fills /// 3D faces and meshes; the pure wireframes drop the fill and keep only -/// edges. `*WithEdges` variants render both. HiddenLine uses a depth +/// edges. The optimized 2-D wireframe retains planar SOLID interiors and +/// entity draw order; the 3-D wireframe uses true depth and outlines them. +/// `*WithEdges` variants render both. HiddenLine uses a depth /// prepass: face/mesh fills are uploaded but routed through depth-only /// pipelines so hidden edges drop out. `FlatShaded` vs `GouraudShaded` /// differ in shader uniform only and produce identical fill flags here. @@ -179,6 +185,7 @@ pub struct RenderModeFlags { pub mesh_fill: bool, pub show_3d_edges: bool, pub hidden_line: bool, + pub show_2d_solid_fills: bool, /// `true` for FlatShaded / FlatShadedWithEdges. The mesh shader /// reads `Uniforms.flat_shade` and replaces the smooth per-vertex /// normal with a per-triangle face normal so each triangle reads @@ -191,11 +198,20 @@ pub fn render_mode_flags( ) -> RenderModeFlags { use acadrust::entities::ViewportRenderMode as M; match mode { - M::Wireframe2D | M::Wireframe3D => RenderModeFlags { + M::Wireframe2D => RenderModeFlags { face3d_fill: false, mesh_fill: false, show_3d_edges: true, hidden_line: false, + show_2d_solid_fills: true, + flat_shade: false, + }, + M::Wireframe3D => RenderModeFlags { + face3d_fill: false, + mesh_fill: false, + show_3d_edges: true, + hidden_line: false, + show_2d_solid_fills: false, flat_shade: false, }, M::HiddenLine => RenderModeFlags { @@ -203,6 +219,7 @@ pub fn render_mode_flags( mesh_fill: true, show_3d_edges: true, hidden_line: true, + show_2d_solid_fills: true, flat_shade: false, }, M::FlatShaded => RenderModeFlags { @@ -210,6 +227,7 @@ pub fn render_mode_flags( mesh_fill: true, show_3d_edges: false, hidden_line: false, + show_2d_solid_fills: true, flat_shade: true, }, M::GouraudShaded => RenderModeFlags { @@ -217,6 +235,7 @@ pub fn render_mode_flags( mesh_fill: true, show_3d_edges: false, hidden_line: false, + show_2d_solid_fills: true, flat_shade: false, }, M::FlatShadedWithEdges => RenderModeFlags { @@ -224,6 +243,7 @@ pub fn render_mode_flags( mesh_fill: true, show_3d_edges: true, hidden_line: false, + show_2d_solid_fills: true, flat_shade: true, }, M::GouraudShadedWithEdges => RenderModeFlags { @@ -231,6 +251,7 @@ pub fn render_mode_flags( mesh_fill: true, show_3d_edges: true, hidden_line: false, + show_2d_solid_fills: true, flat_shade: false, }, } @@ -271,7 +292,7 @@ impl shader::Primitive for Primitive { inner.cached_wire_id = u64::MAX; inner.cached_selection = (u64::MAX, u64::MAX); inner.cached_mesh_content_id = u64::MAX; - inner.cached_face3d_key = (u64::MAX, false); + inner.cached_face3d_key = (u64::MAX, false, false); inner.cached_hatch_source = None; inner.cached_preview_hatch_source = None; inner.cached_wipeout_source = None; @@ -440,18 +461,26 @@ impl shader::Primitive for Primitive { .map_or(true, |source| !Arc::ptr_eq(&source, &draw_depths)) || (inner.cached_face3d_key.0 != vp.wire_content_id && !face_pass_unchanged); - if face3d_changed || face3d_fill_active != inner.cached_face3d_key.1 { + if face3d_changed + || face3d_fill_active != inner.cached_face3d_key.1 + || vp.show_2d_solid_fills != inner.cached_face3d_key.2 + { inner.upload_face3d( device, &vp.face3d_wires[..], &vp_wires[..], !face3d_fill_active, + vp.show_2d_solid_fills, &draw_depths, ); inner.cached_face3d_source = Some(Arc::clone(&vp.face3d_wires)); inner.cached_face3d_depth_source = Some(Arc::downgrade(&draw_depths)); } - inner.cached_face3d_key = (vp.wire_content_id, face3d_fill_active); + inner.cached_face3d_key = ( + vp.wire_content_id, + face3d_fill_active, + vp.show_2d_solid_fills, + ); // Wire buffers are world-space, so a camera move alone doesn't // change them — only the view_proj uniform (uploaded every frame). // Gate the upload on the wire content id instead of the camera tick: @@ -1117,6 +1146,7 @@ fn render_signature(vp: &ViewportData, clip_w: u32, clip_h: u32) -> u64 { vp.wire_content_id.hash(&mut h); vp.fill_mode.hash(&mut h); vp.view_wireframe.hash(&mut h); + vp.show_2d_solid_fills.hash(&mut h); vp.mesh_fill.hash(&mut h); vp.show_3d_edges.hash(&mut h); vp.hidden_line.hash(&mut h); @@ -2272,15 +2302,15 @@ impl Scene { // are NOT part of this buffer anymore (they go in a separate per-frame // overlay buffer below), so the base id is the source's stable content // gen — a drag or camera move never re-uploads the base wire set. - let wire_content_id = self.last_model_wire_gen.get(); - let wire_patch = self.model_wire_patch_for(wire_content_id); + let base_wire_content_id = self.last_model_wire_gen.get(); + let base_wire_patch = self.model_wire_patch_for(base_wire_content_id); // Split Face3D wires from the rest. The split is content-only (keyed // by the wire-set content id), so while the geometry is unchanged it's // memoized rather than re-walking every wire (handle lookup + clone) // each frame — for every source, since all ids are stable now. let (face3d_wires, other_arc) = { - let cached = { self.split_cache.borrow().get(&wire_content_id).cloned() }; - let inherited_empty = if let Some((base, patch)) = wire_patch.as_ref() { + let cached = { self.split_cache.borrow().get(&base_wire_content_id).cloned() }; + let inherited_empty = if let Some((base, patch)) = base_wire_patch.as_ref() { if patch.face_pass_changed { None } else { @@ -2311,12 +2341,12 @@ impl Scene { if c.len() > 8 { c.clear(); } - c.insert(wire_content_id, (fa.clone(), oa.clone())); + c.insert(base_wire_content_id, (fa.clone(), oa.clone())); (fa, oa) }); self.split_cache .borrow_mut() - .entry(wire_content_id) + .entry(base_wire_content_id) .or_insert_with(|| (fa.clone(), oa.clone())); (fa, oa.unwrap_or_else(|| Arc::clone(&base_arc))) }; @@ -2325,6 +2355,24 @@ impl Scene { // per-frame buffer so the (potentially huge) base buffer stays resident // and unchanged while a command preview or grip drag is live. let all_wires = other_arc; + // The 3-D wireframe deliberately ignores entity draw order and lets + // true depth decide overlaps. Tag its resident wire id separately so + // switching between the 2-D and 3-D styles rebuilds the GPU constants + // even though the world-space geometry itself did not change. The + // incremental patch's base id receives the same tag, preserving the + // arena fast path after the first mode switch. + let wire_mode_tag = u64::from( + inst.render_mode == acadrust::entities::ViewportRenderMode::Wireframe3D, + ); + let wire_content_id = base_wire_content_id + .wrapping_mul(2) + .wrapping_add(wire_mode_tag); + let wire_patch = base_wire_patch.map(|(base, patch)| { + ( + base.wrapping_mul(2).wrapping_add(wire_mode_tag), + patch, + ) + }); // A live overlay belongs to one drawing space, but every viewport that // displays that space must project the same world-space preview. In a // paper layout, model-space overlays go to all content viewports while @@ -2490,7 +2538,13 @@ impl Scene { } else { 0x3000_0000_0000_0000 | inst.handle.value() }; - let draw_depths = self.draw_depth_map(); + let draw_depths = if inst.render_mode + == acadrust::entities::ViewportRenderMode::Wireframe3D + { + Arc::clone(&self.no_draw_depths) + } else { + self.draw_depth_map() + }; let text_verts = self.gather_text_verts( &all_wires, wire_content_id, @@ -2567,6 +2621,7 @@ impl Scene { show_viewcube: inst.active && show_viewcube, fill_mode: self.document.header.fill_mode, view_wireframe, + show_2d_solid_fills: flags.show_2d_solid_fills, mesh_fill: flags.mesh_fill, show_3d_edges: flags.show_3d_edges, display_silhouette: flags.hidden_line || self.document.header.display_silhouette, diff --git a/src/scene/view/viewport_pane.rs b/src/scene/view/viewport_pane.rs index 07d87b868..a87a8959c 100644 --- a/src/scene/view/viewport_pane.rs +++ b/src/scene/view/viewport_pane.rs @@ -16,8 +16,9 @@ use iced::{mouse, Event, Rectangle}; pub struct ViewportPane<'a> { pub scene: &'a Scene, pub show_viewcube: bool, - /// Render mode applied to the Model layout's tiles. Paper-space content - /// viewports use the render mode stored on their own viewport entity. + /// Render mode applied to the active Model tile or active paper viewport. + /// The app may supply a temporary gallery-hover mode; inactive viewports + /// keep the values stored on their own tile/entity. pub render_mode: acadrust::entities::ViewportRenderMode, /// `Some(tile_idx)` → this widget renders a single Model pane (one shader /// per `pane_grid` pane, filling its own bounds). `None` → the unified