forked from 1b-agent/opencadstudio-upstream
feat(view): complete ZOOM options
Add command-line navigation modes and persistent wheel controls.\n\nRefs #412
This commit is contained in:
1 parent
3f103d2f8b
commit
adb8ced8b5
17 files changed
+380
-23
No files matched your search
+154
-2
@@ -3,7 +3,10 @@
|
||||
// hit-testing lives in `scene::pick::hit_test`.)
|
||||
use super::*;
|
||||
|
||||
fn rendered_wire_center(wires: &[WireModel], fallback_z: f64) -> Option<glam::DVec3> {
|
||||
fn rendered_wire_bounds(
|
||||
wires: &[WireModel],
|
||||
fallback_z: f64,
|
||||
) -> Option<(glam::DVec3, glam::DVec3)> {
|
||||
let mut min = glam::DVec3::splat(f64::INFINITY);
|
||||
let mut max = glam::DVec3::splat(f64::NEG_INFINITY);
|
||||
let mut include = |point: glam::DVec3| {
|
||||
@@ -55,12 +58,16 @@ fn rendered_wire_center(wires: &[WireModel], fallback_z: f64) -> Option<glam::DV
|
||||
}
|
||||
|
||||
if min.is_finite() && max.is_finite() {
|
||||
Some((min + max) * 0.5)
|
||||
Some((min, max))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn rendered_wire_center(wires: &[WireModel], fallback_z: f64) -> Option<glam::DVec3> {
|
||||
rendered_wire_bounds(wires, fallback_z).map(|(min, max)| (min + max) * 0.5)
|
||||
}
|
||||
|
||||
fn block_entity_transform(
|
||||
document: &CadDocument,
|
||||
block_name: &str,
|
||||
@@ -189,6 +196,105 @@ impl Scene {
|
||||
self.camera_generation += 1;
|
||||
}
|
||||
|
||||
/// Save the active view immediately before a navigation operation. The
|
||||
/// paper-space camera and an entered floating viewport use different state,
|
||||
/// so the snapshot records whichever one currently owns navigation.
|
||||
pub fn remember_current_view(&mut self) {
|
||||
let layout = self.current_layout.clone();
|
||||
if let Some(handle) = self.active_viewport {
|
||||
if let Some(EntityType::Viewport(viewport)) = self.document.get_entity(handle) {
|
||||
self.previous_view = Some(ViewSnapshot::Floating {
|
||||
layout,
|
||||
handle,
|
||||
viewport: Box::new(viewport.clone()),
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
self.previous_view = Some(ViewSnapshot::Main {
|
||||
layout,
|
||||
tile: self.active_model_tile.get(),
|
||||
camera: self.camera.borrow().clone(),
|
||||
});
|
||||
}
|
||||
|
||||
/// Swap the active view with the most recently remembered one. Keeping the
|
||||
/// displaced current view makes repeated ZOOM Previous calls toggle cleanly
|
||||
/// between the last two views.
|
||||
pub fn restore_previous_view(&mut self) -> bool {
|
||||
let Some(snapshot) = self.previous_view.take() else {
|
||||
return false;
|
||||
};
|
||||
match snapshot {
|
||||
ViewSnapshot::Main {
|
||||
layout,
|
||||
tile,
|
||||
camera,
|
||||
} if self.active_viewport.is_none()
|
||||
&& self.current_layout == layout
|
||||
&& (layout != "Model" || self.active_model_tile.get() == tile) =>
|
||||
{
|
||||
let current = ViewSnapshot::Main {
|
||||
layout,
|
||||
tile,
|
||||
camera: self.camera.borrow().clone(),
|
||||
};
|
||||
*self.camera.borrow_mut() = camera;
|
||||
self.previous_view = Some(current);
|
||||
self.camera_generation += 1;
|
||||
true
|
||||
}
|
||||
ViewSnapshot::Floating {
|
||||
layout,
|
||||
handle,
|
||||
viewport,
|
||||
} if self.current_layout == layout && self.active_viewport == Some(handle) => {
|
||||
let Some(EntityType::Viewport(current)) = self.document.get_entity(handle) else {
|
||||
self.previous_view = Some(ViewSnapshot::Floating {
|
||||
layout,
|
||||
handle,
|
||||
viewport,
|
||||
});
|
||||
return false;
|
||||
};
|
||||
if current.status.locked {
|
||||
self.previous_view = Some(ViewSnapshot::Floating {
|
||||
layout,
|
||||
handle,
|
||||
viewport,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
let current = Box::new(current.clone());
|
||||
if let Some(EntityType::Viewport(destination)) =
|
||||
self.document.get_entity_mut(handle)
|
||||
{
|
||||
destination.view_center = viewport.view_center;
|
||||
destination.view_direction = viewport.view_direction;
|
||||
destination.view_target = viewport.view_target;
|
||||
destination.lens_length = viewport.lens_length;
|
||||
destination.front_clip_z = viewport.front_clip_z;
|
||||
destination.back_clip_z = viewport.back_clip_z;
|
||||
destination.view_height = viewport.view_height;
|
||||
destination.twist_angle = viewport.twist_angle;
|
||||
destination.custom_scale = viewport.custom_scale;
|
||||
destination.status.perspective = viewport.status.perspective;
|
||||
}
|
||||
self.previous_view = Some(ViewSnapshot::Floating {
|
||||
layout,
|
||||
handle,
|
||||
viewport: current,
|
||||
});
|
||||
self.camera_generation += 1;
|
||||
true
|
||||
}
|
||||
snapshot => {
|
||||
self.previous_view = Some(snapshot);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Refresh model bounds without moving the live camera. Reusing `fit_all`
|
||||
/// keeps projection framing on the same visibility and outlier filters as
|
||||
/// Zoom Extents instead of accepting every finite cached AABB.
|
||||
@@ -270,6 +376,52 @@ impl Scene {
|
||||
self.camera_generation += 1;
|
||||
}
|
||||
|
||||
/// Fit the active view to the rendered bounds of the supplied entities.
|
||||
/// Returns false when the selection has no finite visible geometry.
|
||||
pub fn zoom_to_entities(&mut self, handles: &[Handle]) -> bool {
|
||||
let fallback_z = self.active_camera_target().z;
|
||||
let wires = self.wire_models_for(handles);
|
||||
let mut bounds = rendered_wire_bounds(&wires, fallback_z);
|
||||
|
||||
for handle in handles {
|
||||
let Some(mesh) = self.meshes.get(handle) else {
|
||||
continue;
|
||||
};
|
||||
let [x0, y0, x1, y1] = mesh.world_aabb;
|
||||
let [z0, z1] = mesh.z_aabb;
|
||||
let mesh_min = glam::DVec3::new(x0 as f64, y0 as f64, z0 as f64);
|
||||
let mesh_max = glam::DVec3::new(x1 as f64, y1 as f64, z1 as f64);
|
||||
if !mesh_min.is_finite() || !mesh_max.is_finite() {
|
||||
continue;
|
||||
}
|
||||
bounds = Some(match bounds {
|
||||
Some((min, max)) => (min.min(mesh_min), max.max(mesh_max)),
|
||||
None => (mesh_min, mesh_max),
|
||||
});
|
||||
}
|
||||
|
||||
let Some((mut min, mut max)) = bounds else {
|
||||
return false;
|
||||
};
|
||||
if min == max {
|
||||
min -= glam::DVec3::splat(1.0);
|
||||
max += glam::DVec3::splat(1.0);
|
||||
}
|
||||
let min = min.as_vec3();
|
||||
let max = max.as_vec3();
|
||||
if !min.is_finite() || !max.is_finite() {
|
||||
return false;
|
||||
}
|
||||
if self.active_viewport.is_some() {
|
||||
return self.fit_active_viewport_to_bounds(min, max);
|
||||
}
|
||||
let aspect = self.active_camera_aspect();
|
||||
self.camera.borrow_mut().fit_to_bounds(min, max, aspect);
|
||||
self.projection_bounds_epoch.set(self.geometry_epoch);
|
||||
self.camera_generation += 1;
|
||||
true
|
||||
}
|
||||
|
||||
/// Centre the active camera on one rendered entity without changing zoom
|
||||
/// distance, orientation, or viewport scale.
|
||||
pub fn center_camera_on_entity(&mut self, handle: Handle) -> bool {
|
||||
|
||||
@@ -1123,6 +1123,20 @@ pub(crate) struct ModelTile {
|
||||
pub(crate) snap_on: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum ViewSnapshot {
|
||||
Main {
|
||||
layout: String,
|
||||
tile: usize,
|
||||
camera: Camera,
|
||||
},
|
||||
Floating {
|
||||
layout: String,
|
||||
handle: Handle,
|
||||
viewport: Box<acadrust::entities::Viewport>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Gap (pixels) between Model panes — the `pane_grid` spacing and the visible
|
||||
/// divider width. The renderer derives tile rects through this same spacing so
|
||||
/// the drawn viewports line up exactly with the pane_grid layout.
|
||||
@@ -1488,6 +1502,10 @@ struct SceneLight {
|
||||
|
||||
pub struct Scene {
|
||||
pub camera: Rc<RefCell<Camera>>,
|
||||
/// View saved immediately before the latest navigation operation. ZOOM
|
||||
/// Previous swaps with this snapshot, allowing the user to toggle between
|
||||
/// the two most recent views without touching drawing history.
|
||||
previous_view: Option<ViewSnapshot>,
|
||||
/// Model-space tiled viewport layout. One full-window tile by default;
|
||||
/// the split buttons / VPORTS subdivide the active tile.
|
||||
pub(crate) model_tiles: RefCell<Vec<ModelTile>>,
|
||||
@@ -1932,6 +1950,7 @@ impl Scene {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
camera: Rc::new(RefCell::new(Camera::default())),
|
||||
previous_view: None,
|
||||
model_tiles: RefCell::new(vec![ModelTile {
|
||||
rect: iced::Rectangle {
|
||||
x: 0.0,
|
||||
|
||||
Reference in new issue
Block a user