Another project
0

Configure Feed

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

feat(types): 3d camera & space types

Lewis: May this revision serve well! <lu5a@proton.me>

author
Lewis
date (Jun 5, 2026, 4:01 PM +0300) commit 7215588c parent d460f186 change-id qzlrzxpz
+91 -10
+8 -8
crates/bone-types/src/camera.rs
··· 286 286 } 287 287 } 288 288 289 - fn axis_angle_to_quat(aa: AxisAngle) -> UnitQuaternion<f64> { 290 - let (x, y, z) = aa.axis().components(); 291 - let axis = Unit::new_unchecked(NVec3::new(x, y, z)); 292 - UnitQuaternion::from_axis_angle(&axis, aa.angle().get::<radian>()) 293 - } 294 - 295 289 fn quat_to_axis_angle(q: UnitQuaternion<f64>) -> AxisAngle { 296 290 match q.axis_angle() { 297 291 Some((axis, angle)) => AxisAngle::new( ··· 338 332 impl TryFrom<OrbitStateWire> for OrbitState { 339 333 type Error = TypesError; 340 334 fn try_from(w: OrbitStateWire) -> Result<Self> { 335 + if !(w.qw.is_finite() && w.qx.is_finite() && w.qy.is_finite() && w.qz.is_finite()) { 336 + return Err(TypesError::OrbitRotationNotFinite); 337 + } 341 338 let rotation = Unit::try_new(Quaternion::new(w.qw, w.qx, w.qy, w.qz), f64::EPSILON) 342 339 .ok_or(TypesError::ZeroLengthAxis)?; 343 340 if !point_is_finite(w.pan_target) { ··· 362 359 } 363 360 364 361 pub fn new(rotation: AxisAngle, zoom: ZoomFactor, pan_target: Point3) -> Result<Self> { 362 + if !rotation.angle().get::<radian>().is_finite() { 363 + return Err(TypesError::OrbitRotationNotFinite); 364 + } 365 365 if !point_is_finite(pan_target) { 366 366 return Err(TypesError::OrbitPanTargetNotFinite); 367 367 } 368 368 Ok(Self { 369 - rotation: axis_angle_to_quat(rotation), 369 + rotation: rotation.to_unit_quaternion(), 370 370 zoom, 371 371 pan_target, 372 372 }) ··· 389 389 390 390 #[must_use] 391 391 pub fn rotated(self, delta: AxisAngle) -> Self { 392 - let combined = axis_angle_to_quat(delta) * self.rotation; 392 + let combined = delta.to_unit_quaternion() * self.rotation; 393 393 Self { 394 394 rotation: UnitQuaternion::new_normalize(combined.into_inner()), 395 395 zoom: self.zoom,
+58 -1
crates/bone-types/src/lib.rs
··· 65 65 NonPositiveZoom(f64), 66 66 #[error("orbit pan target is not finite")] 67 67 OrbitPanTargetNotFinite, 68 + #[error("orbit rotation is not finite")] 69 + OrbitRotationNotFinite, 70 + #[error("view-projection matrix is not invertible")] 71 + NonInvertibleViewProjection, 72 + #[error("viewport pixel coordinate is not finite: ({x}, {y})")] 73 + NonFiniteViewportPixel { x: f64, y: f64 }, 74 + #[error("viewport extent has a zero dimension: {width} x {height}")] 75 + ZeroViewportExtent { width: u32, height: u32 }, 68 76 #[error("step entity instance name must be positive")] 69 77 ZeroStepEntityId, 70 78 #[error("length must be finite and positive: {0} m")] ··· 589 597 590 598 #[test] 591 599 fn unit_vec3_deserialize_preserves_irrational_components_bit_exact() { 592 - let Ok(original) = UnitVec3::try_from_components(1.0, 2.0, 3.0, Tolerance::new(1e-9)) else { 600 + let Ok(original) = UnitVec3::try_from_components(1.0, 2.0, 3.0, Tolerance::new(1e-9)) 601 + else { 593 602 panic!("normalizable direction"); 594 603 }; 595 604 let Ok(text) = ron::to_string(&original) else { ··· 1222 1231 assert!(ron::from_str::<OrbitState>(finite).is_ok()); 1223 1232 let non_finite = "(qw:1.0,qx:0.0,qy:0.0,qz:0.0,zoom:1.0,pan_target:(x:0.0,y:0.0,z:inf))"; 1224 1233 assert!(ron::from_str::<OrbitState>(non_finite).is_err()); 1234 + } 1235 + 1236 + #[test] 1237 + fn orbit_state_deserialize_rejects_non_finite_rotation() { 1238 + let inf = "(qw:inf,qx:0.0,qy:0.0,qz:0.0,zoom:1.0,pan_target:(x:0.0,y:0.0,z:0.0))"; 1239 + assert!(ron::from_str::<OrbitState>(inf).is_err()); 1240 + let nan = "(qw:1.0,qx:NaN,qy:0.0,qz:0.0,zoom:1.0,pan_target:(x:0.0,y:0.0,z:0.0))"; 1241 + assert!(ron::from_str::<OrbitState>(nan).is_err()); 1242 + } 1243 + 1244 + #[test] 1245 + fn orbit_state_new_rejects_non_finite_rotation() { 1246 + let Ok(zoom) = ZoomFactor::new(1.0) else { 1247 + panic!("positive zoom"); 1248 + }; 1249 + let bad = AxisAngle::new(UnitVec3::z_axis(), Angle::new::<radian>(f64::INFINITY)); 1250 + assert!(OrbitState::new(bad, zoom, Point3::origin()).is_err()); 1251 + let good = AxisAngle::new(UnitVec3::z_axis(), Angle::new::<radian>(0.5)); 1252 + assert!(OrbitState::new(good, zoom, Point3::origin()).is_ok()); 1253 + } 1254 + 1255 + #[test] 1256 + fn unit_vec3_rotated_quarter_turn_about_z() { 1257 + let q = AxisAngle::new( 1258 + UnitVec3::z_axis(), 1259 + Angle::new::<radian>(core::f64::consts::FRAC_PI_2), 1260 + ); 1261 + let (cx, cy, cz) = UnitVec3::x_axis().rotated(q).components(); 1262 + assert!( 1263 + cx.abs() < 1e-12 && (cy - 1.0).abs() < 1e-12 && cz.abs() < 1e-12, 1264 + "x rotated +90 deg about z is y: ({cx},{cy},{cz})" 1265 + ); 1266 + } 1267 + 1268 + #[test] 1269 + fn point3_rotated_about_pivot_quarter_turn() { 1270 + let pivot = Point3::from_mm(1.0, 0.0, 0.0); 1271 + let q = AxisAngle::new( 1272 + UnitVec3::z_axis(), 1273 + Angle::new::<radian>(core::f64::consts::FRAC_PI_2), 1274 + ); 1275 + let (x, y, z) = Point3::from_mm(2.0, 0.0, 0.0) 1276 + .rotated_about(pivot, q) 1277 + .coords_mm(); 1278 + assert!( 1279 + (x - 1.0).abs() < 1e-12 && (y - 1.0).abs() < 1e-12 && z.abs() < 1e-12, 1280 + "(2,0,0) about (1,0,0) +90 deg z is (1,1,0): ({x},{y},{z})" 1281 + ); 1225 1282 } 1226 1283 1227 1284 #[test]
+25 -1
crates/bone-types/src/space.rs
··· 1 - use nalgebra::{Point2 as NPoint2, Point3 as NPoint3, Unit, Vector2 as NVec2, Vector3 as NVec3}; 1 + use nalgebra::{ 2 + Point2 as NPoint2, Point3 as NPoint3, Unit, UnitQuaternion, Vector2 as NVec2, Vector3 as NVec3, 3 + }; 2 4 use serde::{Deserialize, Serialize}; 3 5 use uom::si::angle::radian; 4 6 use uom::si::f64::{Angle, Length}; ··· 400 402 pub fn coords_mm(self) -> (f64, f64, f64) { 401 403 (self.0.x, self.0.y, self.0.z) 402 404 } 405 + 406 + #[must_use] 407 + pub fn rotated_about(self, pivot: Point3, by: AxisAngle) -> Self { 408 + let (px, py, pz) = self.coords_mm(); 409 + let (cx, cy, cz) = pivot.coords_mm(); 410 + let r = by.to_unit_quaternion() * NVec3::new(px - cx, py - cy, pz - cz); 411 + Self::from_mm(cx + r.x, cy + r.y, cz + r.z) 412 + } 403 413 } 404 414 405 415 impl core::ops::Sub for Point3 { ··· 515 525 pub fn into_vec(self, length: Length) -> Vec3 { 516 526 Vec3(self.0.into_inner() * length.get::<millimeter>()) 517 527 } 528 + 529 + #[must_use] 530 + pub fn rotated(self, by: AxisAngle) -> Self { 531 + let (x, y, z) = self.components(); 532 + let r = (by.to_unit_quaternion() * NVec3::new(x, y, z)).normalize(); 533 + Self::new_unchecked(r.x, r.y, r.z) 534 + } 518 535 } 519 536 520 537 impl core::fmt::Display for UnitVec3 { ··· 1002 1019 #[must_use] 1003 1020 pub fn angle(self) -> Angle { 1004 1021 self.angle 1022 + } 1023 + 1024 + #[must_use] 1025 + pub(crate) fn to_unit_quaternion(self) -> UnitQuaternion<f64> { 1026 + let (x, y, z) = self.axis.components(); 1027 + let axis = Unit::new_unchecked(NVec3::new(x, y, z)); 1028 + UnitQuaternion::from_axis_angle(&axis, self.angle.get::<radian>()) 1005 1029 } 1006 1030 } 1007 1031