Files
OneCAD-Draft/src/scene/camera.rs
T
Hakan SevenandClaude Sonnet 4.6 b346eb4d6e fix: remove dead code warning and fix sheet viewport double-click in DWG files
Remove unused `screen_delta_to_world` camera method (superseded by the
corrected pan_active_viewport formula).

Fix `is_content_viewport`: the old geometry fallback required the viewport
center to be near the origin, but DWG files never write the viewport id
field (stays 0) and the sheet viewport center is the paper centre
(e.g. 105, 148.5 mm for A4) — not the origin. This caused the sheet
viewport to be classified as a content viewport in DWG files, making it
both tessellated (border visible) and enterable via double-click.

The new heuristic uses only the scale: a sheet viewport has scale ≈ 1:1
(view_height ≈ paper height); true content viewports have deliberate
drawing scales (1:50, 1:100, …) that are far from 1.0.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-02 14:55:12 +03:00

294 lines
12 KiB
Rust

// Arcball orbit camera — quaternion-based rotation, no gimbal lock.
//
// The camera orbits around a `target` point using a unit quaternion (`rotation`)
// that maps the canonical "camera looks down -Z" pose to the current view.
//
// Pan: translates `target` in the view-plane (no rotation change).
// Orbit: updates `rotation` via arcball delta — converts screen drag delta
// to a rotation axis/angle, then pre-multiplies the current quaternion.
// Zoom: adjusts `distance` (exponential feel).
// Snap: directly assigns yaw+pitch encoded as a quaternion (for ViewCube).
//
// Coordinate convention: Z-up world space (same as the rest of H7CAD).
use glam::{vec3, Mat4, Quat, Vec3};
use iced::{Point, Rectangle};
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Projection {
Orthographic,
Perspective,
}
#[derive(Clone)]
pub struct Camera {
/// World-space pivot point the camera orbits around.
pub target: Vec3,
/// Arcball rotation: maps canonical pose to current orientation.
pub rotation: Quat,
/// Distance from eye to target.
pub distance: f32,
/// Vertical field of view in radians (perspective only).
pub fov_y: f32,
pub projection: Projection,
// --- Legacy yaw/pitch exposed only for ViewCube hit-test compatibility ---
// Kept in sync with `rotation` whenever orbit() or snap_angles() is called.
pub yaw: f32,
pub pitch: f32,
}
impl Default for Camera {
fn default() -> Self {
// Default: look straight down at the XY drawing plane (top view, Z-up).
// yaw = 0, pitch = PI/2 → eye is directly above target.
let yaw = 0.0_f32;
let pitch = std::f32::consts::FRAC_PI_2;
Self {
target: Vec3::ZERO,
rotation: yaw_pitch_to_quat(yaw, pitch),
distance: 60.36,
fov_y: 45.0_f32.to_radians(),
projection: Projection::Orthographic,
yaw,
pitch,
}
}
}
pub const OPENGL_TO_WGPU: Mat4 = glam::mat4(
glam::vec4(1.0, 0.0, 0.0, 0.0),
glam::vec4(0.0, 1.0, 0.0, 0.0),
glam::vec4(0.0, 0.0, 0.5, 0.0),
glam::vec4(0.0, 0.0, 0.5, 1.0),
);
impl Camera {
// ── Eye position ───────────────────────────────────────────────────────
pub fn eye(&self) -> Vec3 {
// The canonical eye direction is +Z (looking at origin from above).
// The rotation maps that canonical pose to the current orientation.
let eye_dir = self.rotation * Vec3::Z;
self.target + eye_dir * self.distance
}
/// Half-height of the orthographic frustum in world units.
pub fn ortho_size(&self) -> f32 {
self.distance * (self.fov_y * 0.5).tan()
}
// ── Projection matrices ────────────────────────────────────────────────
pub fn view_proj(&self, bounds: Rectangle) -> Mat4 {
let near = self.distance * 0.001;
let far = self.distance * 1000.0;
let aspect = bounds.width / bounds.height;
// Up vector: use the rotation to find which world direction is "up"
// in the current camera frame.
let up_dir = self.rotation * Vec3::Y;
let view = Mat4::look_at_rh(self.eye(), self.target, up_dir);
let proj = match self.projection {
Projection::Perspective => Mat4::perspective_rh(self.fov_y, aspect, near, far),
Projection::Orthographic => {
let h = self.ortho_size();
let w = h * aspect;
Mat4::orthographic_rh(-w, w, -h, h, near, far)
}
};
OPENGL_TO_WGPU * proj * view
}
/// Project a screen point onto an arbitrary world-space plane.
///
/// The plane is defined by `plane_normal` (unit vector) and a `plane_point`
/// that lies on it. Returns the intersection of the view ray with the plane;
/// falls back to `plane_point` when the ray is nearly parallel to the plane.
pub fn pick_on_plane(
&self,
screen: Point,
bounds: Rectangle,
plane_normal: Vec3,
plane_point: Vec3,
) -> Vec3 {
let ndc_x = (screen.x / bounds.width) * 2.0 - 1.0;
let ndc_y = 1.0 - (screen.y / bounds.height) * 2.0;
let inv = self.view_proj(bounds).inverse();
let (ray_origin, ray_dir) = match self.projection {
Projection::Perspective => {
let near_pt = inv.project_point3(Vec3::new(ndc_x, ndc_y, 0.0));
let far_pt = inv.project_point3(Vec3::new(ndc_x, ndc_y, 1.0));
let dir = (far_pt - near_pt).normalize();
(near_pt, dir)
}
Projection::Orthographic => {
let origin = inv.project_point3(Vec3::new(ndc_x, ndc_y, 0.0));
let forward = (self.target - self.eye()).normalize();
(origin, forward)
}
};
let denom = ray_dir.dot(plane_normal);
if denom.abs() < 1e-6 {
return plane_point;
}
let t = (plane_point - ray_origin).dot(plane_normal) / denom;
if t < 0.0 {
return plane_point;
}
ray_origin + ray_dir * t
}
pub fn pick_on_target_plane(&self, screen: Point, bounds: Rectangle) -> Vec3 {
let ndc_x = (screen.x / bounds.width) * 2.0 - 1.0;
let ndc_y = 1.0 - (screen.y / bounds.height) * 2.0;
let inv = self.view_proj(bounds).inverse();
match self.projection {
Projection::Perspective => {
let near_pt = inv.project_point3(Vec3::new(ndc_x, ndc_y, 0.0));
let far_pt = inv.project_point3(Vec3::new(ndc_x, ndc_y, 1.0));
let dir = (far_pt - near_pt).normalize();
let forward = (self.target - self.eye()).normalize();
let denom = dir.dot(forward);
if denom.abs() < 1e-6 {
return self.target;
}
let t = (self.target - near_pt).dot(forward) / denom;
if t < 0.0 {
return self.target;
}
near_pt + dir * t
}
Projection::Orthographic => {
let ray_origin = inv.project_point3(Vec3::new(ndc_x, ndc_y, 0.0));
let forward = (self.target - self.eye()).normalize();
let t = (self.target - ray_origin).dot(forward) / forward.dot(forward);
ray_origin + forward * t
}
}
}
pub fn position_vec4(&self) -> glam::Vec4 {
glam::Vec4::from((self.eye(), 0.0))
}
// ── ViewCube rotation matrix ───────────────────────────────────────────
/// Returns the rotation matrix for the ViewCube.
///
/// The camera quaternion maps canonical pose (+Z eye) → current view.
/// The ViewCube needs the inverse so the cube stays world-aligned.
/// Inverse of a unit quaternion = its conjugate.
pub fn view_rotation_mat(&self) -> Mat4 {
Mat4::from_quat(self.rotation.conjugate())
}
// ── Navigation ────────────────────────────────────────────────────────
/// Arcball orbit: drag delta (dx, dy) in screen pixels.
pub fn orbit(&mut self, delta_x: f32, delta_y: f32) {
if delta_x.abs() < 1e-6 && delta_y.abs() < 1e-6 {
return;
}
let speed = 0.005_f32;
let angle = (delta_x * delta_x + delta_y * delta_y).sqrt() * speed;
// Screen drag → rotation axis: right drag = rotate around cam_up (Y),
// down drag = rotate around cam_right (X). Negate so drag direction
// matches intuitive "grab and spin" arcball behaviour.
let screen_axis = vec3(-delta_y, -delta_x, 0.0).normalize_or_zero();
let cam_right = self.rotation * Vec3::X;
let cam_up = self.rotation * Vec3::Y;
let world_axis = (cam_right * screen_axis.x + cam_up * screen_axis.y).normalize_or_zero();
if world_axis.length_squared() < 1e-12 {
return;
}
let delta_rot = Quat::from_axis_angle(world_axis, angle);
self.rotation = (delta_rot * self.rotation).normalize();
// Sync legacy yaw/pitch for hit-test functions.
self.sync_yaw_pitch();
}
pub fn zoom(&mut self, delta: f32) {
self.distance = (self.distance * (1.0 - delta * 0.1)).max(0.001);
}
pub fn zoom_about_point(&mut self, screen: Point, bounds: Rectangle, delta: f32) {
if bounds.width <= 0.0 || bounds.height <= 0.0 {
self.zoom(delta);
return;
}
let before = self.pick_on_target_plane(screen, bounds);
self.zoom(delta);
let after = self.pick_on_target_plane(screen, bounds);
self.target += before - after;
}
pub fn pan(&mut self, delta_x: f32, delta_y: f32) {
let speed = self.distance * 0.001;
let cam_right = self.rotation * Vec3::X;
let cam_up = self.rotation * Vec3::Y;
self.target -= cam_right * delta_x * speed;
self.target += cam_up * delta_y * speed;
}
pub fn fit_to_bounds(&mut self, min: Vec3, max: Vec3) {
self.target = (min + max) * 0.5;
let size = (max - min).length();
self.distance = size * 1.5;
}
// ── ViewCube snap ─────────────────────────────────────────────────────
/// Snap to canonical yaw+pitch (called by ViewCubeSnap message).
/// Updates the arcball quaternion to match.
pub fn snap_to_angles(&mut self, yaw: f32, pitch: f32) {
self.yaw = yaw;
self.pitch = pitch;
self.rotation = yaw_pitch_to_quat(yaw, pitch);
}
// ── Internal helpers ───────────────────────────────────────────────────
/// Derive yaw and pitch from the current quaternion for the ViewCube
/// hit-test functions (hit_test / hover_id). These use yaw/pitch to
/// compute the same rotation matrix as the shader, so they must match.
fn sync_yaw_pitch(&mut self) {
// Eye direction in world space (canonical eye dir is +Z).
let eye_dir = self.rotation * Vec3::Z;
// pitch: angle above/below the XY plane.
self.pitch = eye_dir.z.clamp(-0.999, 0.999).asin();
// yaw: atan2(x, y) matches from_rotation_z(yaw) used in view_rotation_mat.
self.yaw = eye_dir.x.atan2(eye_dir.y);
}
}
// ── Free helpers ───────────────────────────────────────────────────────────
/// Build a rotation quaternion from yaw (rotation around Z) and pitch
/// (tilt toward Z). Matches the coordinate convention of the ViewCube
/// so snap angles continue to work unchanged.
///
/// Convention (Z-up, Y-forward):
/// yaw = 0 → camera looks along +Y axis (front view)
/// pitch = PI/2 → camera looks down -Z (top view)
/// pitch = 0 → camera in the XY plane
/// Build a rotation quaternion from yaw and pitch.
/// Positive yaw rotates the view direction clockwise when seen from above (Z-up).
pub fn yaw_pitch_to_quat(yaw: f32, pitch: f32) -> Quat {
// +yaw so ViewCube faces match camera direction (FRONT at yaw=0 = +Y world axis).
let q_yaw = Quat::from_rotation_z(yaw);
let q_pitch = Quat::from_rotation_x(std::f32::consts::FRAC_PI_2 - pitch);
(q_yaw * q_pitch).normalize()
}