fix(view): correct 3D mesh interaction

Sync projection controls from the active camera, preserve the UCS icon in perspective views, and use visible or selected 3D geometry for orbit pivots. Dense mesh vertex grips remain hidden.
This commit is contained in:
Hakan Seven committed 2026-08-12 19:49:52 +03:00
1 parent 1045283082
commit 0df2356243
6 files changed
+136 -80

No files matched your search

+63 -12
View File
@@ -7878,6 +7878,34 @@ impl Scene {
)
}
pub fn view_center_surface_pivot(
&self,
bounds: iced::Rectangle,
) -> Option<glam::DVec3> {
let camera = self.camera.borrow();
let view_rot = camera.view_proj_rte(bounds);
let eye = camera.eye();
drop(camera);
let meshes = self.interaction_meshes_arc();
let center = iced::Point::new(bounds.width * 0.5, bounds.height * 0.5);
pick::hit_test::mesh_click_point(
center,
meshes.iter().filter_map(|set| {
let handle = set.entity_handle()?;
let mesh = set.geometry_lods().first()?;
Some((
handle,
mesh,
set.instance_transform,
mesh_interaction_aabb(set)?,
))
}),
view_rot,
eye,
bounds,
)
}
/// Hover may use the coarsest cached solid LOD. Click selection keeps the
/// full-resolution mesh, while rollover only needs a stable parent handle
/// and must stay within an interactive frame budget.
@@ -9061,8 +9089,8 @@ impl Scene {
/// The AABB centre of the current selection, in absolute world coordinates
/// (same space as `Camera::target`) — the point the 3D view orbits around
/// when something is selected. `None` when nothing is selected; the caller
/// then orbits about the point under the cursor. (#229)
/// when something is selected. `None` keeps the current camera target as
/// the orbit centre. (#229)
pub fn orbit_pivot(&self) -> Option<glam::DVec3> {
if self.selected.is_empty() {
return None;
@@ -9086,9 +9114,16 @@ impl Scene {
self.annotation_all_visible(),
None,
);
let mut min = glam::DVec2::splat(f64::INFINITY);
let mut max = glam::DVec2::splat(f64::NEG_INFINITY);
let mut min = glam::DVec3::splat(f64::INFINITY);
let mut max = glam::DVec3::splat(f64::NEG_INFINITY);
let mut any = false;
let mut include = |point: glam::DVec3| {
if point.is_finite() {
min = min.min(point);
max = max.max(point);
any = true;
}
};
for wire in &wires {
let Some(h) = Self::handle_from_wire_name(&wire.name) else {
continue;
@@ -9096,17 +9131,33 @@ impl Scene {
if !self.selected.contains(&h) {
continue;
}
for &[x, y, _] in &wire.points {
if x.is_finite() && y.is_finite() {
min = min.min(glam::DVec2::new(x as f64, y as f64));
max = max.max(glam::DVec2::new(x as f64, y as f64));
any = true;
}
for (index, &[x, y, z]) in wire.points.iter().enumerate() {
let [lx, ly, lz] = wire
.points_low
.get(index)
.copied()
.unwrap_or([0.0; 3]);
include(glam::DVec3::new(
x as f64 + lx as f64,
y as f64 + ly as f64,
z as f64 + lz as f64,
));
}
}
for set in self.interaction_meshes_arc().iter() {
let Some(handle) = set.entity_handle() else {
continue;
};
if !self.selected.contains(&handle) {
continue;
}
if let Some(aabb) = mesh_interaction_aabb(set) {
include(glam::DVec3::new(aabb[0], aabb[1], aabb[2]));
include(glam::DVec3::new(aabb[3], aabb[4], aabb[5]));
}
}
if any {
let c = (min + max) * 0.5;
Some(glam::DVec3::new(c.x, c.y, 0.0))
Some((min + max) * 0.5)
} else {
None
}
+9
View File
@@ -446,6 +446,15 @@ impl Scene {
self.camera.borrow().rotation
}
pub fn active_camera_projection(&self) -> view::camera::Projection {
if let Some(handle) = self.active_viewport {
if let Some(camera) = self.camera_for_viewport(handle) {
return camera.projection;
}
}
self.camera.borrow().projection
}
/// View-rotation matrix for the active viewport (MSPACE), or the
/// paper-space camera's matrix when not in MSPACE.
/// Used by ViewCube hit-testing so clicks map to the correct camera.
+33 -13
View File
@@ -658,20 +658,40 @@ pub fn click_hits_all<'a, W: WireSource + ?Sized>(
hits.into_iter().map(|(_, name)| name).collect()
}
type MeshPickItem<'a> = (
Handle,
&'a MeshModel,
Option<acadrust::types::Transform>,
[f64; 6],
);
pub(crate) fn mesh_click_hit<'a>(
cursor: Point,
meshes: impl Iterator<
Item = (
Handle,
&'a MeshModel,
Option<acadrust::types::Transform>,
[f64; 6],
),
>,
meshes: impl Iterator<Item = MeshPickItem<'a>>,
view_rot: Mat4,
eye: glam::DVec3,
bounds: Rectangle,
) -> Option<Handle> {
mesh_click_result(cursor, meshes, view_rot, eye, bounds).map(|(_, handle, _)| handle)
}
pub(crate) fn mesh_click_point<'a>(
cursor: Point,
meshes: impl Iterator<Item = MeshPickItem<'a>>,
view_rot: Mat4,
eye: glam::DVec3,
bounds: Rectangle,
) -> Option<glam::DVec3> {
mesh_click_result(cursor, meshes, view_rot, eye, bounds).map(|(_, _, point)| point)
}
fn mesh_click_result<'a>(
cursor: Point,
meshes: impl Iterator<Item = MeshPickItem<'a>>,
view_rot: Mat4,
eye: glam::DVec3,
bounds: Rectangle,
) -> Option<(f64, Handle, glam::DVec3)> {
let profile = crate::perf::enabled().then(std::time::Instant::now);
let mut set_count = 0usize;
let mut bound_hits = 0usize;
@@ -692,7 +712,7 @@ pub(crate) fn mesh_click_hit<'a>(
{
return None;
}
let mut best: Option<(f64, Handle)> = None;
let mut best: Option<(f64, Handle, glam::DVec3)> = None;
for (handle, mesh, transform, aabb) in meshes {
set_count += 1;
let Some((near_t, _)) = ray_aabb(near, world_direction, aabb) else {
@@ -700,7 +720,7 @@ pub(crate) fn mesh_click_hit<'a>(
};
bound_hits += 1;
exact_triangles += mesh.indices.len() / 3;
if best.is_some_and(|(distance, _)| near_t > distance) {
if best.is_some_and(|(distance, _, _)| near_t > distance) {
continue;
}
let model = transform.map(codec_transform_matrix);
@@ -747,8 +767,8 @@ pub(crate) fn mesh_click_hit<'a>(
let local_hit = origin + direction * local_t;
let world_hit = model.map_or(local_hit, |model| model.transform_point3(local_hit));
let distance = (world_hit - near).dot(world_direction);
if distance >= 0.0 && best.is_none_or(|(current, _)| distance < current) {
best = Some((distance, handle));
if distance >= 0.0 && best.is_none_or(|(current, _, _)| distance < current) {
best = Some((distance, handle, world_hit));
}
}
}
@@ -761,7 +781,7 @@ pub(crate) fn mesh_click_hit<'a>(
exact_triangles,
);
}
best.map(|(_, h)| h)
best
}
fn codec_transform_matrix(transform: acadrust::types::Transform) -> glam::DMat4 {