Skip to main content

brep_render/
camera.rs

1//! Orthographic artifact framing and orbit-camera math.
2//! Calculations use f64, converting to f32 at the GPU boundary.
3
4use crate::geometry3d::{cross3 as cross, dot3 as dot, norm3 as norm};
5
6/// Axis-aligned bounding box (world space, f64).
7#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
8pub struct Aabb {
9    pub min: [f64; 3],
10    pub max: [f64; 3],
11}
12
13impl Aabb {
14    pub fn empty() -> Self {
15        Self {
16            min: [f64::INFINITY; 3],
17            max: [f64::NEG_INFINITY; 3],
18        }
19    }
20
21    pub fn is_empty(&self) -> bool {
22        (0..3).any(|i| self.min[i] > self.max[i])
23    }
24
25    pub fn expand(&mut self, p: [f64; 3]) {
26        for i in 0..3 {
27            self.min[i] = self.min[i].min(p[i]);
28            self.max[i] = self.max[i].max(p[i]);
29        }
30    }
31
32    pub fn union(&mut self, other: &Aabb) {
33        if other.is_empty() {
34            return;
35        }
36        self.expand(other.min);
37        self.expand(other.max);
38    }
39
40    pub fn center(&self) -> [f64; 3] {
41        [
42            (self.min[0] + self.max[0]) * 0.5,
43            (self.min[1] + self.max[1]) * 0.5,
44            (self.min[2] + self.max[2]) * 0.5,
45        ]
46    }
47
48    pub fn size(&self) -> [f64; 3] {
49        [
50            self.max[0] - self.min[0],
51            self.max[1] - self.min[1],
52            self.max[2] - self.min[2],
53        ]
54    }
55}
56
57/// A camera resolved to GPU form: column-major view-projection matrix plus the
58/// world-space view direction (camera → scene) the shader needs for
59/// double-sided normal flipping and specular.
60#[derive(Debug, Clone, Copy)]
61pub struct Camera {
62    /// Column-major 4x4, world → wgpu clip space (z in 0..1).
63    pub view_proj: [[f32; 4]; 4],
64    /// Normalized world-space view direction, from the camera toward the scene.
65    pub forward: [f32; 3],
66}
67
68/// Right-handed look-at view matrix (camera looks down its -Z), column-major.
69fn look_at(eye: [f64; 3], target: [f64; 3], up: [f64; 3]) -> [[f64; 4]; 4] {
70    let f = norm([target[0] - eye[0], target[1] - eye[1], target[2] - eye[2]]);
71    let r = norm(cross(f, up));
72    let u = cross(r, f);
73    // Columns of the view matrix (world → view).
74    [
75        [r[0], u[0], -f[0], 0.0],
76        [r[1], u[1], -f[1], 0.0],
77        [r[2], u[2], -f[2], 0.0],
78        [-dot(r, eye), -dot(u, eye), dot(f, eye), 1.0],
79    ]
80}
81
82/// Orthographic projection to wgpu clip space (x,y in -1..1, z in 0..1),
83/// column-major. View-space z of visible points is in [-far, -near].
84fn ortho(l: f64, r: f64, b: f64, t: f64, near: f64, far: f64) -> [[f64; 4]; 4] {
85    let sx = 2.0 / (r - l);
86    let sy = 2.0 / (t - b);
87    let sz = -1.0 / (far - near);
88    [
89        [sx, 0.0, 0.0, 0.0],
90        [0.0, sy, 0.0, 0.0],
91        [0.0, 0.0, sz, 0.0],
92        [
93            -(r + l) / (r - l),
94            -(t + b) / (t - b),
95            -near / (far - near),
96            1.0,
97        ],
98    ]
99}
100
101fn mul(a: [[f64; 4]; 4], b: [[f64; 4]; 4]) -> [[f64; 4]; 4] {
102    let mut out = [[0.0; 4]; 4];
103    for col in 0..4 {
104        for row in 0..4 {
105            let mut sum = 0.0;
106            for k in 0..4 {
107                sum += a[k][row] * b[col][k];
108            }
109            out[col][row] = sum;
110        }
111    }
112    out
113}
114
115fn to_f32(m: [[f64; 4]; 4]) -> [[f32; 4]; 4] {
116    let mut out = [[0.0f32; 4]; 4];
117    for c in 0..4 {
118        for r in 0..4 {
119            out[c][r] = m[c][r] as f32;
120        }
121    }
122    out
123}
124
125/// Build a camera from an explicit eye/target/up + ortho frustum (the shared
126/// path for the artifact framing and the desktop orbit).
127pub fn ortho_camera(
128    eye: [f64; 3],
129    target: [f64; 3],
130    up: [f64; 3],
131    half_width: f64,
132    half_height: f64,
133    near: f64,
134    far: f64,
135) -> Camera {
136    let view = look_at(eye, target, up);
137    let proj = ortho(-half_width, half_width, -half_height, half_height, near, far);
138    let forward = norm([
139        target[0] - eye[0],
140        target[1] - eye[1],
141        target[2] - eye[2],
142    ]);
143    Camera {
144        view_proj: to_f32(mul(proj, view)),
145        forward: [forward[0] as f32, forward[1] as f32, forward[2] as f32],
146    }
147}
148
149/// The artifact framing — a faithful port of the retired artifact renderer:
150/// orthographic, iso view direction (1, -1.2, 0.9), Z up, bbox-fit with a 1.15
151/// margin, camera at center + dir·radius·6, near 0.01 / far radius·20.
152pub fn artifact_camera(bbox: &Aabb, width: u32, height: u32) -> Camera {
153    let bbox = if bbox.is_empty() {
154        Aabb {
155            min: [-1.0, -1.0, -1.0],
156            max: [1.0, 1.0, 1.0],
157        }
158    } else {
159        *bbox
160    };
161    let center = bbox.center();
162    let size = bbox.size();
163    let radius = size[0].max(size[1]).max(size[2]).max(1e-9) * 0.75;
164    let dir = norm([1.0, -1.2, 0.9]);
165    let eye = [
166        center[0] + dir[0] * radius * 6.0,
167        center[1] + dir[1] * radius * 6.0,
168        center[2] + dir[2] * radius * 6.0,
169    ];
170    let aspect = width.max(1) as f64 / height.max(1) as f64;
171    ortho_camera(
172        eye,
173        center,
174        [0.0, 0.0, 1.0],
175        radius * aspect * 1.15,
176        radius * 1.15,
177        0.01,
178        radius * 20.0,
179    )
180}
181
182/// Simple Z-up orbit state for the desktop shell: azimuth/elevation around the
183/// scene bbox, distance-scaled ortho frustum (zoom = frustum scale).
184#[derive(Debug, Clone, Copy)]
185pub struct Orbit {
186    pub azimuth: f64,
187    pub elevation: f64,
188    pub zoom: f64,
189}
190
191impl Default for Orbit {
192    fn default() -> Self {
193        // Match the artifact iso direction: dir (1, -1.2, 0.9).
194        let dir = norm([1.0, -1.2, 0.9]);
195        Self {
196            azimuth: dir[1].atan2(dir[0]),
197            elevation: dir[2].asin(),
198            zoom: 1.0,
199        }
200    }
201}
202
203impl Orbit {
204    pub fn camera(&self, bbox: &Aabb, width: u32, height: u32) -> Camera {
205        let bbox = if bbox.is_empty() {
206            Aabb {
207                min: [-1.0, -1.0, -1.0],
208                max: [1.0, 1.0, 1.0],
209            }
210        } else {
211            *bbox
212        };
213        let center = bbox.center();
214        let size = bbox.size();
215        let radius = size[0].max(size[1]).max(size[2]).max(1e-9) * 0.75;
216        let el = self.elevation.clamp(-1.55, 1.55);
217        let dir = [
218            el.cos() * self.azimuth.cos(),
219            el.cos() * self.azimuth.sin(),
220            el.sin(),
221        ];
222        let eye = [
223            center[0] + dir[0] * radius * 6.0,
224            center[1] + dir[1] * radius * 6.0,
225            center[2] + dir[2] * radius * 6.0,
226        ];
227        let aspect = width.max(1) as f64 / height.max(1) as f64;
228        let half_h = radius * 1.15 * self.zoom;
229        ortho_camera(
230            eye,
231            center,
232            [0.0, 0.0, 1.0],
233            half_h * aspect,
234            half_h,
235            0.01,
236            radius * 20.0,
237        )
238    }
239}
240
241// BREP private tests: 58e0c338569438f8