Another project
1use nalgebra::{Quaternion, Unit, UnitQuaternion, Vector3 as NVec3};
2use serde::{Deserialize, Serialize};
3use uom::si::angle::{degree, radian};
4use uom::si::f64::{Angle, Length};
5use uom::si::length::millimeter;
6
7use crate::dimensioned_serde;
8use crate::{AngleTolerance, AxisAngle, Point3, Result, Tolerance, TypesError, UnitVec3};
9
10const CAMERA_MIN_SEPARATION: Tolerance = Tolerance::new(1e-9);
11const CAMERA_MIN_UP_VIEW_ANGLE: AngleTolerance = AngleTolerance::from_radians(1e-9);
12
13fn point_is_finite(p: Point3) -> bool {
14 let (x, y, z) = p.coords_mm();
15 x.is_finite() && y.is_finite() && z.is_finite()
16}
17
18#[derive(Copy, Clone, Debug, PartialEq)]
19pub enum ProjectionKind {
20 Orthographic {
21 half_height: Length,
22 },
23 Perspective {
24 fov: Angle,
25 near: Length,
26 far: Length,
27 },
28}
29
30#[derive(Copy, Clone, PartialEq, Serialize, Deserialize)]
31#[serde(try_from = "ProjectionWire", into = "ProjectionWire")]
32pub struct Projection(ProjectionKind);
33
34#[derive(Serialize, Deserialize)]
35#[serde(rename = "Projection", deny_unknown_fields)]
36enum ProjectionWire {
37 Orthographic {
38 #[serde(with = "dimensioned_serde::length_si")]
39 half_height: Length,
40 },
41 Perspective {
42 #[serde(with = "dimensioned_serde::angle_si")]
43 fov: Angle,
44 #[serde(with = "dimensioned_serde::length_si")]
45 near: Length,
46 #[serde(with = "dimensioned_serde::length_si")]
47 far: Length,
48 },
49}
50
51impl From<Projection> for ProjectionWire {
52 fn from(p: Projection) -> Self {
53 match p.0 {
54 ProjectionKind::Orthographic { half_height } => Self::Orthographic { half_height },
55 ProjectionKind::Perspective { fov, near, far } => Self::Perspective { fov, near, far },
56 }
57 }
58}
59
60impl TryFrom<ProjectionWire> for Projection {
61 type Error = TypesError;
62 fn try_from(w: ProjectionWire) -> Result<Self> {
63 match w {
64 ProjectionWire::Orthographic { half_height } => Self::orthographic(half_height),
65 ProjectionWire::Perspective { fov, near, far } => Self::perspective(fov, near, far),
66 }
67 }
68}
69
70impl Projection {
71 pub fn orthographic(half_height: Length) -> Result<Self> {
72 let kind = ProjectionKind::Orthographic { half_height };
73 kind.validate()?;
74 Ok(Self(kind))
75 }
76
77 pub fn perspective(fov: Angle, near: Length, far: Length) -> Result<Self> {
78 let kind = ProjectionKind::Perspective { fov, near, far };
79 kind.validate()?;
80 Ok(Self(kind))
81 }
82
83 #[must_use]
84 pub fn kind(self) -> ProjectionKind {
85 self.0
86 }
87}
88
89impl ProjectionKind {
90 fn validate(self) -> Result<()> {
91 match self {
92 Self::Orthographic { half_height } => {
93 let h = half_height.get::<millimeter>();
94 if !h.is_finite() || h <= 0.0 {
95 return Err(TypesError::NonPositiveOrthoHeight(h));
96 }
97 }
98 Self::Perspective { fov, near, far } => {
99 let f = fov.get::<radian>();
100 if !f.is_finite() || f <= 0.0 || f >= core::f64::consts::PI {
101 return Err(TypesError::FovOutOfRange(f));
102 }
103 let n = near.get::<millimeter>();
104 if !n.is_finite() || n <= 0.0 {
105 return Err(TypesError::NonPositiveNear(n));
106 }
107 let fa = far.get::<millimeter>();
108 if !fa.is_finite() || fa <= n {
109 return Err(TypesError::FarNotBeyondNear { near: n, far: fa });
110 }
111 }
112 }
113 Ok(())
114 }
115}
116
117impl core::fmt::Debug for Projection {
118 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
119 match self.0 {
120 ProjectionKind::Orthographic { half_height } => write!(
121 f,
122 "Orthographic {{ half_height: {} mm }}",
123 half_height.get::<millimeter>()
124 ),
125 ProjectionKind::Perspective { fov, near, far } => write!(
126 f,
127 "Perspective {{ fov: {} deg, near: {} mm, far: {} mm }}",
128 fov.get::<degree>(),
129 near.get::<millimeter>(),
130 far.get::<millimeter>()
131 ),
132 }
133 }
134}
135
136impl core::fmt::Display for Projection {
137 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
138 match self.0 {
139 ProjectionKind::Orthographic { half_height } => {
140 write!(
141 f,
142 "ortho{{ half_height={} mm }}",
143 half_height.get::<millimeter>()
144 )
145 }
146 ProjectionKind::Perspective { fov, near, far } => write!(
147 f,
148 "persp{{ fov={} deg, near={} mm, far={} mm }}",
149 fov.get::<degree>(),
150 near.get::<millimeter>(),
151 far.get::<millimeter>()
152 ),
153 }
154 }
155}
156
157#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
158#[serde(try_from = "Camera3Wire", into = "Camera3Wire")]
159pub struct Camera3 {
160 eye: Point3,
161 target: Point3,
162 up: UnitVec3,
163 projection: Projection,
164}
165
166#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
167#[serde(rename = "Camera3", deny_unknown_fields)]
168struct Camera3Wire {
169 eye: Point3,
170 target: Point3,
171 up: UnitVec3,
172 projection: Projection,
173}
174
175impl From<Camera3> for Camera3Wire {
176 fn from(c: Camera3) -> Self {
177 Self {
178 eye: c.eye,
179 target: c.target,
180 up: c.up,
181 projection: c.projection,
182 }
183 }
184}
185
186impl TryFrom<Camera3Wire> for Camera3 {
187 type Error = TypesError;
188 fn try_from(w: Camera3Wire) -> Result<Self> {
189 Self::new(w.eye, w.target, w.up, w.projection)
190 }
191}
192
193impl Camera3 {
194 pub fn new(eye: Point3, target: Point3, up: UnitVec3, projection: Projection) -> Result<Self> {
195 let offset = target - eye;
196 let separation = offset.norm_mm();
197 if !separation.is_finite() {
198 return Err(TypesError::CameraNotFinite);
199 }
200 if separation <= CAMERA_MIN_SEPARATION.value() {
201 return Err(TypesError::DegenerateCamera(separation));
202 }
203 let (ox, oy, oz) = offset.coords_mm();
204 let view = UnitVec3::new_unchecked(ox / separation, oy / separation, oz / separation);
205 let (ux, uy, uz) = up.components();
206 let (vx, vy, vz) = view.components();
207 let up_cross = NVec3::new(uy * vz - uz * vy, uz * vx - ux * vz, ux * vy - uy * vx).norm();
208 if up_cross <= CAMERA_MIN_UP_VIEW_ANGLE.radians() {
209 return Err(TypesError::CameraUpParallelToView);
210 }
211 Ok(Self {
212 eye,
213 target,
214 up,
215 projection,
216 })
217 }
218
219 #[must_use]
220 pub fn eye(self) -> Point3 {
221 self.eye
222 }
223
224 #[must_use]
225 pub fn target(self) -> Point3 {
226 self.target
227 }
228
229 #[must_use]
230 pub fn up(self) -> UnitVec3 {
231 self.up
232 }
233
234 #[must_use]
235 pub fn projection(self) -> Projection {
236 self.projection
237 }
238}
239
240impl core::fmt::Display for Camera3 {
241 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
242 write!(
243 f,
244 "camera{{ eye={}, target={}, up={}, {} }}",
245 self.eye, self.target, self.up, self.projection
246 )
247 }
248}
249
250#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Serialize, Deserialize)]
251#[serde(try_from = "f64", into = "f64")]
252pub struct ZoomFactor(f64);
253
254impl ZoomFactor {
255 pub const IDENTITY: Self = Self(1.0);
256
257 pub fn new(value: f64) -> Result<Self> {
258 if !value.is_finite() || value <= 0.0 {
259 return Err(TypesError::NonPositiveZoom(value));
260 }
261 Ok(Self(value))
262 }
263
264 #[must_use]
265 pub const fn value(self) -> f64 {
266 self.0
267 }
268}
269
270impl TryFrom<f64> for ZoomFactor {
271 type Error = TypesError;
272 fn try_from(value: f64) -> Result<Self> {
273 Self::new(value)
274 }
275}
276
277impl From<ZoomFactor> for f64 {
278 fn from(zoom: ZoomFactor) -> Self {
279 zoom.0
280 }
281}
282
283impl core::fmt::Display for ZoomFactor {
284 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
285 write!(f, "zoom={}", self.0)
286 }
287}
288
289#[derive(Copy, Clone, Debug, PartialEq)]
290pub struct CubicEasing {
291 x1: f64,
292 y1: f64,
293 x2: f64,
294 y2: f64,
295}
296
297impl CubicEasing {
298 pub const LINEAR: Self = Self {
299 x1: 0.0,
300 y1: 0.0,
301 x2: 1.0,
302 y2: 1.0,
303 };
304
305 pub const STANDARD: Self = Self {
306 x1: 0.2,
307 y1: 0.0,
308 x2: 0.0,
309 y2: 1.0,
310 };
311
312 pub fn new(x1: f64, y1: f64, x2: f64, y2: f64) -> Result<Self> {
313 let finite = [x1, y1, x2, y2].iter().all(|v| v.is_finite());
314 if !finite || !(0.0..=1.0).contains(&x1) || !(0.0..=1.0).contains(&x2) {
315 return Err(TypesError::InvalidEasingControl { x1, y1, x2, y2 });
316 }
317 Ok(Self { x1, y1, x2, y2 })
318 }
319
320 #[must_use]
321 pub const fn new_unchecked(x1: f64, y1: f64, x2: f64, y2: f64) -> Self {
322 Self { x1, y1, x2, y2 }
323 }
324
325 #[must_use]
326 pub const fn x1(self) -> f64 {
327 self.x1
328 }
329
330 #[must_use]
331 pub const fn y1(self) -> f64 {
332 self.y1
333 }
334
335 #[must_use]
336 pub const fn x2(self) -> f64 {
337 self.x2
338 }
339
340 #[must_use]
341 pub const fn y2(self) -> f64 {
342 self.y2
343 }
344}
345
346fn quat_to_axis_angle(q: UnitQuaternion<f64>) -> AxisAngle {
347 match q.axis_angle() {
348 Some((axis, angle)) => AxisAngle::new(
349 UnitVec3::new_unchecked(axis.x, axis.y, axis.z),
350 Angle::new::<radian>(angle),
351 ),
352 None => AxisAngle::new(UnitVec3::z_axis(), Angle::new::<radian>(0.0)),
353 }
354}
355
356#[derive(Copy, Clone, PartialEq, Serialize, Deserialize)]
357#[serde(try_from = "OrbitStateWire", into = "OrbitStateWire")]
358pub struct OrbitState {
359 rotation: UnitQuaternion<f64>,
360 zoom: ZoomFactor,
361 pan_target: Point3,
362}
363
364#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
365#[serde(rename = "OrbitState", deny_unknown_fields)]
366struct OrbitStateWire {
367 qw: f64,
368 qx: f64,
369 qy: f64,
370 qz: f64,
371 zoom: ZoomFactor,
372 pan_target: Point3,
373}
374
375impl From<OrbitState> for OrbitStateWire {
376 fn from(o: OrbitState) -> Self {
377 let q = o.rotation.quaternion();
378 Self {
379 qw: q.w,
380 qx: q.i,
381 qy: q.j,
382 qz: q.k,
383 zoom: o.zoom,
384 pan_target: o.pan_target,
385 }
386 }
387}
388
389impl TryFrom<OrbitStateWire> for OrbitState {
390 type Error = TypesError;
391 fn try_from(w: OrbitStateWire) -> Result<Self> {
392 if !(w.qw.is_finite() && w.qx.is_finite() && w.qy.is_finite() && w.qz.is_finite()) {
393 return Err(TypesError::OrbitRotationNotFinite);
394 }
395 let rotation = Unit::try_new(Quaternion::new(w.qw, w.qx, w.qy, w.qz), f64::EPSILON)
396 .ok_or(TypesError::ZeroLengthAxis)?;
397 if !point_is_finite(w.pan_target) {
398 return Err(TypesError::OrbitPanTargetNotFinite);
399 }
400 Ok(Self {
401 rotation,
402 zoom: w.zoom,
403 pan_target: w.pan_target,
404 })
405 }
406}
407
408impl OrbitState {
409 #[must_use]
410 pub fn identity() -> Self {
411 Self {
412 rotation: UnitQuaternion::identity(),
413 zoom: ZoomFactor::IDENTITY,
414 pan_target: Point3::origin(),
415 }
416 }
417
418 pub fn new(rotation: AxisAngle, zoom: ZoomFactor, pan_target: Point3) -> Result<Self> {
419 if !rotation.angle().get::<radian>().is_finite() {
420 return Err(TypesError::OrbitRotationNotFinite);
421 }
422 if !point_is_finite(pan_target) {
423 return Err(TypesError::OrbitPanTargetNotFinite);
424 }
425 Ok(Self {
426 rotation: rotation.to_unit_quaternion(),
427 zoom,
428 pan_target,
429 })
430 }
431
432 #[must_use]
433 pub fn rotation(self) -> AxisAngle {
434 quat_to_axis_angle(self.rotation)
435 }
436
437 #[must_use]
438 pub fn zoom(self) -> ZoomFactor {
439 self.zoom
440 }
441
442 #[must_use]
443 pub fn pan_target(self) -> Point3 {
444 self.pan_target
445 }
446
447 #[must_use]
448 pub fn rotated(self, delta: AxisAngle) -> Self {
449 let combined = delta.to_unit_quaternion() * self.rotation;
450 Self {
451 rotation: UnitQuaternion::new_normalize(combined.into_inner()),
452 zoom: self.zoom,
453 pan_target: self.pan_target,
454 }
455 }
456}
457
458impl core::fmt::Debug for OrbitState {
459 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
460 write!(
461 f,
462 "OrbitState {{ rotation: {:?}, zoom: {}, pan_target: {:?} }}",
463 self.rotation(),
464 self.zoom,
465 self.pan_target
466 )
467 }
468}
469
470impl core::fmt::Display for OrbitState {
471 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
472 write!(
473 f,
474 "orbit{{ rot={}, {}, target={} }}",
475 self.rotation(),
476 self.zoom,
477 self.pan_target
478 )
479 }
480}
481
482#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
483pub enum StandardView {
484 Front,
485 Back,
486 Left,
487 Right,
488 Top,
489 Bottom,
490 Isometric,
491 NormalTo,
492}
493
494impl StandardView {
495 #[must_use]
496 pub const fn label(self) -> &'static str {
497 match self {
498 Self::Front => "front",
499 Self::Back => "back",
500 Self::Left => "left",
501 Self::Right => "right",
502 Self::Top => "top",
503 Self::Bottom => "bottom",
504 Self::Isometric => "isometric",
505 Self::NormalTo => "normal_to",
506 }
507 }
508}
509
510impl core::fmt::Display for StandardView {
511 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
512 f.write_str(self.label())
513 }
514}