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 <noreply@anthropic.com>
This commit is contained in:
parent
f147e384d0
commit
568422679d
2 changed files with 123 additions and 38 deletions
|
|
@ -4254,6 +4254,9 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.tabs[i].scene.current_layout = new_name.clone();
|
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.deselect_all();
|
||||||
self.tabs[i].scene.fit_all();
|
self.tabs[i].scene.fit_all();
|
||||||
if let Some(idx) = self.ribbon.layout_module_index() {
|
if let Some(idx) = self.ribbon.layout_module_index() {
|
||||||
|
|
|
||||||
140
src/scene/mod.rs
140
src/scene/mod.rs
|
|
@ -935,6 +935,85 @@ impl Scene {
|
||||||
}).unwrap_or(Handle::NULL)
|
}).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(
|
fn is_content_viewport_in_layout(
|
||||||
&self,
|
&self,
|
||||||
vp: &acadrust::entities::Viewport,
|
vp: &acadrust::entities::Viewport,
|
||||||
|
|
@ -4809,26 +4888,17 @@ impl Scene {
|
||||||
|
|
||||||
/// Set the paper-space camera from the sheet viewport's stored view.
|
/// 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.
|
/// 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_<layout>` 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 {
|
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();
|
let layout_block = self.current_layout_block_handle();
|
||||||
if layout_block.is_null() {
|
let sheet_vp = if layout_block.is_null() {
|
||||||
return false;
|
None
|
||||||
}
|
} else {
|
||||||
|
self.document
|
||||||
let sheet_vp = self
|
|
||||||
.document
|
|
||||||
.entities()
|
.entities()
|
||||||
.filter_map(|e| {
|
.filter_map(|e| {
|
||||||
if let EntityType::Viewport(vp) = e {
|
if let EntityType::Viewport(vp) = e {
|
||||||
|
|
@ -4837,16 +4907,26 @@ impl Scene {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.find(|vp| !self.is_content_viewport_in_layout(vp, layout_block));
|
.find(|vp| {
|
||||||
|
vp.common.owner_handle == layout_block
|
||||||
let vp = match sheet_vp {
|
&& !self.is_content_viewport_in_layout(vp, layout_block)
|
||||||
Some(v) => v,
|
})
|
||||||
None => return false,
|
.cloned()
|
||||||
};
|
};
|
||||||
|
|
||||||
if vp.view_height.abs() < 1e-9 {
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let vd = glam::Vec3::new(
|
let vd = glam::Vec3::new(
|
||||||
vp.view_direction.x as f32,
|
vp.view_direction.x as f32,
|
||||||
|
|
@ -4938,9 +5018,9 @@ impl Scene {
|
||||||
y: cam.target.y as f64,
|
y: cam.target.y as f64,
|
||||||
z: cam.target.z 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();
|
let layout_block = self.current_layout_block_handle();
|
||||||
if !layout_block.is_null() {
|
if !layout_block.is_null() {
|
||||||
let sheet_handle = self
|
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
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -5006,6 +5083,10 @@ impl Scene {
|
||||||
// Single-tile files fall through to the *Active branch.
|
// Single-tile files fall through to the *Active branch.
|
||||||
self.restore_model_tiles_from_vports() || self.apply_active_vport_camera()
|
self.restore_model_tiles_from_vports() || self.apply_active_vport_camera()
|
||||||
} else {
|
} 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()
|
self.apply_sheet_viewport_camera()
|
||||||
};
|
};
|
||||||
if !restored {
|
if !restored {
|
||||||
|
|
@ -7012,3 +7093,4 @@ fn is_unindexable_entity(e: &acadrust::EntityType) -> bool {
|
||||||
E::Insert(_) | E::Viewport(_) | E::Block(_) | E::BlockEnd(_)
|
E::Insert(_) | E::Viewport(_) | E::Block(_) | E::BlockEnd(_)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue