Another project
0

Configure Feed

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

feat(types): 3d space & geometry newtypes

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

author
Lewis
date (May 23, 2026, 2:19 PM +0300) commit 0946c999 parent 6bf815a0 change-id nqnkunqw
+560 -19
+560 -19
crates/bone-types/src/space.rs
··· 1 1 use nalgebra::{Point2 as NPoint2, Point3 as NPoint3, Unit, Vector2 as NVec2, Vector3 as NVec3}; 2 2 use serde::{Deserialize, Serialize}; 3 - use uom::si::f64::Length; 3 + use uom::si::angle::radian; 4 + use uom::si::f64::{Angle, Length}; 4 5 use uom::si::length::millimeter; 5 6 6 7 use crate::{Result, Tolerance, TypesError}; ··· 8 9 #[must_use] 9 10 fn mm(value: f64) -> Length { 10 11 Length::new::<millimeter>(value) 12 + } 13 + 14 + const PLANE_ORTHOGONALITY_TOLERANCE: Tolerance = Tolerance::new(1e-9); 15 + 16 + #[must_use] 17 + fn without_neg_zero(value: f64) -> f64 { 18 + if value == 0.0 { 0.0 } else { value } 11 19 } 12 20 13 21 #[derive(Copy, Clone, PartialEq, Serialize, Deserialize)] ··· 306 314 307 315 impl core::fmt::Display for UnitVec2 { 308 316 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 309 - write!(f, "[{}, {}]", self.0.x, self.0.y) 317 + write!( 318 + f, 319 + "[{}, {}]", 320 + without_neg_zero(self.0.x), 321 + without_neg_zero(self.0.y) 322 + ) 310 323 } 311 324 } 312 325 ··· 372 385 pub fn z(self) -> Length { 373 386 Length::new::<millimeter>(self.0.z) 374 387 } 388 + 389 + #[must_use] 390 + pub fn coords_mm(self) -> (f64, f64, f64) { 391 + (self.0.x, self.0.y, self.0.z) 392 + } 393 + } 394 + 395 + impl core::ops::Sub for Point3 { 396 + type Output = Vec3; 397 + fn sub(self, rhs: Self) -> Vec3 { 398 + Vec3(self.0 - rhs.0) 399 + } 400 + } 401 + 402 + impl core::ops::Add<Vec3> for Point3 { 403 + type Output = Self; 404 + fn add(self, rhs: Vec3) -> Self { 405 + Self(self.0 + rhs.0) 406 + } 407 + } 408 + 409 + impl core::ops::Sub<Vec3> for Point3 { 410 + type Output = Self; 411 + fn sub(self, rhs: Vec3) -> Self { 412 + Self(self.0 - rhs.0) 413 + } 375 414 } 376 415 377 416 impl core::fmt::Debug for Point3 { ··· 415 454 impl TryFrom<UnitVec3Wire> for UnitVec3 { 416 455 type Error = TypesError; 417 456 fn try_from(w: UnitVec3Wire) -> Result<Self> { 418 - Self::try_from_components(w.x, w.y, w.z) 457 + Self::try_from_components(w.x, w.y, w.z, Tolerance::new(f64::EPSILON)) 419 458 } 420 459 } 421 460 ··· 435 474 Self(Unit::new_unchecked(NVec3::new(0.0, 0.0, 1.0))) 436 475 } 437 476 438 - pub fn try_from_components(x: f64, y: f64, z: f64) -> Result<Self> { 439 - Unit::try_new(NVec3::new(x, y, z), f64::EPSILON) 477 + #[must_use] 478 + pub fn new_unchecked(x: f64, y: f64, z: f64) -> Self { 479 + Self(Unit::new_unchecked(NVec3::new(x, y, z))) 480 + } 481 + 482 + pub fn try_from_components(x: f64, y: f64, z: f64, tolerance: Tolerance) -> Result<Self> { 483 + Unit::try_new(NVec3::new(x, y, z), tolerance.value()) 440 484 .map(Self) 441 485 .ok_or(TypesError::ZeroLengthAxis) 442 486 } ··· 450 494 pub fn dot(self, other: Self) -> f64 { 451 495 self.0.dot(&other.0) 452 496 } 497 + 498 + #[must_use] 499 + pub fn reversed(self) -> Self { 500 + Self(Unit::new_unchecked(-self.0.into_inner())) 501 + } 502 + 503 + #[must_use] 504 + pub fn into_vec(self, length: Length) -> Vec3 { 505 + Vec3(self.0.into_inner() * length.get::<millimeter>()) 506 + } 453 507 } 454 508 455 509 impl core::fmt::Display for UnitVec3 { 456 510 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 457 - write!(f, "[{}, {}, {}]", self.0.x, self.0.y, self.0.z) 511 + write!( 512 + f, 513 + "[{}, {}, {}]", 514 + without_neg_zero(self.0.x), 515 + without_neg_zero(self.0.y), 516 + without_neg_zero(self.0.z) 517 + ) 518 + } 519 + } 520 + 521 + fn orthonormalize(x: UnitVec3, y: UnitVec3, tolerance: Tolerance) -> Result<(UnitVec3, UnitVec3)> { 522 + let dot = x.dot(y); 523 + if dot.abs() > tolerance.value() { 524 + return Err(TypesError::NonOrthogonalPlaneAxes(dot.abs())); 458 525 } 526 + let y_projected = y.0.into_inner() - x.0.into_inner() * dot; 527 + let y_orthonormal = 528 + UnitVec3(Unit::try_new(y_projected, f64::EPSILON).ok_or(TypesError::ZeroLengthAxis)?); 529 + Ok((x, y_orthonormal)) 459 530 } 460 531 461 532 #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] ··· 487 558 impl TryFrom<SketchPlaneBasisWire> for SketchPlaneBasis { 488 559 type Error = TypesError; 489 560 fn try_from(w: SketchPlaneBasisWire) -> Result<Self> { 490 - Self::new(w.origin, w.x_axis, w.y_axis, Tolerance::new(1e-9)) 561 + Self::new(w.origin, w.x_axis, w.y_axis, PLANE_ORTHOGONALITY_TOLERANCE) 491 562 } 492 563 } 493 564 494 565 impl SketchPlaneBasis { 495 566 pub fn new(origin: Point3, x: UnitVec3, y: UnitVec3, tolerance: Tolerance) -> Result<Self> { 496 - let dot = x.dot(y); 497 - if dot.abs() > tolerance.value() { 498 - return Err(TypesError::NonOrthogonalPlaneAxes(dot.abs())); 499 - } 500 - let y_projected = y.0.into_inner() - x.0.into_inner() * dot; 501 - let y_orthonormal = 502 - UnitVec3(Unit::try_new(y_projected, f64::EPSILON).ok_or(TypesError::ZeroLengthAxis)?); 503 - Ok(Self { 504 - origin, 505 - x, 506 - y: y_orthonormal, 507 - }) 567 + let (x, y) = orthonormalize(x, y, tolerance)?; 568 + Ok(Self { origin, x, y }) 508 569 } 509 570 510 571 #[must_use] ··· 537 598 ) 538 599 } 539 600 } 601 + 602 + #[derive(Copy, Clone, PartialEq, Serialize, Deserialize)] 603 + #[serde(from = "Vec3Wire", into = "Vec3Wire")] 604 + pub struct Vec3(NVec3<f64>); 605 + 606 + #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] 607 + #[serde(rename = "Vec3", deny_unknown_fields)] 608 + struct Vec3Wire { 609 + x: f64, 610 + y: f64, 611 + z: f64, 612 + } 613 + 614 + impl From<Vec3> for Vec3Wire { 615 + fn from(v: Vec3) -> Self { 616 + Self { 617 + x: v.0.x, 618 + y: v.0.y, 619 + z: v.0.z, 620 + } 621 + } 622 + } 623 + 624 + impl From<Vec3Wire> for Vec3 { 625 + fn from(w: Vec3Wire) -> Self { 626 + Self(NVec3::new(w.x, w.y, w.z)) 627 + } 628 + } 629 + 630 + impl Vec3 { 631 + #[must_use] 632 + pub fn zero() -> Self { 633 + Self(NVec3::zeros()) 634 + } 635 + 636 + #[must_use] 637 + pub fn from_mm(x: f64, y: f64, z: f64) -> Self { 638 + Self(NVec3::new(x, y, z)) 639 + } 640 + 641 + #[must_use] 642 + pub fn from_lengths(x: Length, y: Length, z: Length) -> Self { 643 + Self(NVec3::new( 644 + x.get::<millimeter>(), 645 + y.get::<millimeter>(), 646 + z.get::<millimeter>(), 647 + )) 648 + } 649 + 650 + #[must_use] 651 + pub fn x(self) -> Length { 652 + mm(self.0.x) 653 + } 654 + 655 + #[must_use] 656 + pub fn y(self) -> Length { 657 + mm(self.0.y) 658 + } 659 + 660 + #[must_use] 661 + pub fn z(self) -> Length { 662 + mm(self.0.z) 663 + } 664 + 665 + #[must_use] 666 + pub fn coords_mm(self) -> (f64, f64, f64) { 667 + (self.0.x, self.0.y, self.0.z) 668 + } 669 + 670 + #[must_use] 671 + pub fn dot_mm2(self, other: Self) -> f64 { 672 + self.0.dot(&other.0) 673 + } 674 + 675 + #[must_use] 676 + pub fn cross(self, other: Self) -> Self { 677 + Self(self.0.cross(&other.0)) 678 + } 679 + 680 + #[must_use] 681 + pub fn norm_squared_mm2(self) -> f64 { 682 + self.0.norm_squared() 683 + } 684 + 685 + #[must_use] 686 + pub fn norm_mm(self) -> f64 { 687 + self.0.norm() 688 + } 689 + 690 + #[must_use] 691 + pub fn norm(self) -> Length { 692 + mm(self.0.norm()) 693 + } 694 + 695 + pub fn try_normalize(self, tolerance: Tolerance) -> Result<UnitVec3> { 696 + Unit::try_new(self.0, tolerance.value()) 697 + .map(UnitVec3) 698 + .ok_or(TypesError::ZeroLengthAxis) 699 + } 700 + } 701 + 702 + impl core::fmt::Debug for Vec3 { 703 + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 704 + write!(f, "Vec3({} mm, {} mm, {} mm)", self.0.x, self.0.y, self.0.z) 705 + } 706 + } 707 + 708 + impl core::fmt::Display for Vec3 { 709 + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 710 + write!(f, "<{} mm, {} mm, {} mm>", self.0.x, self.0.y, self.0.z) 711 + } 712 + } 713 + 714 + impl core::ops::Add for Vec3 { 715 + type Output = Self; 716 + fn add(self, rhs: Self) -> Self { 717 + Self(self.0 + rhs.0) 718 + } 719 + } 720 + 721 + impl core::ops::Sub for Vec3 { 722 + type Output = Self; 723 + fn sub(self, rhs: Self) -> Self { 724 + Self(self.0 - rhs.0) 725 + } 726 + } 727 + 728 + impl core::ops::Mul<f64> for Vec3 { 729 + type Output = Self; 730 + fn mul(self, rhs: f64) -> Self { 731 + Self(self.0 * rhs) 732 + } 733 + } 734 + 735 + impl core::ops::Mul<Vec3> for f64 { 736 + type Output = Vec3; 737 + fn mul(self, rhs: Vec3) -> Vec3 { 738 + Vec3(rhs.0 * self) 739 + } 740 + } 741 + 742 + impl core::ops::Neg for Vec3 { 743 + type Output = Self; 744 + fn neg(self) -> Self { 745 + Self(-self.0) 746 + } 747 + } 748 + 749 + #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] 750 + #[serde(from = "Aabb3Wire", into = "Aabb3Wire")] 751 + pub struct Aabb3 { 752 + min: Point3, 753 + max: Point3, 754 + } 755 + 756 + #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] 757 + #[serde(rename = "Aabb3", deny_unknown_fields)] 758 + struct Aabb3Wire { 759 + min: Point3, 760 + max: Point3, 761 + } 762 + 763 + impl From<Aabb3> for Aabb3Wire { 764 + fn from(b: Aabb3) -> Self { 765 + Self { 766 + min: b.min, 767 + max: b.max, 768 + } 769 + } 770 + } 771 + 772 + impl From<Aabb3Wire> for Aabb3 { 773 + fn from(w: Aabb3Wire) -> Self { 774 + Self::from_corners(w.min, w.max) 775 + } 776 + } 777 + 778 + impl Aabb3 { 779 + #[must_use] 780 + pub fn from_corners(a: Point3, b: Point3) -> Self { 781 + let (ax, ay, az) = a.coords_mm(); 782 + let (bx, by, bz) = b.coords_mm(); 783 + Self { 784 + min: Point3::from_mm(ax.min(bx), ay.min(by), az.min(bz)), 785 + max: Point3::from_mm(ax.max(bx), ay.max(by), az.max(bz)), 786 + } 787 + } 788 + 789 + #[must_use] 790 + pub fn from_points(points: impl IntoIterator<Item = Point3>) -> Option<Self> { 791 + points 792 + .into_iter() 793 + .map(|p| Self { min: p, max: p }) 794 + .reduce(Self::union) 795 + } 796 + 797 + #[must_use] 798 + pub fn min(self) -> Point3 { 799 + self.min 800 + } 801 + 802 + #[must_use] 803 + pub fn max(self) -> Point3 { 804 + self.max 805 + } 806 + 807 + #[must_use] 808 + pub fn center(self) -> Point3 { 809 + let (lx, ly, lz) = self.min.coords_mm(); 810 + let (hx, hy, hz) = self.max.coords_mm(); 811 + Point3::from_mm( 812 + f64::midpoint(lx, hx), 813 + f64::midpoint(ly, hy), 814 + f64::midpoint(lz, hz), 815 + ) 816 + } 817 + 818 + #[must_use] 819 + pub fn extent(self) -> Vec3 { 820 + self.max - self.min 821 + } 822 + 823 + #[must_use] 824 + pub fn union(self, other: Self) -> Self { 825 + let (lx, ly, lz) = self.min.coords_mm(); 826 + let (hx, hy, hz) = self.max.coords_mm(); 827 + let (olx, oly, olz) = other.min.coords_mm(); 828 + let (ohx, ohy, ohz) = other.max.coords_mm(); 829 + Self { 830 + min: Point3::from_mm(lx.min(olx), ly.min(oly), lz.min(olz)), 831 + max: Point3::from_mm(hx.max(ohx), hy.max(ohy), hz.max(ohz)), 832 + } 833 + } 834 + 835 + #[must_use] 836 + pub fn contains(self, p: Point3) -> bool { 837 + let (lx, ly, lz) = self.min.coords_mm(); 838 + let (hx, hy, hz) = self.max.coords_mm(); 839 + let (px, py, pz) = p.coords_mm(); 840 + (lx..=hx).contains(&px) && (ly..=hy).contains(&py) && (lz..=hz).contains(&pz) 841 + } 842 + } 843 + 844 + impl core::fmt::Display for Aabb3 { 845 + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 846 + write!(f, "aabb[{}..{}]", self.min, self.max) 847 + } 848 + } 849 + 850 + #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] 851 + #[serde(try_from = "Plane3Wire", into = "Plane3Wire")] 852 + pub struct Plane3 { 853 + origin: Point3, 854 + x: UnitVec3, 855 + y: UnitVec3, 856 + } 857 + 858 + #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] 859 + #[serde(rename = "Plane3", deny_unknown_fields)] 860 + struct Plane3Wire { 861 + origin: Point3, 862 + x_axis: UnitVec3, 863 + y_axis: UnitVec3, 864 + } 865 + 866 + impl From<Plane3> for Plane3Wire { 867 + fn from(p: Plane3) -> Self { 868 + Self { 869 + origin: p.origin, 870 + x_axis: p.x, 871 + y_axis: p.y, 872 + } 873 + } 874 + } 875 + 876 + impl TryFrom<Plane3Wire> for Plane3 { 877 + type Error = TypesError; 878 + fn try_from(w: Plane3Wire) -> Result<Self> { 879 + Self::new(w.origin, w.x_axis, w.y_axis, PLANE_ORTHOGONALITY_TOLERANCE) 880 + } 881 + } 882 + 883 + impl Plane3 { 884 + pub fn new(origin: Point3, x: UnitVec3, y: UnitVec3, tolerance: Tolerance) -> Result<Self> { 885 + let (x, y) = orthonormalize(x, y, tolerance)?; 886 + Ok(Self { origin, x, y }) 887 + } 888 + 889 + #[must_use] 890 + pub fn new_unchecked(origin: Point3, x: UnitVec3, y: UnitVec3) -> Self { 891 + debug_assert!( 892 + x.dot(y).abs() <= PLANE_ORTHOGONALITY_TOLERANCE.value(), 893 + "Plane3::new_unchecked requires orthonormal axes" 894 + ); 895 + Self { origin, x, y } 896 + } 897 + 898 + #[must_use] 899 + pub fn origin(self) -> Point3 { 900 + self.origin 901 + } 902 + 903 + #[must_use] 904 + pub fn x_axis(self) -> UnitVec3 { 905 + self.x 906 + } 907 + 908 + #[must_use] 909 + pub fn y_axis(self) -> UnitVec3 { 910 + self.y 911 + } 912 + 913 + #[must_use] 914 + pub fn normal(self) -> UnitVec3 { 915 + UnitVec3(Unit::new_normalize(self.x.0.cross(&self.y.0))) 916 + } 917 + } 918 + 919 + impl core::fmt::Display for Plane3 { 920 + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 921 + write!( 922 + f, 923 + "plane{{ o={}, x={}, y={} }}", 924 + self.origin, self.x, self.y, 925 + ) 926 + } 927 + } 928 + 929 + #[derive(Copy, Clone, PartialEq, Serialize, Deserialize)] 930 + #[serde(from = "AxisAngleWire", into = "AxisAngleWire")] 931 + pub struct AxisAngle { 932 + axis: UnitVec3, 933 + angle: Angle, 934 + } 935 + 936 + #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] 937 + #[serde(rename = "AxisAngle", deny_unknown_fields)] 938 + struct AxisAngleWire { 939 + axis: UnitVec3, 940 + angle_rad: f64, 941 + } 942 + 943 + impl From<AxisAngle> for AxisAngleWire { 944 + fn from(a: AxisAngle) -> Self { 945 + Self { 946 + axis: a.axis, 947 + angle_rad: a.angle.get::<radian>(), 948 + } 949 + } 950 + } 951 + 952 + impl From<AxisAngleWire> for AxisAngle { 953 + fn from(w: AxisAngleWire) -> Self { 954 + Self { 955 + axis: w.axis, 956 + angle: Angle::new::<radian>(w.angle_rad), 957 + } 958 + } 959 + } 960 + 961 + impl AxisAngle { 962 + #[must_use] 963 + pub fn new(axis: UnitVec3, angle: Angle) -> Self { 964 + Self { axis, angle } 965 + } 966 + 967 + #[must_use] 968 + pub fn axis(self) -> UnitVec3 { 969 + self.axis 970 + } 971 + 972 + #[must_use] 973 + pub fn angle(self) -> Angle { 974 + self.angle 975 + } 976 + } 977 + 978 + impl core::fmt::Debug for AxisAngle { 979 + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 980 + write!( 981 + f, 982 + "AxisAngle(axis={:?}, angle={} rad)", 983 + self.axis, 984 + self.angle.get::<radian>() 985 + ) 986 + } 987 + } 988 + 989 + impl core::fmt::Display for AxisAngle { 990 + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 991 + write!( 992 + f, 993 + "axisangle{{ axis={}, angle={} rad }}", 994 + self.axis, 995 + self.angle.get::<radian>() 996 + ) 997 + } 998 + } 999 + 1000 + #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] 1001 + #[serde(try_from = "OrientedBox3Wire", into = "OrientedBox3Wire")] 1002 + pub struct OrientedBox3 { 1003 + frame: Plane3, 1004 + half_extents: Vec3, 1005 + } 1006 + 1007 + #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] 1008 + #[serde(rename = "OrientedBox3", deny_unknown_fields)] 1009 + struct OrientedBox3Wire { 1010 + frame: Plane3, 1011 + half_extents: Vec3, 1012 + } 1013 + 1014 + impl From<OrientedBox3> for OrientedBox3Wire { 1015 + fn from(b: OrientedBox3) -> Self { 1016 + Self { 1017 + frame: b.frame, 1018 + half_extents: b.half_extents, 1019 + } 1020 + } 1021 + } 1022 + 1023 + impl TryFrom<OrientedBox3Wire> for OrientedBox3 { 1024 + type Error = TypesError; 1025 + fn try_from(w: OrientedBox3Wire) -> Result<Self> { 1026 + Self::new(w.frame, w.half_extents) 1027 + } 1028 + } 1029 + 1030 + impl OrientedBox3 { 1031 + pub fn new(frame: Plane3, half_extents: Vec3) -> Result<Self> { 1032 + let (hx, hy, hz) = half_extents.coords_mm(); 1033 + match [hx, hy, hz] 1034 + .iter() 1035 + .copied() 1036 + .find(|h| !h.is_finite() || *h < 0.0) 1037 + { 1038 + Some(bad) => Err(TypesError::InvalidHalfExtent(bad)), 1039 + None => Ok(Self { 1040 + frame, 1041 + half_extents, 1042 + }), 1043 + } 1044 + } 1045 + 1046 + #[must_use] 1047 + pub fn from_aabb(aabb: Aabb3) -> Self { 1048 + let frame = Plane3::new_unchecked(aabb.center(), UnitVec3::x_axis(), UnitVec3::y_axis()); 1049 + Self { 1050 + frame, 1051 + half_extents: aabb.extent() * 0.5, 1052 + } 1053 + } 1054 + 1055 + #[must_use] 1056 + pub fn center(self) -> Point3 { 1057 + self.frame.origin() 1058 + } 1059 + 1060 + #[must_use] 1061 + pub fn frame(self) -> Plane3 { 1062 + self.frame 1063 + } 1064 + 1065 + #[must_use] 1066 + pub fn half_extents(self) -> Vec3 { 1067 + self.half_extents 1068 + } 1069 + } 1070 + 1071 + impl core::fmt::Display for OrientedBox3 { 1072 + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 1073 + write!( 1074 + f, 1075 + "obox{{ c={}, half={} }}", 1076 + self.center(), 1077 + self.half_extents 1078 + ) 1079 + } 1080 + }