git.lucas.co / cce-designer
graphic design tool
git clone https://git.lucas.co/cce-designer.git

commit928698ca02fc01cd423292f8fc088d77561afdd6
parenteee3ba04fd
authorLucas Galante <[email protected]>
date2026-07-30 08:13
fix: camera-pivot marker is world-fixed — the last camera-glued draw

The pivot marker's axis beams (camera_pivot_vertices, scaled by Camera
Pivot Size) were drawn under a yaw rotation tracking the camera. That
rotation existed to keep the marker glued to the old model-matrix orbit's
rotating world; once the orbit moved the camera instead, the beams became
the one draw still rotating WITH the camera — big RGB geometry visibly
spinning at the pivot over a stationary grid, reading as scene geometry
that won't stay put (they cross right under the sphere). The marker now
draws translation-only, world-fixed like the origin gizmo.

Also adds a scene-camera yaw regression test. Note for verification: the
default grid is invariant under 90-degree yaws and the default UV sphere
under 15-degree multiples — test orbits with asymmetric angles.

Co-Authored-By: Claude Fable 5 <[email protected]>

 src/app.rs  | 15 ++++++---------
 src/main.rs | 17 +++++++++++++++++
 2 files changed, 23 insertions(+), 9 deletions(-)

diff --git a/src/app.rs b/src/app.rs
index 8dcb7d2..c1f8335 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -5284,15 +5284,12 @@ 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();
 
-                    // 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 - self.viewport().rotation_y
-                    };
-                    let model_pivot = Mat4::from_translation(pivot) * Mat4::from_rotation_y(rot_angle);
+                    // The camera-pivot marker is WORLD-FIXED at the pivot point, like
+                    // the origin gizmo. Its old yaw rotation existed to keep it glued
+                    // to the model-matrix orbit's rotating world; with the camera
+                    // doing the moving it must not rotate, or its axis beams read as
+                    // scene geometry spinning with the camera over the stationary grid.
+                    let model_pivot = Mat4::from_translation(pivot);
                     let mvp_pivot = (proj * view_mat * model_pivot).to_cols_array_2d();
 
                     // Same draw order as the wgpu pass: bg quad, grid, origin,
diff --git a/src/main.rs b/src/main.rs
index a9668a0..9fc03cd 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -122,6 +122,23 @@ mod tests {
         }
     }
 
+    #[test]
+    fn test_scene_camera_yaw_changes_view() {
+        use cce_ui::widget::WidgetHost;
+        use glam::Vec3;
+        let mut vp = crate::viewport_3d::Viewport3D::new();
+        let inner = vp.as_any_mut().downcast_mut::<crate::viewport_3d::Viewport3D>().unwrap();
+        inner.active_camera = "Camera 1".to_string();
+        let pos = Vec3::new(2.5, 1.8, 2.5);
+        let piv = Vec3::ZERO;
+        let (_, v1, _) = inner.get_matrices(1.0, Some(pos), Some(Vec3::new(23.62, -58.83, 0.0)), Some(piv));
+        let (_, v2, _) = inner.get_matrices(1.0, Some(pos), Some(Vec3::new(23.62, 31.17, 0.0)), Some(piv));
+        let p1 = v1.transform_point3(Vec3::new(1.0, 0.0, 0.0));
+        let p2 = v2.transform_point3(Vec3::new(1.0, 0.0, 0.0));
+        println!("world (1,0,0) in eye space: {p1:?} vs {p2:?}");
+        assert!((p1 - p2).length() > 0.1, "yaw had no effect: {p1:?} vs {p2:?}");
+    }
+
     #[test]
     fn test_node_template_names() {
         let templates_root = crate::app::load_fs_tree();