From 568422679d587b93b95e972a5734f68fdc5ea07e Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Tue, 2 Jun 2026 19:38:42 +0300 Subject: [PATCH] feat(paper): native sheet viewport for paper-space camera Persist the paper-space camera through the sheet viewport entity (which round-trips natively in both DXF and DWG) instead of the OpenCADStudio_Camera_* named-View side-channel: - apply_sheet_viewport_camera reads the sheet viewport entity first; the named View is only a backward-compat fallback for older files. - sync_camera_to_document (paper) writes the camera only to the sheet viewport entity. Model branch is unchanged. - Add Scene::ensure_sheet_viewport: guarantee every paper layout has its full-screen overall (id==1) viewport, creating one if a loaded file lacks it; wired into layout enter and add_layout. - Restore the owner-handle guard on the sheet-viewport finder so a foreign layout's viewport can't be picked as the sheet viewport. Co-Authored-By: Claude Opus 4.8 --- src/app/update.rs | 3 + src/scene/mod.rs | 160 +++++++++++++++++++++++++++++++++++----------- 2 files changed, 124 insertions(+), 39 deletions(-) diff --git a/src/app/update.rs b/src/app/update.rs index 02bdf2b93..8af59488d 100644 --- a/src/app/update.rs +++ b/src/app/update.rs @@ -4254,6 +4254,9 @@ impl OpenCADStudio { } } self.tabs[i].scene.current_layout = new_name.clone(); + // Safety net — `add_layout` already creates the overall + // sheet viewport; this covers any path that doesn't. + self.tabs[i].scene.ensure_sheet_viewport(&new_name); self.tabs[i].scene.deselect_all(); self.tabs[i].scene.fit_all(); if let Some(idx) = self.ribbon.layout_module_index() { diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 24dcbff3c..84e6245fc 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -935,6 +935,85 @@ impl Scene { }).unwrap_or(Handle::NULL) } + /// Guarantee that a paper layout has its full-screen overall (`id == 1`) + /// sheet viewport. AutoCAD always writes one, and `add_layout` creates it, + /// but this is a safety net for layouts that arrive without it. The sheet + /// viewport is the authoritative paper-space view and the canvas every + /// floating viewport overlays. + pub fn ensure_sheet_viewport(&mut self, layout_name: &str) { + if layout_name == "Model" { + return; + } + // Locate the layout: its object handle, block-record handle, current + // sheet-viewport link, and paper limits. + let info = self.document.objects.iter().find_map(|(h, obj)| { + if let ObjectType::Layout(l) = obj { + if l.name == layout_name { + return Some((*h, l.block_record, l.viewport, l.min_limits, l.max_limits)); + } + } + None + }); + let Some((layout_handle, block_record, cur_vp, min_lim, max_lim)) = info else { + return; + }; + if block_record.is_null() { + return; + } + + // Already present? Accept either the linked viewport handle or any + // `id == 1` viewport owned by the layout block. + let has_sheet = self.document.entities().any(|e| { + matches!(e, EntityType::Viewport(vp) + if vp.common.owner_handle == block_record + && (vp.id == 1 || vp.common.handle == cur_vp)) + }); + if has_sheet { + // Keep the layout's link in sync if it was missing. + if !cur_vp.is_valid() { + let h = self.document.entities().find_map(|e| match e { + EntityType::Viewport(vp) + if vp.common.owner_handle == block_record && vp.id == 1 => + { + Some(vp.common.handle) + } + _ => None, + }); + if let Some(h) = h { + if let Some(ObjectType::Layout(l)) = + self.document.objects.get_mut(&layout_handle) + { + l.viewport = h; + } + } + } + return; + } + + // Create the full-screen overall viewport covering the paper limits. + let pw = (max_lim.0 - min_lim.0).abs().max(1.0); + let ph = (max_lim.1 - min_lim.1).abs().max(1.0); + let mut vp = acadrust::entities::Viewport::new(); + vp.id = 1; + vp.status = acadrust::entities::ViewportStatusFlags::default_on(); + // Paper plane is (x, z) with y = 0 (same convention MVIEW uses). + vp.center = acadrust::types::Vector3::new( + (min_lim.0 + max_lim.0) / 2.0, + 0.0, + (min_lim.1 + max_lim.1) / 2.0, + ); + vp.width = pw; + vp.height = ph; + if let Ok(handle) = + self.document + .add_entity_to_layout(EntityType::Viewport(vp), layout_name) + { + if let Some(ObjectType::Layout(l)) = self.document.objects.get_mut(&layout_handle) { + l.viewport = handle; + } + } + } + fn is_content_viewport_in_layout( &self, vp: &acadrust::entities::Viewport, @@ -4809,44 +4888,45 @@ impl Scene { /// Set the paper-space camera from the sheet viewport's stored view. /// Returns true if a valid sheet viewport was found and the camera was set. + /// + /// The sheet viewport entity is the authoritative paper-space view (it + /// round-trips through both the DXF and DWG writers). An older + /// `OpenCADStudio_Camera_` named View is honoured only as a + /// backward-compatible fallback for files saved under the previous scheme. fn apply_sheet_viewport_camera(&mut self) -> bool { - // Prefer our named View entry — survives DWG save without being overridden. - let view_name = format!("OpenCADStudio_Camera_{}", self.current_layout); - let saved_view = self - .document - .views - .iter() - .find(|v| v.name == view_name) - .cloned(); - if let Some(view) = saved_view { - return self.apply_camera_from_view_entry(&view, false); - } - let layout_block = self.current_layout_block_handle(); - if layout_block.is_null() { - return false; - } - - let sheet_vp = self - .document - .entities() - .filter_map(|e| { - if let EntityType::Viewport(vp) = e { - Some(vp) - } else { - None - } - }) -.find(|vp| !self.is_content_viewport_in_layout(vp, layout_block)); - - let vp = match sheet_vp { - Some(v) => v, - None => return false, + let sheet_vp = if layout_block.is_null() { + None + } else { + self.document + .entities() + .filter_map(|e| { + if let EntityType::Viewport(vp) = e { + Some(vp) + } else { + None + } + }) + .find(|vp| { + vp.common.owner_handle == layout_block + && !self.is_content_viewport_in_layout(vp, layout_block) + }) + .cloned() }; - if vp.view_height.abs() < 1e-9 { - return false; - } + let vp = match sheet_vp { + Some(v) if v.view_height.abs() >= 1e-9 => v, + _ => { + // Back-compat: files OCS saved with the named-View side-channel. + let view_name = format!("OpenCADStudio_Camera_{}", self.current_layout); + let fallback = + self.document.views.iter().find(|v| v.name == view_name).cloned(); + if let Some(view) = fallback { + return self.apply_camera_from_view_entry(&view, false); + } + return false; + } + }; let vd = glam::Vec3::new( vp.view_direction.x as f32, @@ -4938,9 +5018,9 @@ impl Scene { y: cam.target.y as f64, z: cam.target.z as f64, }; - let view_name = format!("OpenCADStudio_Camera_{}", self.current_layout); - // Write back to the sheet viewport entity. + // The sheet viewport entity is the authoritative paper-space view; + // it round-trips natively, so no named-View side-channel is needed. let layout_block = self.current_layout_block_handle(); if !layout_block.is_null() { let sheet_handle = self @@ -4967,9 +5047,6 @@ impl Scene { } } } - - // Also write to View table — survives DWG save without override. - self.write_camera_view_entry(&view_name, target_wcs, vd3, view_height); true } } @@ -5006,6 +5083,10 @@ impl Scene { // Single-tile files fall through to the *Active branch. self.restore_model_tiles_from_vports() || self.apply_active_vport_camera() } else { + // Every paper layout has a full-screen sheet viewport that holds + // its view; create one if a loaded file lacks it. + let layout = self.current_layout.clone(); + self.ensure_sheet_viewport(&layout); self.apply_sheet_viewport_camera() }; if !restored { @@ -7012,3 +7093,4 @@ fn is_unindexable_entity(e: &acadrust::EntityType) -> bool { E::Insert(_) | E::Viewport(_) | E::Block(_) | E::BlockEnd(_) ) } +