From f0b11acc8808e54ca4bd89ca04422f7df3e686af Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Wed, 3 Jun 2026 10:32:48 +0300 Subject: [PATCH] feat(viewport): per-tile model render mode, persisted via VPORT entries Each model-space tile now carries its own visual style instead of one global mode shared by every pane: - ModelTile gains a `render_mode`; the active tile renders with the live picker mode, inactive tiles keep their stored style, so changing one tile no longer changes the others. Split inherits the source style. - SetRenderMode writes only the active tile; switching the active tile mirrors its style back into the tab (picker + live render). - Persisted through the *Active VPORT entries (acadrust now round-trips the render mode via DXF code 281 / DWG RC 281), restored on open for both tiled and single-tile model layouts. - Bump acadrust to pick up the VPORT render-mode field. Co-Authored-By: Claude Opus 4.8 --- Cargo.lock | 4 ++-- src/app/update.rs | 36 +++++++++++++++++++++++++++++ src/scene/mod.rs | 59 ++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 96 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 451641a90..d9ae3e72e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -54,7 +54,7 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" [[package]] name = "acadrust" version = "0.3.4" -source = "git+https://github.com/HakanSeven12/acadrust?branch=main#b6834f22c6761b253f3753b26e2b6aefc23f4ff6" +source = "git+https://github.com/HakanSeven12/acadrust?branch=main#5b09ccc00d92bb55dcccbef97c0fcecf94417e16" dependencies = [ "ahash", "anyhow", @@ -3176,7 +3176,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b577e2d69827c4740cba2b52efaad1c4cc7c73042860b199710b3575c68438d" dependencies = [ "bytecount", - "memchr 1.0.2", + "memchr 2.8.1", "nom 8.0.0", ] diff --git a/src/app/update.rs b/src/app/update.rs index a467f4299..727add69e 100644 --- a/src/app/update.rs +++ b/src/app/update.rs @@ -230,6 +230,7 @@ impl OpenCADStudio { // selected — see #21. self.sync_ribbon_from_selection(); self.tabs[i].scene.restore_saved_camera(); + self.sync_render_mode_to_active_tile(i); self.tabs[i].last_synced_camera_gen = self.tabs[i].scene.camera_generation; self.tabs[i].dirty = false; self.tabs[i].history = super::document::HistoryState::default(); @@ -704,6 +705,10 @@ impl OpenCADStudio { return Task::none(); } self.tabs[i].render_mode = mode; + // Write the style onto the active Model tile alone so it + // sticks when that tile loses focus and the other tiles keep + // their own styles. + self.tabs[i].scene.set_active_model_tile_render_mode(mode); // Keep the legacy `wireframe` bool synced — both wireframe // modes set it, everything else clears it. let wf = matches!(mode, M::Wireframe2D | M::Wireframe3D); @@ -2024,6 +2029,7 @@ impl OpenCADStudio { .set_active_model_tile_at(p.x / vp_size.0, p.y / vp_size.1) { self.tabs[i].scene.camera_generation += 1; + self.sync_render_mode_to_active_tile(i); } } @@ -2386,6 +2392,7 @@ impl OpenCADStudio { .set_active_model_tile_at(p.x / vw, p.y / vh) { self.tabs[i].scene.camera_generation += 1; + self.sync_render_mode_to_active_tile(i); return Task::none(); } } @@ -6056,6 +6063,35 @@ impl OpenCADStudio { /// `grip_hover` while the cursor sits within `GRIP_THRESHOLD_PX` of /// a grip and opens `grip_popup` once the dwell exceeds the /// threshold. Cursor drift clears both. + /// After the active Model tile changes, mirror its stored visual style + /// into the tab so the picker shows it and the tile renders with it + /// (the active tile draws with the tab's live render mode). + fn sync_render_mode_to_active_tile(&mut self, i: usize) { + use acadrust::entities::ViewportRenderMode as M; + if self.tabs[i].scene.current_layout != "Model" { + return; + } + let mode = self.tabs[i].scene.active_model_tile_render_mode(); + if self.tabs[i].render_mode == mode { + return; + } + let label = match mode { + M::Wireframe2D => "Wireframe 2D", + M::Wireframe3D => "Wireframe 3D", + M::HiddenLine => "Hidden Line", + M::FlatShaded => "Flat Shaded", + M::GouraudShaded => "Gouraud Shaded", + M::FlatShadedWithEdges => "Flat Shaded + Edges", + M::GouraudShadedWithEdges => "Gouraud Shaded + Edges", + }; + self.tabs[i].render_mode = mode; + let wf = matches!(mode, M::Wireframe2D | M::Wireframe3D); + self.tabs[i].wireframe = wf; + self.ribbon.set_wireframe(wf); + self.tabs[i].visual_style = label.into(); + self.tabs[i].scene.bump_geometry(); + } + fn update_grip_hover(&mut self, i: usize, p: iced::Point) { const HOVER_OPEN_MS: u128 = 600; const POPUP_DISMISS_PX: f32 = 80.0; diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 24d679032..47cca0240 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -449,6 +449,9 @@ pub struct ViewportInstance { pub(crate) struct ModelTile { pub(crate) rect: iced::Rectangle, pub(crate) camera: Camera, + /// Visual style for this tile alone — each pane carries its own so + /// changing one tile's render mode never touches the others. + pub(crate) render_mode: acadrust::entities::ViewportRenderMode, } /// Tolerance for matching two normalized tile coordinates as "the same" @@ -684,6 +687,7 @@ impl Scene { height: 1.0, }, camera: Camera::default(), + render_mode: acadrust::entities::ViewportRenderMode::Wireframe2D, }]), active_model_tile: std::cell::Cell::new(0), selection: Rc::new(RefCell::new(SelectionState::default())), @@ -2809,6 +2813,30 @@ impl Scene { } } + /// Visual style of the active Model tile (for the render-mode picker). + pub fn active_model_tile_render_mode( + &self, + ) -> acadrust::entities::ViewportRenderMode { + let tiles = self.model_tiles.borrow(); + let active = self.active_model_tile.get().min(tiles.len().saturating_sub(1)); + tiles + .get(active) + .map(|t| t.render_mode) + .unwrap_or(acadrust::entities::ViewportRenderMode::Wireframe2D) + } + + /// Set only the active Model tile's render mode. Other tiles keep theirs. + pub fn set_active_model_tile_render_mode( + &self, + mode: acadrust::entities::ViewportRenderMode, + ) { + let mut tiles = self.model_tiles.borrow_mut(); + let active = self.active_model_tile.get().min(tiles.len().saturating_sub(1)); + if let Some(t) = tiles.get_mut(active) { + t.render_mode = mode; + } + } + /// View-rotation matrix for the active viewport (MSPACE), or the /// paper-space camera's matrix when not in MSPACE. /// Used by ViewCube hit-testing so clicks map to the correct camera. @@ -4726,6 +4754,16 @@ impl Scene { /// Set the model-space camera from the VPORT table's *Active entry. /// Returns true if the entry was found and the camera was set. fn apply_active_vport_camera(&mut self) -> bool { + // Restore the single tile's visual style from the *Active entry, + // independent of where the camera itself comes from below. + if let Some(vp) = self.document.vports.iter().find(|v| v.name == "*Active") { + let mode = vp.render_mode; + let mut tiles = self.model_tiles.borrow_mut(); + let active = self.active_model_tile.get().min(tiles.len().saturating_sub(1)); + if let Some(t) = tiles.get_mut(active) { + t.render_mode = mode; + } + } // Prefer our named View entry — survives DWG save without being overridden. let saved_view = self .document @@ -4874,6 +4912,7 @@ impl Scene { self.camera_from_vport(vp).map(|cam| ModelTile { rect: Self::vport_to_tile_rect(vp.lower_left, vp.upper_right), camera: cam, + render_mode: vp.render_mode, }) }) .collect(); @@ -4935,6 +4974,7 @@ impl Scene { for tile in ordered_tiles { let (ll, ur) = Self::tile_rect_to_vport(tile.rect); let mut entry = self.vport_from_camera("*Active", &tile.camera, ll, ur); + entry.render_mode = tile.render_mode; entry.handle = self.document.allocate_handle(); self.document.vports.add_allow_duplicate(entry); } @@ -5614,15 +5654,18 @@ impl Scene { }, ) }; + let mode = tiles[active].render_mode; tiles[active] = ModelTile { rect: a, camera: cam_now.clone(), + render_mode: mode, }; tiles.insert( active + 1, ModelTile { rect: b, camera: cam_now, + render_mode: mode, }, ); } @@ -5669,11 +5712,21 @@ impl Scene { /// tile becomes active. Used by VPORTS presets and `reset_model_tiles`. pub fn set_model_tile_layout(&self, rects: Vec) { let cam_now = self.camera.borrow().clone(); + // Every new pane inherits the active tile's current visual style. + let mode = { + let tiles = self.model_tiles.borrow(); + let active = self.active_model_tile.get().min(tiles.len().saturating_sub(1)); + tiles + .get(active) + .map(|t| t.render_mode) + .unwrap_or(acadrust::entities::ViewportRenderMode::Wireframe2D) + }; let tiles: Vec = rects .into_iter() .map(|rect| ModelTile { rect, camera: cam_now.clone(), + render_mode: mode, }) .collect(); *self.model_tiles.borrow_mut() = if tiles.is_empty() { @@ -5685,6 +5738,7 @@ impl Scene { height: 1.0, }, camera: cam_now, + render_mode: mode, }] } else { tiles @@ -5751,7 +5805,10 @@ impl Scene { height: tile.rect.height * canvas_h, }, camera, - render_mode: model_mode, + // The active tile shows the live mode the picker + // drives; every other tile keeps its own stored + // style so editing one never disturbs the rest. + render_mode: if i == active { model_mode } else { tile.render_mode }, active: i == active, paper_sheet: false, }