From cbb37a0909a3912a4fad6e6071e77b26c368838d Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Fri, 1 May 2026 07:43:45 +0300 Subject: [PATCH] fix: persist camera in View table and fix pitch clamp for exact top/bottom views MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Save camera to named View table entries (H7CAD_Camera_Model / H7CAD_Camera_) on every navigation change; restore from these entries on file open and tab switch. acadrust's DWG writer overrides *Active VPort view_height/view_center with zoom-extents values, but leaves the View table untouched, so this survives save. - Fix pitch recovery: clamp vd.z to [-1, 1] instead of [-0.999, 0.999] so that asin(1.0) = π/2 exactly for plan/top views, preventing the subtle tilt on reload. - Fix IEEE 754 edge case: atan2(+0, -0) = π for plan view direction (0,0,1); special-case yaw = 0 when both vd.x and vd.y are near zero. Co-Authored-By: Claude Sonnet 4.6 --- src/scene/mod.rs | 200 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 151 insertions(+), 49 deletions(-) diff --git a/src/scene/mod.rs b/src/scene/mod.rs index d7c341c65..5418ff39d 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -2311,7 +2311,7 @@ impl Scene { // Build rotation: canonical eye is +Z, rotate to eye_dir. cam.rotation = glam::Quat::from_rotation_arc(Vec3::Z, eye_dir); // Sync yaw/pitch from new rotation (for ViewCube). - let pitch = eye_dir.z.clamp(-0.999, 0.999).asin(); + let pitch = eye_dir.z.clamp(-1.0, 1.0).asin(); let yaw = eye_dir.x.atan2(eye_dir.y); cam.yaw = yaw; cam.pitch = pitch; @@ -2380,9 +2380,65 @@ impl Scene { self.camera_generation += 1; } + /// Apply camera state from an acadrust View table entry. + /// `model_space`: if true, subtracts world_offset from target (wire-space). + fn apply_camera_from_view_entry(&mut self, view: &acadrust::tables::View, model_space: bool) -> bool { + if view.height.abs() < 1e-9 { + return false; + } + let vd = glam::Vec3::new( + view.direction.x as f32, + view.direction.y as f32, + view.direction.z as f32, + ).normalize_or(glam::Vec3::Z); + let pitch = vd.z.clamp(-1.0, 1.0).asin(); + let yaw = if vd.x.abs() < 1e-6 && vd.y.abs() < 1e-6 { + 0.0_f32 + } else { + vd.x.atan2(-vd.y) + }; + let rotation = camera::yaw_pitch_to_quat(yaw, pitch); + let view_right = rotation * glam::Vec3::X; + let view_up = rotation * glam::Vec3::Y; + let base = if model_space { + glam::Vec3::new( + (view.target.x - self.world_offset[0]) as f32, + (view.target.y - self.world_offset[1]) as f32, + (view.target.z - self.world_offset[2]) as f32, + ) + } else { + glam::Vec3::new( + view.target.x as f32, + view.target.y as f32, + view.target.z as f32, + ) + }; + let target = base + + view_right * view.center.x as f32 + + view_up * view.center.y as f32; + let fov_y = 45.0_f32.to_radians(); + let distance = ((view.height as f32 / 2.0) / (fov_y * 0.5).tan()).max(0.001); + let mut cam = self.camera.borrow_mut(); + cam.target = target; + cam.rotation = rotation; + cam.distance = distance; + cam.yaw = yaw; + cam.pitch = pitch; + cam.fov_y = fov_y; + cam.projection = camera::Projection::Orthographic; + drop(cam); + self.camera_generation += 1; + true + } + /// Set the model-space camera from the VPORT table's *Active entry. /// Returns true if the entry was found and the camera was set. fn apply_active_vport_camera(&mut self) -> bool { + // Prefer our named View entry — survives DWG save without being overridden. + let saved_view = self.document.views.iter().find(|v| v.name == "H7CAD_Camera_Model").cloned(); + if let Some(view) = saved_view { + return self.apply_camera_from_view_entry(&view, true); + } let vp = match self.document.vports.iter().find(|v| v.name == "*Active") { Some(v) => v.clone(), None => return false, @@ -2399,8 +2455,15 @@ impl Scene { ) .normalize_or(glam::Vec3::Z); - let pitch = vd.z.clamp(-0.999, 0.999).asin(); - let yaw = vd.x.atan2(-vd.y); + let pitch = vd.z.clamp(-1.0, 1.0).asin(); + // view_dir = (sin(yaw)*cos(pitch), -cos(yaw)*cos(pitch), sin(pitch)) + // → yaw = atan2(x, -y), but when looking straight up/down cos(pitch)≈0 + // both x and y are near zero and atan2(0, -0.0) = π due to IEEE 754. + let yaw = if vd.x.abs() < 1e-6 && vd.y.abs() < 1e-6 { + 0.0_f32 // plan/nadir view: yaw is undefined, default to 0 + } else { + vd.x.atan2(-vd.y) + }; let rotation = camera::yaw_pitch_to_quat(yaw, pitch); let view_right = rotation * glam::Vec3::X; let view_up = rotation * glam::Vec3::Y; @@ -2435,6 +2498,13 @@ impl Scene { /// Set the paper-space camera from the sheet viewport's stored view. /// Returns true if a valid sheet viewport was found and the camera was set. fn apply_sheet_viewport_camera(&mut self) -> bool { + // Prefer our named View entry — survives DWG save without being overridden. + let view_name = format!("H7CAD_Camera_{}", self.current_layout); + let saved_view = self.document.views.iter().find(|v| v.name == view_name).cloned(); + if let Some(view) = saved_view { + return self.apply_camera_from_view_entry(&view, false); + } + let layout_block = self.current_layout_block_handle(); if layout_block.is_null() { return false; @@ -2462,8 +2532,15 @@ impl Scene { ) .normalize_or(glam::Vec3::Z); - let pitch = vd.z.clamp(-0.999, 0.999).asin(); - let yaw = vd.x.atan2(-vd.y); + let pitch = vd.z.clamp(-1.0, 1.0).asin(); + // view_dir = (sin(yaw)*cos(pitch), -cos(yaw)*cos(pitch), sin(pitch)) + // → yaw = atan2(x, -y), but when looking straight up/down cos(pitch)≈0 + // both x and y are near zero and atan2(0, -0.0) = π due to IEEE 754. + let yaw = if vd.x.abs() < 1e-6 && vd.y.abs() < 1e-6 { + 0.0_f32 // plan/nadir view: yaw is undefined, default to 0 + } else { + vd.x.atan2(-vd.y) + }; let rotation = camera::yaw_pitch_to_quat(yaw, pitch); let view_right = rotation * glam::Vec3::X; let view_up = rotation * glam::Vec3::Y; @@ -2501,63 +2578,81 @@ impl Scene { let cam = self.camera.borrow().clone(); let view_dir = cam.rotation * glam::Vec3::Z; let view_height = cam.ortho_size() * 2.0; + let vd3 = acadrust::types::Vector3 { + x: view_dir.x as f64, + y: view_dir.y as f64, + z: view_dir.z as f64, + }; if self.current_layout == "Model" { - // Write back to the *Active VPort entry. - let vp = match self.document.vports.iter_mut().find(|v| v.name == "*Active") { - Some(v) => v, - None => return false, - }; - // cam.target is in wire-space; convert back to raw model WCS. - vp.view_target = acadrust::types::Vector3 { + let target_wcs = acadrust::types::Vector3 { x: (cam.target.x as f64) + self.world_offset[0], y: (cam.target.y as f64) + self.world_offset[1], z: (cam.target.z as f64) + self.world_offset[2], }; - vp.view_center = acadrust::types::Vector2::ZERO; - vp.view_direction = acadrust::types::Vector3 { - x: view_dir.x as f64, - y: view_dir.y as f64, - z: view_dir.z as f64, - }; - vp.view_height = view_height as f64; + + // Write back to the *Active VPort entry (may be overridden by DWG writer). + if let Some(vp) = self.document.vports.iter_mut().find(|v| v.name == "*Active") { + vp.view_target = target_wcs; + vp.view_center = acadrust::types::Vector2::ZERO; + vp.view_direction = vd3; + vp.view_height = view_height as f64; + } + + // Also write to View table — survives DWG save without override. + self.write_camera_view_entry("H7CAD_Camera_Model", target_wcs, vd3, view_height); true } else { + let target_wcs = acadrust::types::Vector3 { + x: cam.target.x as f64, + y: cam.target.y as f64, + z: cam.target.z as f64, + }; + let view_name = format!("H7CAD_Camera_{}", self.current_layout); + // Write back to the sheet viewport entity. let layout_block = self.current_layout_block_handle(); - if layout_block.is_null() { return false; } + if !layout_block.is_null() { + let sheet_handle = self.document.entities() + .filter_map(|e| if let EntityType::Viewport(vp) = e { Some(vp) } else { None }) + .find(|vp| vp.common.owner_handle == layout_block && !Self::is_content_viewport(vp)) + .map(|vp| vp.common.handle); - let sheet_handle = self.document.entities() - .filter_map(|e| if let EntityType::Viewport(vp) = e { Some(vp) } else { None }) - .find(|vp| vp.common.owner_handle == layout_block && !Self::is_content_viewport(vp)) - .map(|vp| vp.common.handle); - - let handle = match sheet_handle { - Some(h) => h, - None => return false, - }; - - if let Some(EntityType::Viewport(vp)) = self.document.get_entity_mut(handle) { - // Paper-space entities have no world_offset; target is raw paper coords. - vp.view_target = acadrust::types::Vector3 { - x: cam.target.x as f64, - y: cam.target.y as f64, - z: cam.target.z as f64, - }; - vp.view_center = acadrust::types::Vector3::ZERO; - vp.view_direction = acadrust::types::Vector3 { - x: view_dir.x as f64, - y: view_dir.y as f64, - z: view_dir.z as f64, - }; - vp.view_height = view_height as f64; - true - } else { - false + if let Some(handle) = sheet_handle { + if let Some(EntityType::Viewport(vp)) = self.document.get_entity_mut(handle) { + vp.view_target = target_wcs; + vp.view_center = acadrust::types::Vector3::ZERO; + vp.view_direction = vd3; + vp.view_height = view_height as f64; + } + } } + + // Also write to View table — survives DWG save without override. + self.write_camera_view_entry(&view_name, target_wcs, vd3, view_height); + true } } + /// Upsert a named View entry with the given camera fields. + fn write_camera_view_entry( + &mut self, + name: &str, + target: acadrust::types::Vector3, + direction: acadrust::types::Vector3, + height: f32, + ) { + let existing_handle = self.document.views.iter().find(|v| v.name == name).map(|v| v.handle); + let mut entry = acadrust::tables::View::new(name); + entry.handle = existing_handle.unwrap_or_else(|| self.document.allocate_handle()); + entry.target = target; + entry.direction = direction; + entry.height = height as f64; + entry.width = height as f64; + entry.center = acadrust::types::Vector3::ZERO; + self.document.views.add_or_replace(entry); + } + /// Restore the camera from the file's saved view (called once on open). /// Falls back to fit_all() if no saved view is available. pub fn restore_saved_camera(&mut self) { @@ -2720,8 +2815,15 @@ impl Scene { ) .normalize_or(glam::Vec3::Z); - let pitch = vd.z.clamp(-0.999, 0.999).asin(); - let yaw = vd.x.atan2(-vd.y); + let pitch = vd.z.clamp(-1.0, 1.0).asin(); + // view_dir = (sin(yaw)*cos(pitch), -cos(yaw)*cos(pitch), sin(pitch)) + // → yaw = atan2(x, -y), but when looking straight up/down cos(pitch)≈0 + // both x and y are near zero and atan2(0, -0.0) = π due to IEEE 754. + let yaw = if vd.x.abs() < 1e-6 && vd.y.abs() < 1e-6 { + 0.0_f32 // plan/nadir view: yaw is undefined, default to 0 + } else { + vd.x.atan2(-vd.y) + }; let rotation = camera::yaw_pitch_to_quat(yaw, pitch); let view_right = rotation * glam::Vec3::X;