fix(scene): isolate drawing-space state

Cancel stale interaction state across space changes and keep live previews and zoom extents in their active paper/model context.\n\nRefs #539\nRefs #540\nRefs #541
This commit is contained in:
Hakan Seven committed 2026-07-28 18:51:20 +03:00
1 parent 8050fc79fd
commit 00cc17ea55
10 files changed
+424 -96

No files matched your search

+101 -3
View File
@@ -655,7 +655,108 @@ impl Scene {
self.camera.borrow_mut().fit_depth_to_bounds(min, max);
}
fn fit_paper_space_extents(&mut self) {
let layout_block = self.current_layout_block_handle();
let mut min = glam::Vec3::splat(f32::INFINITY);
let mut max = glam::Vec3::splat(f32::NEG_INFINITY);
{
let mut include = |x: f64, y: f64, z: f64| {
const SANE_EXTENT: f64 = 1.0e16;
if x.is_finite()
&& y.is_finite()
&& z.is_finite()
&& x.abs() < SANE_EXTENT
&& y.abs() < SANE_EXTENT
&& z.abs() < SANE_EXTENT
{
let point = glam::Vec3::new(x as f32, y as f32, z as f32);
min = min.min(point);
max = max.max(point);
}
};
// The physical sheet is always part of Paper Space extents, even
// when the layout contains no entities (#539).
if let Some(((x0, y0), (x1, y1))) = self.paper_limits() {
include(x0, y0, 0.0);
include(x1, y1, 0.0);
}
// Paper entities and viewport borders belong to the sheet. Model
// content projected through those viewports deliberately does not.
for wire in self.wires_for_block_culled(layout_block, None, None, None, None) {
let is_infinite = Self::handle_from_wire_name(&wire.name)
.and_then(|handle| self.document.get_entity(handle))
.is_some_and(|entity| {
matches!(entity, EntityType::XLine(_) | EntityType::Ray(_))
});
if is_infinite {
for &[x, y, z] in &wire.key_vertices {
include(x, y, z);
}
} else {
for (index, &[x, y, z]) in wire.points.iter().enumerate() {
let low = wire.points_low.get(index).copied().unwrap_or([0.0; 3]);
include(
x as f64 + low[0] as f64,
y as f64 + low[1] as f64,
z as f64 + low[2] as f64,
);
}
}
}
// Hatches, wipeouts and raster images are GPU-only and may carry
// no normal wire outline, so include their visible paper bounds.
let (hatches, wipeouts, images) = self.paper_sheet_render_models();
for hatch in hatches.iter().chain(wipeouts.iter()) {
for &[x, y] in hatch.boundary.iter() {
include(
hatch.world_origin[0] + x as f64,
hatch.world_origin[1] + y as f64,
0.0,
);
}
}
for image in images.iter() {
for (corner, low) in image.corners.iter().zip(image.corners_low.iter()) {
include(
corner[0] as f64 + low[0] as f64,
corner[1] as f64 + low[1] as f64,
corner[2] as f64 + low[2] as f64,
);
}
}
}
if !min.is_finite() || !max.is_finite() {
return;
}
if min == max {
max += glam::Vec3::splat(1.0);
}
self.camera
.borrow_mut()
.fit_to_bounds(min, max, self.last_render_aspect.get().max(0.01));
self.camera_generation += 1;
}
pub fn fit_all(&mut self) {
if self.active_viewport.is_some() {
if let Some((mut min, mut max)) = self.model_space_extents() {
if min == max {
max += glam::Vec3::splat(1.0);
min -= glam::Vec3::splat(1.0);
}
self.fit_active_viewport_to_bounds(min, max);
}
return;
}
if self.current_layout != "Model" {
self.fit_paper_space_extents();
return;
}
// Use the FULL, un-culled wire set — not `entity_wires()`, which is
// frustum-culled to the current view. Culled input would fit only the
// entities already on screen, so each call would zoom out a little and
@@ -664,9 +765,6 @@ impl Scene {
// the bounds don't drift with zoom-adaptive curve sampling.
let layout_block = self.current_layout_block_handle();
let mut wires = self.wires_for_block_culled(layout_block, None, None, None, None);
if self.current_layout != "Model" {
wires.extend(self.viewport_content_wires(layout_block, None, None));
}
// Ray / XLine tessellate as ±DISPLAY_EXTENT display segments
// (entities/ray.rs) — their endpoints are rendering artifacts, not
// drawing extent. A construction line through the drawing defeats
+2 -30
View File
@@ -132,36 +132,8 @@ impl Scene {
let max = glam::Vec3::new(limit_max.x as f32, limit_max.y as f32, 0.0);
// MSPACE owns a camera encoded on the active viewport entity.
if let Some(viewport_handle) = self.active_viewport {
let (width, height, locked) = match self.document.get_entity(viewport_handle) {
Some(EntityType::Viewport(viewport)) => {
(viewport.width, viewport.height, viewport.status.locked)
}
_ => return,
};
if locked {
return;
}
let aspect = (width / height.max(1e-9)) as f32;
let mut camera = match self.viewport_edit_frame(self.selection.borrow().vp_size) {
Some((camera, _)) => camera,
None => return,
};
camera.fit_to_bounds(min, max, aspect.max(0.01));
if let Some(EntityType::Viewport(viewport)) =
self.document.get_entity_mut(viewport_handle)
{
viewport.view_target.x = camera.target.x;
viewport.view_target.y = camera.target.y;
viewport.view_target.z = camera.target.z;
viewport.view_center.x = 0.0;
viewport.view_center.y = 0.0;
viewport.view_height = camera.ortho_size() as f64 * 2.0;
if viewport.view_height > 1e-9 {
viewport.custom_scale = viewport.height / viewport.view_height;
}
}
self.camera_generation += 1;
if self.active_viewport.is_some() {
self.fit_active_viewport_to_bounds(min, max);
return;
}
+40
View File
@@ -111,6 +111,46 @@ impl Scene {
}
}
/// Fit model-space bounds into the active floating viewport without moving
/// the surrounding paper-space camera.
pub fn fit_active_viewport_to_bounds(
&mut self,
min: glam::Vec3,
max: glam::Vec3,
) -> bool {
let Some(viewport_handle) = self.active_viewport else {
return false;
};
let (width, height, locked) = match self.document.get_entity(viewport_handle) {
Some(acadrust::EntityType::Viewport(viewport)) => {
(viewport.width, viewport.height, viewport.status.locked)
}
_ => return false,
};
if locked {
return false;
}
let Some(mut camera) = self.camera_for_viewport(viewport_handle) else {
return false;
};
camera.fit_to_bounds(min, max, (width / height.max(1e-9)) as f32);
if let Some(acadrust::EntityType::Viewport(viewport)) =
self.document.get_entity_mut(viewport_handle)
{
viewport.view_target.x = camera.target.x;
viewport.view_target.y = camera.target.y;
viewport.view_target.z = camera.target.z;
viewport.view_center.x = 0.0;
viewport.view_center.y = 0.0;
viewport.view_height = camera.ortho_size() as f64 * 2.0;
if viewport.view_height > 1e-9 {
viewport.custom_scale = viewport.height / viewport.view_height;
}
}
self.camera_generation += 1;
true
}
/// Pan the active viewport's model-space view by `(screen_dx, screen_dy)` pixels.
/// The delta is converted to model-space units using the camera and viewport scale.
/// No-op when there is no active viewport.
+18 -2
View File
@@ -1991,7 +1991,21 @@ impl Scene {
// per-frame buffer so the (potentially huge) base buffer stays resident
// and unchanged while a command preview or grip drag is live.
let all_wires = other_arc;
let preview_wires = if self.interim_wire.is_none() && self.preview_wires.is_empty() {
// A live overlay belongs to exactly one drawing context. In a paper
// layout, feeding model-space preview coordinates to the full-canvas
// sheet pass draws a second copy outside the floating viewport (#540).
// The inverse is equally wrong: paper-space coordinates must not be
// interpreted by a content viewport's model camera.
let show_live_overlay = if self.current_layout == "Model" {
inst.active
} else if let Some(active_viewport) = self.active_viewport {
!inst.paper_sheet && inst.handle == active_viewport
} else {
inst.paper_sheet
};
let preview_wires = if !show_live_overlay
|| (self.interim_wire.is_none() && self.preview_wires.is_empty())
{
Arc::new(Vec::new())
} else {
let mut v: Vec<WireModel> = Vec::with_capacity(self.preview_wires.len() + 1);
@@ -2150,7 +2164,9 @@ impl Scene {
pv.extend_from_slice(&w.text_verts);
}
}
pv.extend_from_slice(&self.preview_text);
if show_live_overlay {
pv.extend_from_slice(&self.preview_text);
}
Arc::new(pv)
};
// Stable per-viewport identity (tagged so tile / sheet / content /