1use crate::camera::{Aabb, Camera};
8
9pub use crate::geometry3d::{add3, cross3, dot3, len3, norm3, rotate3, scale3, sub3};
10
11pub fn invert4_columns(m: &[f64; 16]) -> Option<[f64; 16]> {
15 let a00 = m[0]; let a01 = m[1]; let a02 = m[2]; let a03 = m[3];
16 let a10 = m[4]; let a11 = m[5]; let a12 = m[6]; let a13 = m[7];
17 let a20 = m[8]; let a21 = m[9]; let a22 = m[10]; let a23 = m[11];
18 let a30 = m[12]; let a31 = m[13]; let a32 = m[14]; let a33 = m[15];
19
20 let b00 = a00 * a11 - a01 * a10;
21 let b01 = a00 * a12 - a02 * a10;
22 let b02 = a00 * a13 - a03 * a10;
23 let b03 = a01 * a12 - a02 * a11;
24 let b04 = a01 * a13 - a03 * a11;
25 let b05 = a02 * a13 - a03 * a12;
26 let b06 = a20 * a31 - a21 * a30;
27 let b07 = a20 * a32 - a22 * a30;
28 let b08 = a20 * a33 - a23 * a30;
29 let b09 = a21 * a32 - a22 * a31;
30 let b10 = a21 * a33 - a23 * a31;
31 let b11 = a22 * a33 - a23 * a32;
32
33 let det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06;
34 if det.abs() < 1e-300 {
35 return None;
36 }
37 let inv = 1.0 / det;
38 Some([
39 (a11 * b11 - a12 * b10 + a13 * b09) * inv,
40 (a02 * b10 - a01 * b11 - a03 * b09) * inv,
41 (a31 * b05 - a32 * b04 + a33 * b03) * inv,
42 (a22 * b04 - a21 * b05 - a23 * b03) * inv,
43 (a12 * b08 - a10 * b11 - a13 * b07) * inv,
44 (a00 * b11 - a02 * b08 + a03 * b07) * inv,
45 (a32 * b02 - a30 * b05 - a33 * b01) * inv,
46 (a20 * b05 - a22 * b02 + a23 * b01) * inv,
47 (a10 * b10 - a11 * b08 + a13 * b06) * inv,
48 (a01 * b08 - a00 * b10 - a03 * b06) * inv,
49 (a30 * b04 - a31 * b02 + a33 * b00) * inv,
50 (a21 * b02 - a20 * b04 - a23 * b00) * inv,
51 (a11 * b07 - a10 * b09 - a12 * b06) * inv,
52 (a00 * b09 - a01 * b07 + a02 * b06) * inv,
53 (a31 * b01 - a30 * b03 - a32 * b00) * inv,
54 (a20 * b03 - a21 * b01 + a22 * b00) * inv,
55 ])
56}
57
58#[derive(Debug, Clone, Copy, PartialEq)]
61pub enum Projection {
62 Orthographic { half_height: f64 },
64 Perspective { fov_y_deg: f64 },
65}
66
67#[derive(Debug, Clone, Copy)]
69pub struct Ray {
70 pub origin: [f64; 3],
71 pub dir: [f64; 3],
72}
73
74#[derive(Debug, Clone)]
75pub struct ViewCamera {
76 pub eye: [f64; 3],
77 pub target: [f64; 3],
78 pub up: [f64; 3],
79 pub projection: Projection,
80 pub width: f64,
82 pub height: f64,
83 pub near: f64,
97 pub far: f64,
98}
99
100impl Default for ViewCamera {
101 fn default() -> Self {
102 Self {
105 eye: [15.0, 12.0, 15.0],
106 target: [0.0, 0.0, 0.0],
107 up: [0.0, 1.0, 0.0],
108 projection: Projection::Orthographic { half_height: 10.0 },
109 width: 800.0,
110 height: 600.0,
111 near: -100000.0,
112 far: 100000.0,
113 }
114 }
115}
116
117impl ViewCamera {
118 pub fn aspect(&self) -> f64 {
119 (self.width / self.height.max(1.0)).max(1e-6)
120 }
121
122 pub fn basis(&self) -> ([f64; 3], [f64; 3], [f64; 3]) {
125 let forward = norm3(sub3(self.target, self.eye));
126 let right = norm3(cross3(forward, self.up));
127 let up = cross3(right, forward);
128 (right, up, forward)
129 }
130
131 pub fn distance(&self) -> f64 {
132 len3(sub3(self.eye, self.target)).max(1e-9)
133 }
134
135 pub fn world_per_pixel(&self) -> f64 {
138 match self.projection {
139 Projection::Orthographic { half_height } => 2.0 * half_height / self.height.max(1.0),
140 Projection::Perspective { fov_y_deg } => {
141 let fov = fov_y_deg.to_radians();
142 2.0 * (fov * 0.5).tan() * self.distance() / self.height.max(1.0)
143 }
144 }
145 }
146
147 pub fn view_depth(&self, world: [f64; 3]) -> f64 {
152 let (_, _, forward) = self.basis();
153 dot3(sub3(world, self.eye), forward)
154 }
155
156 pub fn projectable(&self, world: [f64; 3]) -> bool {
171 matches!(self.projection, Projection::Orthographic { .. })
172 || self.view_depth(world) > 1e-6
173 }
174
175 pub fn label_anchor_visible(&self, world: [f64; 3]) -> bool {
185 if !self.projectable(world) {
186 return false;
187 }
188 let (sx, sy, _) = self.project(world);
189 sx >= 0.0 && sx <= self.width && sy >= 0.0 && sy <= self.height
190 }
191
192 pub fn view_proj_cols(&self) -> [[f64; 4]; 4] {
197 let (right, up, forward) = self.basis();
198 let half_h = match self.projection {
199 Projection::Orthographic { half_height } => half_height,
200 Projection::Perspective { fov_y_deg } => (fov_y_deg.to_radians() * 0.5).tan(),
201 };
202 let half_w = half_h * self.aspect();
203
204 let ex = -dot3(right, self.eye);
206 let ey = -dot3(up, self.eye);
207 let ez = dot3(forward, self.eye);
208 let view = [
209 [right[0], up[0], -forward[0], 0.0],
210 [right[1], up[1], -forward[1], 0.0],
211 [right[2], up[2], -forward[2], 0.0],
212 [ex, ey, ez, 1.0],
213 ];
214
215 let proj = match self.projection {
216 Projection::Orthographic { .. } => {
217 let sx = 1.0 / half_w;
219 let sy = 1.0 / half_h;
220 let sz = -1.0 / (self.far - self.near);
221 [
222 [sx, 0.0, 0.0, 0.0],
223 [0.0, sy, 0.0, 0.0],
224 [0.0, 0.0, sz, 0.0],
225 [0.0, 0.0, -self.near / (self.far - self.near), 1.0],
226 ]
227 }
228 Projection::Perspective { .. } => {
229 let near = self.near.max(1e-6);
242 let far = self.far.max(near * 1.0001);
243 let f = 1.0 / half_h;
244 [
245 [f / self.aspect(), 0.0, 0.0, 0.0],
246 [0.0, f, 0.0, 0.0],
247 [0.0, 0.0, far / (near - far), -1.0],
248 [0.0, 0.0, near * far / (near - far), 0.0],
249 ]
250 }
251 };
252
253 let mut view_proj = [[0.0f64; 4]; 4];
254 for col in 0..4 {
255 for row in 0..4 {
256 let mut sum = 0.0;
257 for k in 0..4 {
258 sum += proj[k][row] * view[col][k];
259 }
260 view_proj[col][row] = sum;
261 }
262 }
263 view_proj
264 }
265
266 pub fn resolve(&self) -> Camera {
268 let cols = self.view_proj_cols();
269 let mut view_proj = [[0.0f32; 4]; 4];
270 for col in 0..4 {
271 for row in 0..4 {
272 view_proj[col][row] = cols[col][row] as f32;
273 }
274 }
275 let fwd = norm3(sub3(self.target, self.eye));
276 Camera {
277 view_proj,
278 forward: [fwd[0] as f32, fwd[1] as f32, fwd[2] as f32],
279 }
280 }
281
282 pub fn view_proj_flat(&self) -> [f64; 16] {
287 let cols = self.view_proj_cols();
288 let mut out = [0.0f64; 16];
289 for col in 0..4 {
290 for row in 0..4 {
291 out[col * 4 + row] = cols[col][row];
292 }
293 }
294 out
295 }
296
297 pub fn view_proj_inverse_flat(&self) -> [f64; 16] {
301 invert4_columns(&self.view_proj_flat())
302 .unwrap_or([1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0])
303 }
304
305 pub fn project(&self, world: [f64; 3]) -> (f64, f64, f64) {
309 let (right, up, forward) = self.basis();
310 let rel = sub3(world, self.eye);
311 let vx = dot3(rel, right);
312 let vy = dot3(rel, up);
313 let depth = dot3(rel, forward);
314 match self.projection {
315 Projection::Orthographic { half_height } => {
316 let half_w = half_height * self.aspect();
317 let sx = (vx / half_w * 0.5 + 0.5) * self.width;
318 let sy = (0.5 - vy / half_height * 0.5) * self.height;
319 (sx, sy, depth)
320 }
321 Projection::Perspective { fov_y_deg } => {
322 let half_h = (fov_y_deg.to_radians() * 0.5).tan();
323 let half_w = half_h * self.aspect();
324 let d = depth.max(1e-9);
325 let sx = (vx / (half_w * d) * 0.5 + 0.5) * self.width;
326 let sy = (0.5 - vy / (half_h * d) * 0.5) * self.height;
327 (sx, sy, depth)
328 }
329 }
330 }
331
332 pub fn pick_ray(&self, x: f64, y: f64) -> Ray {
336 let (right, up, forward) = self.basis();
337 let ndc_x = (x / self.width.max(1.0)) * 2.0 - 1.0;
338 let ndc_y = -((y / self.height.max(1.0)) * 2.0 - 1.0);
339 match self.projection {
340 Projection::Orthographic { half_height } => {
341 let half_w = half_height * self.aspect();
342 let span = self.far.abs().max(self.near.abs()).max(half_height * 40.0).max(1.0);
343 let on_plane = add3(
344 self.eye,
345 add3(scale3(right, ndc_x * half_w), scale3(up, ndc_y * half_height)),
346 );
347 Ray {
348 origin: sub3(on_plane, scale3(forward, span)),
349 dir: forward,
350 }
351 }
352 Projection::Perspective { fov_y_deg } => {
353 let half_h = (fov_y_deg.to_radians() * 0.5).tan();
354 let half_w = half_h * self.aspect();
355 let dir = norm3(add3(
356 forward,
357 add3(scale3(right, ndc_x * half_w), scale3(up, ndc_y * half_h)),
358 ));
359 Ray {
360 origin: self.eye,
361 dir,
362 }
363 }
364 }
365 }
366
367 pub fn fit_depth_range(&mut self, bbox: &Aabb) {
370 if bbox.is_empty() {
371 match self.projection {
380 Projection::Orthographic { .. } => {
381 self.near = -100000.0;
382 self.far = 100000.0;
383 }
384 Projection::Perspective { .. } => {
385 self.near = 0.1;
386 self.far = 1e5;
387 }
388 }
389 return;
390 }
391 let (_, _, forward) = self.basis();
392 let mut min_d = f64::INFINITY;
393 let mut max_d = f64::NEG_INFINITY;
394 for i in 0..8 {
395 let corner = [
396 if i & 1 == 0 { bbox.min[0] } else { bbox.max[0] },
397 if i & 2 == 0 { bbox.min[1] } else { bbox.max[1] },
398 if i & 4 == 0 { bbox.min[2] } else { bbox.max[2] },
399 ];
400 let d = dot3(sub3(corner, self.eye), forward);
401 min_d = min_d.min(d);
402 max_d = max_d.max(d);
403 }
404 let diag = len3(sub3(bbox.max, bbox.min));
405 let pad = ((max_d - min_d) * 0.1).max(diag * 0.1).max(0.5);
406 match self.projection {
407 Projection::Orthographic { .. } => {
408 self.near = min_d - pad;
409 self.far = max_d + pad;
410 }
411 Projection::Perspective { .. } => {
412 let far = (max_d + pad).max(1.0);
413 self.near = (far * 0.001).clamp(1e-4, 1.0).min((min_d - pad).max(1e-4));
414 self.far = far;
415 }
416 }
417 }
418
419 pub fn zoom_to_fit(&mut self, bbox: &Aabb, margin: f64) {
423 if bbox.is_empty() {
424 return;
425 }
426 let margin = margin.max(1.0);
427 let (right, up, forward) = self.basis();
428 let center = bbox.center();
429 let mut half_w = 0.0f64;
430 let mut half_h = 0.0f64;
431 for i in 0..8 {
432 let corner = [
433 if i & 1 == 0 { bbox.min[0] } else { bbox.max[0] },
434 if i & 2 == 0 { bbox.min[1] } else { bbox.max[1] },
435 if i & 4 == 0 { bbox.min[2] } else { bbox.max[2] },
436 ];
437 let rel = sub3(corner, center);
438 half_w = half_w.max(dot3(rel, right).abs());
439 half_h = half_h.max(dot3(rel, up).abs());
440 }
441 half_w = (half_w * margin).max(1e-6);
442 half_h = (half_h * margin).max(1e-6);
443
444 let dist = self.distance();
445 let aspect = self.aspect();
446 self.target = center;
447 match self.projection {
448 Projection::Orthographic { ref mut half_height } => {
449 *half_height = half_h.max(half_w / aspect);
450 self.eye = sub3(center, scale3(forward, dist));
451 }
452 Projection::Perspective { fov_y_deg } => {
453 let fov = fov_y_deg.to_radians();
454 let dist_h = half_h / (fov * 0.5).tan().max(1e-6);
455 let tan_half_h_fov = (fov * 0.5).tan() * aspect;
456 let dist_w = half_w / tan_half_h_fov.max(1e-6);
457 let target_dist = dist_h.max(dist_w).max(1e-3);
458 self.eye = sub3(center, scale3(forward, target_dist));
459 }
460 }
461 self.fit_depth_range(bbox);
462 }
463
464 pub fn toggle_projection(&mut self) -> &'static str {
467 const FOV: f64 = 50.0;
468 let forward = norm3(sub3(self.target, self.eye));
469 match self.projection {
470 Projection::Orthographic { half_height } => {
471 let denom = (FOV.to_radians() * 0.5).tan();
472 let mut distance = half_height / denom.max(1e-9);
473 if !distance.is_finite() || distance < 1e-4 {
474 distance = 10.0;
475 }
476 self.eye = sub3(self.target, scale3(forward, distance));
477 self.projection = Projection::Perspective { fov_y_deg: FOV };
478 "perspective"
479 }
480 Projection::Perspective { fov_y_deg } => {
481 let dist = self.distance();
482 let half_height = ((fov_y_deg.to_radians() * 0.5).tan() * dist).max(1e-6);
483 self.projection = Projection::Orthographic { half_height };
484 "orthographic"
485 }
486 }
487 }
488
489 pub fn standard_view(&mut self, name: &str) -> bool {
492 let dist = self.distance();
493 let iso = norm3([1.0, 1.0, 1.0]);
494 let (dir, up): ([f64; 3], [f64; 3]) = match name.to_ascii_uppercase().as_str() {
495 "FRONT" => ([0.0, 0.0, 1.0], [0.0, 1.0, 0.0]),
496 "BACK" => ([0.0, 0.0, -1.0], [0.0, 1.0, 0.0]),
497 "RIGHT" => ([1.0, 0.0, 0.0], [0.0, 1.0, 0.0]),
498 "LEFT" => ([-1.0, 0.0, 0.0], [0.0, 1.0, 0.0]),
499 "TOP" => ([0.0, 1.0, 0.0], [0.0, 0.0, -1.0]),
500 "BOTTOM" => ([0.0, -1.0, 0.0], [0.0, 0.0, 1.0]),
501 "ISO" => (iso, [0.0, 1.0, 0.0]),
502 _ => return false,
503 };
504 self.eye = add3(self.target, scale3(dir, dist));
505 self.up = up;
506 true
507 }
508
509 pub fn state_json(&self) -> String {
511 let (kind, scale) = match self.projection {
512 Projection::Orthographic { half_height } => ("orthographic", half_height),
513 Projection::Perspective { fov_y_deg } => ("perspective", fov_y_deg),
514 };
515 serde_json::json!({
516 "kind": kind,
517 "eye": self.eye,
518 "target": self.target,
519 "up": self.up,
520 "scale": scale,
522 "near": self.near,
523 "far": self.far,
524 "width": self.width,
525 "height": self.height,
526 "worldPerPixel": self.world_per_pixel(),
527 })
528 .to_string()
529 }
530
531 pub fn apply_state_json(&mut self, json: &str) -> Result<(), String> {
534 let value: serde_json::Value =
535 serde_json::from_str(json).map_err(|error| format!("camera state parse: {error}"))?;
536 let vec3 = |key: &str| -> Option<[f64; 3]> {
537 let arr = value.get(key)?.as_array()?;
538 Some([arr.first()?.as_f64()?, arr.get(1)?.as_f64()?, arr.get(2)?.as_f64()?])
539 };
540 if let Some(eye) = vec3("eye") {
541 self.eye = eye;
542 }
543 if let Some(target) = vec3("target") {
544 self.target = target;
545 }
546 if let Some(up) = vec3("up") {
547 self.up = up;
548 }
549 let scale = value.get("scale").and_then(|v| v.as_f64());
550 match value.get("kind").and_then(|v| v.as_str()) {
551 Some("perspective") => {
552 self.projection = Projection::Perspective {
553 fov_y_deg: scale.unwrap_or(50.0),
554 }
555 }
556 Some("orthographic") => {
557 self.projection = Projection::Orthographic {
558 half_height: scale.unwrap_or(10.0).max(1e-9),
559 }
560 }
561 _ => {}
562 }
563 if let Some(near) = value.get("near").and_then(|v| v.as_f64()) {
564 self.near = near;
565 }
566 if let Some(far) = value.get("far").and_then(|v| v.as_f64()) {
567 self.far = far;
568 }
569 Ok(())
570 }
571}
572
573impl brep_gizmos::hit_region::RegionCamera for ViewCamera {
577 fn is_orthographic(&self) -> bool {
578 matches!(self.projection, Projection::Orthographic { .. })
579 }
580 fn depth(&self, p: [f64; 3]) -> f64 {
581 self.view_depth(p)
582 }
583 fn project_px(&self, p: [f64; 3]) -> Option<[f32; 2]> {
584 if !self.projectable(p) {
587 return None;
588 }
589 let (sx, sy, _) = self.project(p);
590 Some([sx as f32, sy as f32])
591 }
592}
593
594