Another project
0

Configure Feed

Select the types of activity you want to include in your feed.

at main 14 kB View raw
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}