forked from 1b-agent/opencadstudio-upstream
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:
1 parent
8050fc79fd
commit
00cc17ea55
10 files changed
+424
-96
No files matched your search
+101
-3
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 /
|
||||
|
||||
Reference in new issue
Block a user