fix(scene): use layout-specific viewport classification and preserve duplicate AutoCAD *Active viewports

This commit is contained in:
Hakan Seven committed 2026-06-01 19:57:36 +03:00
1 parent 47a4bf19b3
commit 4a55cb1b0f
1 file changed
+90 -74
+90 -74
View File
@@ -922,6 +922,35 @@ impl Scene {
vp.center.x.abs() >= 0.5 || vp.center.y.abs() >= 0.5 vp.center.x.abs() >= 0.5 || vp.center.y.abs() >= 0.5
} }
fn current_layout_sheet_viewport_handle(&self) -> Handle {
self.document.objects.values().find_map(|obj| {
let ObjectType::Layout(layout) = obj else {
return None;
};
if layout.name == self.current_layout {
Some(layout.viewport)
} else {
None
}
}).unwrap_or(Handle::NULL)
}
fn is_content_viewport_in_layout(
&self,
vp: &acadrust::entities::Viewport,
layout_block: Handle,
) -> bool {
if vp.common.owner_handle != layout_block {
return false;
}
let sheet_handle = self.current_layout_sheet_viewport_handle();
if sheet_handle.is_valid() {
vp.common.handle != sheet_handle
} else {
Self::is_content_viewport(vp)
}
}
/// Public accessor for the block-record handle of the current layout. /// Public accessor for the block-record handle of the current layout.
/// Used by external callers (e.g. `commit_entity`) that need the handle /// Used by external callers (e.g. `commit_entity`) that need the handle
/// without going through private API. /// without going through private API.
@@ -1081,7 +1110,7 @@ impl Scene {
} }
self.document.entities().find_map(|e| { self.document.entities().find_map(|e| {
if let EntityType::Viewport(vp) = e { if let EntityType::Viewport(vp) = e {
if Self::is_content_viewport(vp) && vp.common.owner_handle == layout_block { if self.is_content_viewport_in_layout(vp, layout_block) {
return Some(vp_effective_scale( return Some(vp_effective_scale(
vp.custom_scale, vp.custom_scale,
vp.view_height, vp.view_height,
@@ -1107,7 +1136,7 @@ impl Scene {
.entities() .entities()
.filter_map(|e| { .filter_map(|e| {
if let EntityType::Viewport(vp) = e { if let EntityType::Viewport(vp) = e {
if Self::is_content_viewport(vp) && vp.common.owner_handle == layout_block { if self.is_content_viewport_in_layout(vp, layout_block) {
Some((vp.common.handle, vp.id, vp.frozen_layers.clone())) Some((vp.common.handle, vp.id, vp.frozen_layers.clone()))
} else { } else {
None None
@@ -1145,7 +1174,7 @@ impl Scene {
.entities() .entities()
.filter(|e| { .filter(|e| {
if let EntityType::Viewport(vp) = e { if let EntityType::Viewport(vp) = e {
Self::is_content_viewport(vp) && vp.common.owner_handle == layout_block self.is_content_viewport_in_layout(vp, layout_block)
} else { } else {
false false
} }
@@ -1173,7 +1202,7 @@ impl Scene {
} }
self.document.entities().find_map(|e| { self.document.entities().find_map(|e| {
if let EntityType::Viewport(vp) = e { if let EntityType::Viewport(vp) = e {
if Self::is_content_viewport(vp) && vp.common.owner_handle == layout_block { if self.is_content_viewport_in_layout(vp, layout_block) {
return Some(vp.common.handle); return Some(vp.common.handle);
} }
} }
@@ -2187,8 +2216,7 @@ impl Scene {
} }
}) })
.filter(|vp| { .filter(|vp| {
Self::is_content_viewport(vp) self.is_content_viewport_in_layout(vp, paper_block)
&& vp.common.owner_handle == paper_block
&& vp.status.is_on && vp.status.is_on
&& only_vp.map_or(true, |h| vp.common.handle == h) && only_vp.map_or(true, |h| vp.common.handle == h)
&& exclude_vp.map_or(true, |h| vp.common.handle != h) && exclude_vp.map_or(true, |h| vp.common.handle != h)
@@ -2668,8 +2696,7 @@ impl Scene {
let EntityType::Viewport(vp) = e else { let EntityType::Viewport(vp) = e else {
return None; return None;
}; };
if !Self::is_content_viewport(vp) if !self.is_content_viewport_in_layout(vp, layout_block)
|| vp.common.owner_handle != layout_block
|| !vp.status.is_on || !vp.status.is_on
{ {
return None; return None;
@@ -2694,8 +2721,7 @@ impl Scene {
let EntityType::Viewport(vp) = e else { let EntityType::Viewport(vp) = e else {
return None; return None;
}; };
if Self::is_content_viewport(vp) if self.is_content_viewport_in_layout(vp, layout_block)
&& vp.common.owner_handle == layout_block
&& vp.status.is_on && vp.status.is_on
{ {
Some(vp.common.handle) Some(vp.common.handle)
@@ -4690,66 +4716,49 @@ impl Scene {
} }
} }
/// Restore `model_tiles` from `*OCS_Tile_*` VPort entries that a previous /// Restore `model_tiles` from VPort entries that a previous save left in
/// save left in the document. Returns true on success — the caller skips /// the document. Native AutoCAD tiled model-space layouts are represented
/// `apply_active_vport_camera` in that case because the active tile's /// by duplicate `*Active` VPort entries.
/// camera has already been loaded into `self.camera`. /// Returns true on success — the caller skips `apply_active_vport_camera`
/// in that case because the active tile's camera has already been loaded
/// into `self.camera`.
fn restore_model_tiles_from_vports(&mut self) -> bool { fn restore_model_tiles_from_vports(&mut self) -> bool {
let mut indexed: Vec<(usize, acadrust::tables::VPort)> = self let active_vports: Vec<acadrust::tables::VPort> = self
.document .document
.vports .vports
.iter() .iter()
.filter_map(|v| { .filter(|v| v.name == "*Active")
let idx: usize = v .cloned()
.name
.strip_prefix("*OCS_Tile_")
.and_then(|s| s.parse().ok())?;
Some((idx, v.clone()))
})
.collect(); .collect();
if indexed.len() < 2 {
if active_vports.len() <= 1 {
return false; return false;
} }
indexed.sort_by_key(|(i, _)| *i);
let mut tiles = Vec::with_capacity(indexed.len()); let tiles: Vec<ModelTile> = active_vports
for (_, vp) in &indexed {
let Some(cam) = self.camera_from_vport(vp) else {
return false;
};
tiles.push(ModelTile {
rect: Self::vport_to_tile_rect(vp.lower_left, vp.upper_right),
camera: cam,
});
}
// Active tile = the one whose stored camera fields match `*Active`.
// The save path writes `*Active` to mirror the active tile's view, so
// this matches by view_target (the most discriminating field).
let active_idx = self
.document
.vports
.iter() .iter()
.find(|v| v.name == "*Active") .filter_map(|vp| {
.and_then(|act| { self.camera_from_vport(vp).map(|cam| ModelTile {
indexed.iter().position(|(_, vp)| { rect: Self::vport_to_tile_rect(vp.lower_left, vp.upper_right),
let dx = vp.view_target.x - act.view_target.x; camera: cam,
let dy = vp.view_target.y - act.view_target.y;
let dz = vp.view_target.z - act.view_target.z;
dx * dx + dy * dy + dz * dz < 1e-12
}) })
}) })
.unwrap_or(0); .collect();
let active_cam = tiles[active_idx].camera.clone();
if tiles.len() <= 1 {
return false;
}
let active_cam = tiles[0].camera.clone();
*self.model_tiles.borrow_mut() = tiles; *self.model_tiles.borrow_mut() = tiles;
self.active_model_tile.set(active_idx); self.active_model_tile.set(0);
*self.camera.borrow_mut() = active_cam; *self.camera.borrow_mut() = active_cam;
self.camera_generation += 1; self.camera_generation += 1;
true true
} }
/// Persist `model_tiles` to the VPort table. The active tile is also /// Persist `model_tiles` to the VPort table. Native AutoCAD tiled model
/// mirrored to `*Active` (already written by `sync_camera_to_document`); /// viewports are written as duplicate `*Active` entries.
/// every tile (including a single full-screen one) is written as a
/// `*OCS_Tile_<i>` entry so the layout survives roundtrip.
fn save_model_tiles_to_vports(&mut self) { fn save_model_tiles_to_vports(&mut self) {
// Stash the live camera into the active tile so the about-to-write // Stash the live camera into the active tile so the about-to-write
// snapshot reflects the user's most recent orbit / pan / zoom. // snapshot reflects the user's most recent orbit / pan / zoom.
@@ -4761,29 +4770,40 @@ impl Scene {
t.camera = live_cam; t.camera = live_cam;
} }
} }
// Remove every previous tile entry — names are reassigned by index.
let to_remove: Vec<String> = self let table_handle = self.document.vports.handle();
let preserved_vps: Vec<acadrust::tables::VPort> = self
.document .document
.vports .vports
.iter() .iter()
.filter(|v| v.name.starts_with("*OCS_Tile_")) .filter(|v| v.name != "*Active")
.map(|v| v.name.clone()) .cloned()
.collect(); .collect();
for name in to_remove { let mut new_vports = acadrust::tables::Table::with_handle(table_handle);
self.document.vports.remove(&name); for vp in preserved_vps {
new_vports.add_or_replace(vp);
} }
// Skip writing tile entries for the trivial single-tile config — self.document.vports = new_vports;
// `*Active` alone is enough and keeps the file noise-free.
let tiles = self.model_tiles.borrow().clone(); let tiles = self.model_tiles.borrow().clone();
if tiles.len() <= 1 { if tiles.is_empty() {
return; return;
} }
let active = self.active_model_tile.get().min(tiles.len().saturating_sub(1));
let mut ordered_tiles = Vec::with_capacity(tiles.len());
ordered_tiles.push(tiles[active].clone());
for (i, tile) in tiles.iter().enumerate() { for (i, tile) in tiles.iter().enumerate() {
if i != active {
ordered_tiles.push(tile.clone());
}
}
for tile in ordered_tiles {
let (ll, ur) = Self::tile_rect_to_vport(tile.rect); let (ll, ur) = Self::tile_rect_to_vport(tile.rect);
let name = format!("*OCS_Tile_{i}"); let mut entry = self.vport_from_camera("*Active", &tile.camera, ll, ur);
let mut entry = self.vport_from_camera(&name, &tile.camera, ll, ur);
entry.handle = self.document.allocate_handle(); entry.handle = self.document.allocate_handle();
self.document.vports.add_or_replace(entry); self.document.vports.add_allow_duplicate(entry);
} }
} }
@@ -4817,7 +4837,7 @@ impl Scene {
None None
} }
}) })
.find(|vp| vp.common.owner_handle == layout_block && !Self::is_content_viewport(vp)); .find(|vp| !self.is_content_viewport_in_layout(vp, layout_block));
let vp = match sheet_vp { let vp = match sheet_vp {
Some(v) => v, Some(v) => v,
@@ -4906,9 +4926,7 @@ impl Scene {
vp.view_height = view_height as f64; vp.view_height = view_height as f64;
} }
// Persist the tiled layout (multiple `*OCS_Tile_<i>` entries) so // Persist the tiled layout as duplicate `*Active` VPort entries.
// the panel split survives save→reopen. Single-tile configs skip
// this — `*Active` alone is enough.
self.save_model_tiles_to_vports(); self.save_model_tiles_to_vports();
// Also write to View table — survives DWG save without override. // Also write to View table — survives DWG save without override.
@@ -4936,7 +4954,7 @@ impl Scene {
} }
}) })
.find(|vp| { .find(|vp| {
vp.common.owner_handle == layout_block && !Self::is_content_viewport(vp) vp.common.owner_handle == layout_block && !self.is_content_viewport_in_layout(vp, layout_block)
}) })
.map(|vp| vp.common.handle); .map(|vp| vp.common.handle);
@@ -5610,8 +5628,7 @@ impl Scene {
let EntityType::Viewport(vp) = e else { let EntityType::Viewport(vp) = e else {
continue; continue;
}; };
if !Self::is_content_viewport(vp) if !self.is_content_viewport_in_layout(vp, layout_block)
|| vp.common.owner_handle != layout_block
|| !vp.status.is_on || !vp.status.is_on
{ {
continue; continue;
@@ -6995,4 +7012,3 @@ fn is_unindexable_entity(e: &acadrust::EntityType) -> bool {
E::Insert(_) | E::Viewport(_) | E::Block(_) | E::BlockEnd(_) E::Insert(_) | E::Viewport(_) | E::Block(_) | E::BlockEnd(_)
) )
} }