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 <noreply@anthropic.com>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-06-03 10:32:48 +03:00
1 parent addf4e3904
commit f0b11acc88
3 files changed
+96 -3

No files matched your search

Generated
+2 -2
View File
@@ -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",
]
+36
View File
@@ -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;
+58 -1
View File
@@ -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<iced::Rectangle>) {
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<ModelTile> = 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,
}