graphic design tool
git clone https://git.lucas.co/cce-designer.git
fix: viewport scroll orbit moves the camera, not the geometry
The default-camera orbit angles built a model matrix, literally rotating
the geometry within world space while the camera stood still — visible
against the pivot marker (drawn without the model matrix) and in the
shading, which swung across surfaces as if the object turned under the
light. The angles now fold into the camera's orbit around the pivot
(subtracted, preserving the drag direction) and the model matrix is
identity: geometry stays stationary and the camera does the moving, same
as the scene-camera path. The pivot billboard's yaw follows the orbited
camera through one unified formula for both camera modes.
Co-Authored-By: Claude Fable 5 <[email protected]>
src/app.rs | 10 +++++-----
src/main.rs | 24 ++++++++++++++++++++++++
src/viewport_3d.rs | 13 ++++++++++---
3 files changed, 39 insertions(+), 8 deletions(-)
diff --git a/src/app.rs b/src/app.rs
index b57815d..673c2dd 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -5264,13 +5264,13 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
let (proj, view_mat, model) = self.viewport().get_matrices(aspect, Some(camera_pos), Some(Vec3::new(rx, ry, rz)), Some(pivot));
let mvp = (proj * view_mat * model).to_cols_array_2d();
- let cam_angle_y = camera_pos.x.atan2(camera_pos.z);
- let rot_angle = if self.active_camera != "Default Camera" {
+ // The camera's world yaw, matching get_matrices' fold: the scroll
+ // orbit now moves the camera (subtracted), not the model, so the
+ // billboard faces the orbited camera in both camera modes.
+ let rot_angle = {
let base_offset = camera_pos - pivot;
let yaw0 = base_offset.x.atan2(base_offset.z);
- ry.to_radians() + yaw0
- } else {
- self.viewport().rotation_y + cam_angle_y
+ ry.to_radians() + yaw0 - self.viewport().rotation_y
};
let model_pivot = Mat4::from_translation(pivot) * Mat4::from_rotation_y(rot_angle);
let mvp_pivot = (proj * view_mat * model_pivot).to_cols_array_2d();
diff --git a/src/main.rs b/src/main.rs
index afdaae6..0ad35a0 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -81,6 +81,30 @@ mod tests {
assert_eq!(proj.root.children[1].position, (4.0, 2.0));
}
+ #[test]
+ fn test_default_camera_orbit_moves_camera_not_geometry() {
+ use cce_ui::widget::WidgetHost;
+ use glam::{Mat4, Vec3};
+ let mut vp = crate::viewport_3d::Viewport3D::new();
+ let inner = vp.as_any_mut().downcast_mut::<crate::viewport_3d::Viewport3D>().unwrap();
+
+ let (_, view0, model0) = inner.get_matrices(1.0, None, None, None);
+ inner.rotation_y = 0.7;
+ inner.rotation_x = 0.2;
+ let (_, view1, model1) = inner.get_matrices(1.0, None, None, None);
+
+ // The scroll orbit must never rotate the geometry within world space.
+ assert_eq!(model0, Mat4::IDENTITY);
+ assert_eq!(model1, Mat4::IDENTITY);
+ // The camera moved...
+ assert!(view0 != view1);
+ // ...by orbiting: the pivot (look-at center) stays at the same eye-space
+ // point, and the camera keeps its distance from it.
+ let p0 = view0.transform_point3(Vec3::ZERO);
+ let p1 = view1.transform_point3(Vec3::ZERO);
+ assert!((p0 - p1).length() < 1e-4, "pivot drifted: {p0:?} vs {p1:?}");
+ }
+
#[test]
fn test_node_template_names() {
let templates_root = crate::app::load_fs_tree();
diff --git a/src/viewport_3d.rs b/src/viewport_3d.rs
index ac57fd8..5e7361f 100644
--- a/src/viewport_3d.rs
+++ b/src/viewport_3d.rs
@@ -122,15 +122,22 @@ impl Viewport3D {
let yaw0 = base_offset.x.atan2(base_offset.z);
let pitch0 = (base_offset.y / distance.max(1e-5)).asin();
- let total_ry = ry.to_radians() + yaw0;
- let total_rx = rx.to_radians() + pitch0;
+ // The default-camera scroll orbit (`rotation_x`/`rotation_y`) folds into
+ // the CAMERA's orbit around the pivot — subtracted, because moving the
+ // camera one way spins the view the way rotating the world the other way
+ // used to. The old path put these angles in the model matrix, which
+ // rotated the geometry within world space (visible against the pivot
+ // marker, and it swung the shading) instead of moving the camera.
+ let total_ry = ry.to_radians() + yaw0 - self.rotation_y;
+ let total_rx = rx.to_radians() + pitch0 - self.rotation_x;
let view_rot_pos = Mat4::from_rotation_y(total_ry) * Mat4::from_rotation_x(-total_rx);
let camera_up = view_rot_pos.transform_vector3(Vec3::Y);
let camera_world_pos = pivot + view_rot_pos.transform_vector3(Vec3::new(0.0, 0.0, distance) * self.zoom);
let view_mat = Mat4::from_rotation_z(rz.to_radians()) * Mat4::look_at_rh(camera_world_pos, pivot, camera_up);
- let model = Mat4::from_rotation_y(self.rotation_y) * Mat4::from_rotation_x(self.rotation_x);
+ // Geometry stays stationary in world space; the camera does the moving.
+ let model = Mat4::IDENTITY;
let proj = Mat4::perspective_rh(0.9, aspect, 0.1, 100.0);
(proj, view_mat, model)