fix(layout): identify sheet viewports reliably
This commit is contained in:
parent
8c61d89b1e
commit
bbd58eb9f2
7 changed files with 123 additions and 130 deletions
|
|
@ -827,30 +827,8 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// A viewport frames the model, so its framing is measured in model
|
// Model framing follows model units; sheet framing stays in paper units.
|
||||||
// units and has to follow them. Left alone, every layout would suddenly
|
let sheets = self.tabs[i].scene.sheet_viewport_handles();
|
||||||
// look at a region a thousand times too large. The paper side of the
|
|
||||||
// ratio is untouched — the sheet is still the same sheet — so the ratio
|
|
||||||
// itself moves the other way.
|
|
||||||
//
|
|
||||||
// `transform_viewport` covers the rectangle and the target but not the
|
|
||||||
// framing, so the framing is done here for every viewport, and the
|
|
||||||
// target only for the ones the scale above did not already reach.
|
|
||||||
// Not every viewport frames the model, though. Each layout's sheet
|
|
||||||
// viewport frames the sheet — its view is the paper, measured in paper
|
|
||||||
// units — so scaling it by the model's factor would leave the drawing
|
|
||||||
// frame the same size as before while the view of it changed by a
|
|
||||||
// thousand, which is the boundary appearing to break.
|
|
||||||
let sheets: std::collections::HashSet<acadrust::Handle> = self.tabs[i]
|
|
||||||
.scene
|
|
||||||
.document
|
|
||||||
.objects
|
|
||||||
.values()
|
|
||||||
.filter_map(|object| match object {
|
|
||||||
acadrust::objects::ObjectType::Layout(layout) => Some(layout.viewport),
|
|
||||||
_ => None,
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
let scaled: std::collections::HashSet<acadrust::Handle> = handles.iter().copied().collect();
|
let scaled: std::collections::HashSet<acadrust::Handle> = handles.iter().copied().collect();
|
||||||
let mut reframed = 0usize;
|
let mut reframed = 0usize;
|
||||||
for entity in self.tabs[i].scene.document.entities_mut() {
|
for entity in self.tabs[i].scene.document.entities_mut() {
|
||||||
|
|
@ -858,14 +836,7 @@ impl OpenCADStudio {
|
||||||
let acadrust::entities::EntityType::Viewport(vp) = entity else {
|
let acadrust::entities::EntityType::Viewport(vp) = entity else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
// Only a paper-space viewport can be a sheet — the scale above
|
let is_sheet = !scaled.contains(&handle) && sheets.contains(&handle);
|
||||||
// reached everything in model space, and what it reached frames the
|
|
||||||
// model by definition. Among the rest, the layout names its sheet
|
|
||||||
// outright, and a file that arrives without that link still gives
|
|
||||||
// itself away by sitting at the paper origin, where only the sheet
|
|
||||||
// sits.
|
|
||||||
let is_sheet = !scaled.contains(&handle)
|
|
||||||
&& (sheets.contains(&handle) || !crate::scene::Scene::is_content_viewport(vp));
|
|
||||||
if is_sheet {
|
if is_sheet {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -204,7 +204,14 @@ fn plot_scene_content(
|
||||||
|| !crate::scene::Scene::handle_from_wire_name(&wire.name)
|
|| !crate::scene::Scene::handle_from_wire_name(&wire.name)
|
||||||
.and_then(|handle| scene.document.get_entity(handle))
|
.and_then(|handle| scene.document.get_entity(handle))
|
||||||
.is_some_and(|entity| {
|
.is_some_and(|entity| {
|
||||||
matches!(entity, acadrust::EntityType::Viewport(viewport) if crate::scene::Scene::is_content_viewport(viewport))
|
matches!(
|
||||||
|
entity,
|
||||||
|
acadrust::EntityType::Viewport(viewport)
|
||||||
|
if !crate::scene::Scene::is_sheet_viewport(
|
||||||
|
&scene.document,
|
||||||
|
viewport,
|
||||||
|
)
|
||||||
|
)
|
||||||
}))
|
}))
|
||||||
});
|
});
|
||||||
model_wires.retain(|wire| wire.plot_visible);
|
model_wires.retain(|wire| wire.plot_visible);
|
||||||
|
|
|
||||||
|
|
@ -4620,7 +4620,10 @@ impl OpenCADStudio {
|
||||||
if let Some(AcadEntityType::Viewport(vp)) =
|
if let Some(AcadEntityType::Viewport(vp)) =
|
||||||
self.tabs[i].scene.document.get_entity(h)
|
self.tabs[i].scene.document.get_entity(h)
|
||||||
{
|
{
|
||||||
if Scene::is_content_viewport(vp) {
|
if !Scene::is_sheet_viewport(
|
||||||
|
&self.tabs[i].scene.document,
|
||||||
|
vp,
|
||||||
|
) {
|
||||||
Some(h)
|
Some(h)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
|
|
||||||
|
|
@ -527,7 +527,7 @@ fn tessellate_entity_inner(
|
||||||
if let EntityType::Viewport(vp) = e {
|
if let EntityType::Viewport(vp) = e {
|
||||||
// The sheet viewport (overall/id=1) is never shown — it represents the
|
// The sheet viewport (overall/id=1) is never shown — it represents the
|
||||||
// paper boundary, not a user-defined content window.
|
// paper boundary, not a user-defined content window.
|
||||||
if !Scene::is_content_viewport(vp) {
|
if Scene::is_sheet_viewport(document, vp) {
|
||||||
return vec![];
|
return vec![];
|
||||||
}
|
}
|
||||||
let is_active = active_viewport == Some(h);
|
let is_active = active_viewport == Some(h);
|
||||||
|
|
|
||||||
|
|
@ -1718,24 +1718,9 @@ pub fn tessellate(
|
||||||
// f64 for the WireModel's double-single-era snap buffer.
|
// f64 for the WireModel's double-single-era snap buffer.
|
||||||
let snap_pts: Vec<(glam::DVec3, SnapHint)> =
|
let snap_pts: Vec<(glam::DVec3, SnapHint)> =
|
||||||
snap_pts.into_iter().map(|(p, h)| (p.as_dvec3(), h)).collect();
|
snap_pts.into_iter().map(|(p, h)| (p.as_dvec3(), h)).collect();
|
||||||
// A paper-space viewport is a window, not a wireframe: give it an interior
|
// Paper viewports are pickable inside their frames; the sheet viewport is not.
|
||||||
// pick surface so a click anywhere inside the frame selects it. Ranked
|
|
||||||
// below edge and fill hits, so content drawn inside still wins the click.
|
|
||||||
// The sheet ("overall") viewport is the layout's own invisible camera
|
|
||||||
// frame covering the whole page — never pickable, or it would swallow
|
|
||||||
// every click over the real viewports beneath it. It is identified by the
|
|
||||||
// Layout object's viewport link (authoritative — DWG files carry id = 0
|
|
||||||
// and this file class centres the sheet viewport off-origin, so neither
|
|
||||||
// the id nor the geometry heuristic alone is reliable), with
|
|
||||||
// `is_content_viewport` as the fallback classifier.
|
|
||||||
let is_sheet_vp = |vp: &acadrust::entities::Viewport| {
|
|
||||||
let h = vp.common.handle;
|
|
||||||
document.objects.values().any(|obj| {
|
|
||||||
matches!(obj, acadrust::objects::ObjectType::Layout(l) if l.viewport == h)
|
|
||||||
}) || !crate::scene::Scene::is_content_viewport(vp)
|
|
||||||
};
|
|
||||||
let (pick_tris, pick_tris_low) = match entity {
|
let (pick_tris, pick_tris_low) = match entity {
|
||||||
EntityType::Viewport(vp) if !is_sheet_vp(vp) => {
|
EntityType::Viewport(vp) if !crate::scene::Scene::is_sheet_viewport(document, vp) => {
|
||||||
if let Some(polygon) = clipped_viewport_polygon.as_ref() {
|
if let Some(polygon) = clipped_viewport_polygon.as_ref() {
|
||||||
points_to_ds(crate::entities::mesh::triangulate_planar(polygon))
|
points_to_ds(crate::entities::mesh::triangulate_planar(polygon))
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
183
src/scene/mod.rs
183
src/scene/mod.rs
|
|
@ -2998,32 +2998,96 @@ impl Scene {
|
||||||
crate::io::set_saved_active_layout(&mut self.document, &self.current_layout);
|
crate::io::set_saved_active_layout(&mut self.document, &self.current_layout);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns true if this viewport should display model-space content
|
pub(crate) fn layout_sheet_viewport_handle(
|
||||||
/// (i.e. it is a user viewport, not the sheet/overall viewport).
|
document: &acadrust::CadDocument,
|
||||||
///
|
layout: &acadrust::objects::Layout,
|
||||||
/// Rules:
|
) -> Handle {
|
||||||
/// - id=1 → always the sheet viewport → false
|
let owned_viewport = |handle| match document.get_entity(handle) {
|
||||||
/// - id≥2 → always a user viewport → true
|
Some(EntityType::Viewport(vp)) if vp.common.owner_handle == layout.block_record => {
|
||||||
/// - id=0 or id<0 (DWG reader omits the id; some DXF exporters write -1):
|
Some(vp)
|
||||||
/// use geometry: the sheet viewport is centred at the paper origin (0,0)
|
}
|
||||||
/// with scale≈1.0 (view_height ≈ paper-space height).
|
_ => None,
|
||||||
pub fn is_content_viewport(vp: &acadrust::entities::Viewport) -> bool {
|
};
|
||||||
if vp.id == 1 {
|
|
||||||
return false;
|
let listed = layout
|
||||||
|
.viewports
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.filter_map(|handle| owned_viewport(handle).map(|vp| (handle, vp)));
|
||||||
|
if let Some((handle, _)) = listed.clone().find(|(_, vp)| vp.id == 1) {
|
||||||
|
return handle;
|
||||||
}
|
}
|
||||||
if vp.id > 1 {
|
if let Some((handle, _)) = listed.into_iter().next() {
|
||||||
|
return handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
let block_handles = document
|
||||||
|
.block_records
|
||||||
|
.iter()
|
||||||
|
.find(|block| block.handle == layout.block_record)
|
||||||
|
.map(|block| block.entity_handles.as_slice())
|
||||||
|
.unwrap_or_default();
|
||||||
|
let block_viewports = block_handles
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.filter_map(|handle| owned_viewport(handle).map(|vp| (handle, vp)));
|
||||||
|
if let Some((handle, _)) = block_viewports.clone().find(|(_, vp)| vp.id == 1) {
|
||||||
|
return handle;
|
||||||
|
}
|
||||||
|
if let Some((handle, _)) = block_viewports.into_iter().next() {
|
||||||
|
return handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
document
|
||||||
|
.entities()
|
||||||
|
.filter_map(|entity| match entity {
|
||||||
|
EntityType::Viewport(vp) if vp.common.owner_handle == layout.block_record => {
|
||||||
|
Some((
|
||||||
|
vp.common.handle,
|
||||||
|
vp.id == 1,
|
||||||
|
vp.width.abs() * vp.height.abs(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.max_by(|a, b| a.1.cmp(&b.1).then_with(|| a.2.total_cmp(&b.2)))
|
||||||
|
.map(|(handle, _, _)| handle)
|
||||||
|
.unwrap_or(Handle::NULL)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_sheet_viewport(
|
||||||
|
document: &acadrust::CadDocument,
|
||||||
|
vp: &acadrust::entities::Viewport,
|
||||||
|
) -> bool {
|
||||||
|
if vp.id == 1 {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// id ≤ 0: DWG files never write group-code 69 (viewport id), so all
|
if vp.id > 1 {
|
||||||
// viewports arrive with id=0.
|
return false;
|
||||||
//
|
}
|
||||||
// In DWG format the sheet ("overall") viewport always has its center at
|
document.objects.values().any(|object| {
|
||||||
// the paper-space origin (0, 0). Content viewports are placed at their
|
matches!(
|
||||||
// actual position on the paper and therefore have a non-zero center.
|
object,
|
||||||
// Using center position is more reliable than a scale heuristic because
|
ObjectType::Layout(layout)
|
||||||
// the sheet viewport's scale is not always exactly 1:1 (observed: 0.8965
|
if layout.block_record == vp.common.owner_handle
|
||||||
// in real-world files, which the old 0.02 tolerance missed entirely).
|
&& Self::layout_sheet_viewport_handle(document, layout)
|
||||||
vp.center.x.abs() >= 0.5 || vp.center.y.abs() >= 0.5
|
== vp.common.handle
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn sheet_viewport_handles(&self) -> std::collections::HashSet<Handle> {
|
||||||
|
self.document
|
||||||
|
.objects
|
||||||
|
.values()
|
||||||
|
.filter_map(|object| match object {
|
||||||
|
ObjectType::Layout(layout) => {
|
||||||
|
let handle = Self::layout_sheet_viewport_handle(&self.document, layout);
|
||||||
|
handle.is_valid().then_some(handle)
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn current_layout_sheet_viewport_handle(&self) -> Handle {
|
fn current_layout_sheet_viewport_handle(&self) -> Handle {
|
||||||
|
|
@ -3035,7 +3099,7 @@ impl Scene {
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
if layout.name == self.current_layout {
|
if layout.name == self.current_layout {
|
||||||
Some(layout.viewport)
|
Some(Self::layout_sheet_viewport_handle(&self.document, layout))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
@ -3043,82 +3107,49 @@ impl Scene {
|
||||||
.unwrap_or(Handle::NULL)
|
.unwrap_or(Handle::NULL)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Guarantee that a paper layout has its full-screen overall (`id == 1`)
|
/// Ensure a paper layout has its overall (`id == 1`) viewport.
|
||||||
/// sheet viewport. `add_layout` creates it; 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) {
|
pub fn ensure_sheet_viewport(&mut self, layout_name: &str) {
|
||||||
if layout_name == "Model" {
|
if layout_name == "Model" {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Locate the layout: its object handle, block-record handle, current
|
// Locate the layout and its paper limits.
|
||||||
// sheet-viewport link, and paper limits.
|
|
||||||
let info = self.document.objects.iter().find_map(|(h, obj)| {
|
let info = self.document.objects.iter().find_map(|(h, obj)| {
|
||||||
if let ObjectType::Layout(l) = obj {
|
if let ObjectType::Layout(l) = obj {
|
||||||
if l.name == layout_name {
|
if l.name == layout_name {
|
||||||
return Some((*h, l.block_record, l.viewport, l.min_limits, l.max_limits));
|
return Some((
|
||||||
|
*h,
|
||||||
|
l.block_record,
|
||||||
|
Self::layout_sheet_viewport_handle(&self.document, l),
|
||||||
|
l.min_limits,
|
||||||
|
l.max_limits,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
});
|
});
|
||||||
let Some((layout_handle, block_record, cur_vp, min_lim, max_lim)) = info else {
|
let Some((layout_handle, block_record, sheet, min_lim, max_lim)) = info else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
if block_record.is_null() {
|
if block_record.is_null() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normal files carry a valid direct Layout→Viewport link. This O(1)
|
if sheet.is_valid()
|
||||||
// path is hit on every ordinary layout-tab switch.
|
|
||||||
if cur_vp.is_valid()
|
|
||||||
&& matches!(
|
&& matches!(
|
||||||
self.document.get_entity(cur_vp),
|
self.document.get_entity(sheet),
|
||||||
Some(EntityType::Viewport(vp)) if vp.common.owner_handle == block_record
|
Some(EntityType::Viewport(vp)) if vp.common.owner_handle == block_record
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
return;
|
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.
|
// Create the full-screen overall viewport covering the paper limits.
|
||||||
let pw = (max_lim.0 - min_lim.0).abs().max(1.0);
|
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 ph = (max_lim.1 - min_lim.1).abs().max(1.0);
|
||||||
let mut vp = acadrust::entities::Viewport::new();
|
let mut vp = acadrust::entities::Viewport::new();
|
||||||
vp.id = 1;
|
vp.id = 1;
|
||||||
vp.status = acadrust::entities::ViewportStatusFlags::default_on();
|
vp.status = acadrust::entities::ViewportStatusFlags::default_on();
|
||||||
// Paper-space center is a 2D (x, y) point with z = 0. Putting the
|
// Paper-space center is an (x, y) point with z = 0.
|
||||||
// paper-height midpoint in z
|
|
||||||
// (with y = 0) left the sheet view centered at y = 0, shifting the whole
|
|
||||||
// layout half a page down. See issue #156.
|
|
||||||
vp.center = acadrust::types::Vector3::new(
|
vp.center = acadrust::types::Vector3::new(
|
||||||
(min_lim.0 + max_lim.0) / 2.0,
|
(min_lim.0 + max_lim.0) / 2.0,
|
||||||
(min_lim.1 + max_lim.1) / 2.0,
|
(min_lim.1 + max_lim.1) / 2.0,
|
||||||
|
|
@ -3126,12 +3157,7 @@ impl Scene {
|
||||||
);
|
);
|
||||||
vp.width = pw;
|
vp.width = pw;
|
||||||
vp.height = ph;
|
vp.height = ph;
|
||||||
// Frame the new layout on the whole sheet: look straight down at the
|
// Frame the full sheet with a small margin.
|
||||||
// paper centre with the visible height a touch taller than the page.
|
|
||||||
// Without this the viewport keeps `Viewport::new`'s default view
|
|
||||||
// (target 0,0 / height 210), so the first time a fresh drawing's
|
|
||||||
// layout is opened the camera sits on the paper's bottom-left corner
|
|
||||||
// instead of centring the sheet.
|
|
||||||
vp.view_target = acadrust::types::Vector3::new(
|
vp.view_target = acadrust::types::Vector3::new(
|
||||||
(min_lim.0 + max_lim.0) / 2.0,
|
(min_lim.0 + max_lim.0) / 2.0,
|
||||||
(min_lim.1 + max_lim.1) / 2.0,
|
(min_lim.1 + max_lim.1) / 2.0,
|
||||||
|
|
@ -3144,7 +3170,8 @@ impl Scene {
|
||||||
.add_entity_to_layout(EntityType::Viewport(vp), layout_name)
|
.add_entity_to_layout(EntityType::Viewport(vp), layout_name)
|
||||||
{
|
{
|
||||||
if let Some(ObjectType::Layout(l)) = self.document.objects.get_mut(&layout_handle) {
|
if let Some(ObjectType::Layout(l)) = self.document.objects.get_mut(&layout_handle) {
|
||||||
l.viewport = handle;
|
l.viewports.retain(|candidate| *candidate != handle);
|
||||||
|
l.viewports.insert(0, handle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3161,7 +3188,7 @@ impl Scene {
|
||||||
if sheet_handle.is_valid() {
|
if sheet_handle.is_valid() {
|
||||||
vp.common.handle != sheet_handle
|
vp.common.handle != sheet_handle
|
||||||
} else {
|
} else {
|
||||||
Self::is_content_viewport(vp)
|
!Self::is_sheet_viewport(&self.document, vp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ impl Scene {
|
||||||
&& if sheet.is_valid() {
|
&& if sheet.is_valid() {
|
||||||
handle != sheet
|
handle != sheet
|
||||||
} else {
|
} else {
|
||||||
Self::is_content_viewport(vp)
|
!Self::is_sheet_viewport(&self.document, vp)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let content = if let Some(block) = self
|
let content = if let Some(block) = self
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue