pub use uom::si::angle::{degree, radian};
pub use uom::si::f64::{Angle, Length};
pub use uom::si::length::millimeter;

pub mod camera;
pub mod dimensioned_serde;
pub mod display;
pub mod label;
pub mod schema;
pub mod solver;
pub mod space;
pub mod step;

pub use camera::{Camera3, OrbitState, Projection, ProjectionKind, StandardView, ZoomFactor};
pub use display::{DisplayMode, ShadingModel};
pub use label::{
    EdgeLabel, EdgeRole, FaceLabel, FaceRole, ImportOrdinal, LoopIndex, SideKind, VertexLabel,
    VertexRole,
};
pub use schema::{SchemaHeader, SchemaVersion};
pub use solver::{
    BudgetCeiling, DegreesOfFreedom, NewtonDamping, NewtonStepTolerance, Parameter, ParameterIndex,
    ParentIndex, ResidualIndex, SketchItemId, SketchStatus, SolverResidual, SolverSeed,
};
pub use space::{
    Aabb3, AxisAngle, OrientedBox3, Plane3, Point2, Point3, SketchPlaneBasis, UnitVec2, UnitVec3,
    Vec2, Vec3,
};
pub use step::{
    StepEntityId, StepFileHeader, StepFileName, StepOrganization, StepOriginatingSystem, StepSchema,
};

#[cfg(feature = "testing")]
pub mod testing;

#[derive(Debug, thiserror::Error)]
pub enum TypesError {
    #[error("sketch plane axes not orthogonal: |x·y|={0}")]
    NonOrthogonalPlaneAxes(f64),
    #[error("axis vector is zero-length")]
    ZeroLengthAxis,
    #[error("oriented box half-extent must be finite and non-negative: {0}")]
    InvalidHalfExtent(f64),
    #[error("camera eye and target coincide: separation {0} mm")]
    DegenerateCamera(f64),
    #[error("camera eye or target is not finite")]
    CameraNotFinite,
    #[error("camera up direction is parallel to the view direction")]
    CameraUpParallelToView,
    #[error("orthographic half-height must be finite and positive: {0} mm")]
    NonPositiveOrthoHeight(f64),
    #[error("perspective field of view must be within (0, pi) radians: {0}")]
    FovOutOfRange(f64),
    #[error("perspective near distance must be finite and positive: {0} mm")]
    NonPositiveNear(f64),
    #[error("perspective far distance must exceed the near distance: near {near} mm, far {far} mm")]
    FarNotBeyondNear { near: f64, far: f64 },
    #[error("zoom factor must be finite and positive: {0}")]
    NonPositiveZoom(f64),
    #[error("orbit pan target is not finite")]
    OrbitPanTargetNotFinite,
    #[error("step entity instance name must be positive")]
    ZeroStepEntityId,
}

pub type Result<T, E = TypesError> = core::result::Result<T, E>;

slotmap::new_key_type! {
    pub struct FaceId;
    pub struct EdgeId;
    pub struct VertexId;
    pub struct ShellId;
    pub struct SolidId;
    pub struct LoopId;
    pub struct WireId;
    pub struct FeatureId;
    pub struct NodeId;
    pub struct DocumentId;
    pub struct SketchId;
    pub struct SketchEntityId;
    pub struct SketchRelationId;
    pub struct SketchDimensionId;
    pub struct SketchParameterId;
    pub struct ExtrudeId;
    pub struct BodyId;
    pub struct BrepShellId;
    pub struct BrepFaceId;
    pub struct BrepEdgeId;
    pub struct BrepVertexId;
    pub struct BrepLoopId;
}

impl SketchId {
    /// Encodes this id as an opaque `u64`, stable for the same slotmap slot+version
    /// within a single process. Use only for deterministic widget-key derivation; the
    /// representation is not portable across builds or persisted artifacts.
    #[must_use]
    pub fn as_u64(self) -> u64 {
        use slotmap::Key;
        self.data().as_ffi()
    }
}

#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
pub struct Tolerance(f64);

impl Tolerance {
    #[must_use]
    pub const fn new(value: f64) -> Self {
        Self(value)
    }

    #[must_use]
    pub const fn value(self) -> f64 {
        self.0
    }
}

impl core::fmt::Display for Tolerance {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        write!(f, "tol={}", self.0)
    }
}

#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
pub struct AngleTolerance(f64);

impl AngleTolerance {
    pub const ZERO: Self = Self(0.0);

    #[must_use]
    pub const fn from_radians(value: f64) -> Self {
        Self(value)
    }

    #[must_use]
    pub const fn radians(self) -> f64 {
        self.0
    }

    #[must_use]
    pub fn from_arc_length(linear: Tolerance, radius: Length) -> Self {
        let r = radius.get::<millimeter>().abs();
        if r > linear.value() {
            Self(linear.value() / r)
        } else {
            Self(linear.value())
        }
    }
}

impl core::fmt::Display for AngleTolerance {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        write!(f, "tol_angle={} rad", self.0)
    }
}

#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
pub struct ChordHeightTolerance(Length);

impl ChordHeightTolerance {
    #[must_use]
    pub fn new(length: Length) -> Self {
        Self(length)
    }

    #[must_use]
    pub fn from_mm(value: f64) -> Self {
        Self(Length::new::<millimeter>(value))
    }

    #[must_use]
    pub fn length(self) -> Length {
        self.0
    }

    #[must_use]
    pub fn millimeters(self) -> f64 {
        self.0.get::<millimeter>()
    }
}

impl core::fmt::Display for ChordHeightTolerance {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        write!(f, "chord_tol={} mm", self.0.get::<millimeter>())
    }
}

#[cfg(test)]
mod tests {
    use super::{
        Aabb3, Angle, AngleTolerance, AxisAngle, BodyId, BrepEdgeId, BrepFaceId, BrepLoopId,
        BrepShellId, BrepVertexId, Camera3, ChordHeightTolerance, DegreesOfFreedom, DisplayMode,
        DocumentId, EdgeId, EdgeLabel, EdgeRole, ExtrudeId, FaceId, FaceLabel, FaceRole, FeatureId,
        ImportOrdinal, Length, LoopId, LoopIndex, NodeId, OrbitState, OrientedBox3, Parameter,
        Plane3, Point2, Point3, Projection, ProjectionKind, ShadingModel, ShellId, SideKind,
        SketchDimensionId, SketchEntityId, SketchId, SketchParameterId, SketchPlaneBasis,
        SketchRelationId, SolidId, SolverResidual, StandardView, StepEntityId, StepFileHeader,
        StepFileName, StepOrganization, StepOriginatingSystem, StepSchema, Tolerance, UnitVec2,
        UnitVec3, Vec2, Vec3, VertexId, VertexLabel, VertexRole, WireId, ZoomFactor, degree,
        millimeter, radian,
    };
    use slotmap::Key;

    fn ortho_camera() -> Camera3 {
        let Ok(projection) = Projection::orthographic(Length::new::<millimeter>(25.0)) else {
            panic!("25 mm half-height is positive");
        };
        let Ok(camera) = Camera3::new(
            Point3::from_mm(0.0, 0.0, 10.0),
            Point3::origin(),
            UnitVec3::y_axis(),
            projection,
        ) else {
            panic!("eye and target are 10 mm apart");
        };
        camera
    }

    #[test]
    fn id_null_is_null() {
        assert!(FaceId::null().is_null());
        assert!(EdgeId::null().is_null());
        assert!(VertexId::null().is_null());
        assert!(ShellId::null().is_null());
        assert!(SolidId::null().is_null());
        assert!(LoopId::null().is_null());
        assert!(WireId::null().is_null());
        assert!(FeatureId::null().is_null());
        assert!(NodeId::null().is_null());
        assert!(DocumentId::null().is_null());
        assert!(SketchId::null().is_null());
        assert!(SketchEntityId::null().is_null());
        assert!(SketchRelationId::null().is_null());
        assert!(SketchDimensionId::null().is_null());
        assert!(SketchParameterId::null().is_null());
        assert!(ExtrudeId::null().is_null());
        assert!(BodyId::null().is_null());
        assert!(BrepShellId::null().is_null());
        assert!(BrepFaceId::null().is_null());
        assert!(BrepEdgeId::null().is_null());
        assert!(BrepVertexId::null().is_null());
        assert!(BrepLoopId::null().is_null());
    }

    #[test]
    fn ids_are_distinct_types() {
        let _f: FaceId = FaceId::null();
        let _e: EdgeId = EdgeId::null();
        let _s: SketchEntityId = SketchEntityId::null();
        let _r: SketchRelationId = SketchRelationId::null();
    }

    #[test]
    fn length_mm_roundtrip() {
        let l = Length::new::<millimeter>(12.5);
        assert!((l.get::<millimeter>() - 12.5).abs() < f64::EPSILON);
    }

    #[test]
    fn angle_deg_converts_to_rad() {
        let a = Angle::new::<degree>(180.0);
        assert!((a.get::<radian>() - core::f64::consts::PI).abs() < 1e-12);
    }

    #[test]
    fn tolerance_display() {
        let t = Tolerance::new(1e-6);
        assert_eq!(format!("{t}"), "tol=0.000001");
        assert!((t.value() - 1e-6).abs() < f64::EPSILON);
    }

    #[test]
    fn angle_tolerance_from_arc_length_scales_by_radius() {
        let linear = Tolerance::new(1e-3);
        let r = Length::new::<millimeter>(10.0);
        let a = AngleTolerance::from_arc_length(linear, r);
        assert!((a.radians() - 1e-4).abs() < 1e-15);
    }

    #[test]
    fn angle_tolerance_degenerate_radius_falls_back_to_linear() {
        let linear = Tolerance::new(1e-3);
        let r = Length::new::<millimeter>(0.0);
        let a = AngleTolerance::from_arc_length(linear, r);
        assert!((a.radians() - 1e-3).abs() < 1e-15);
    }

    #[test]
    fn angle_tolerance_zero_constant() {
        assert!(AngleTolerance::ZERO.radians().abs() < f64::EPSILON);
    }

    #[test]
    fn point2_minus_point2_is_vec2() {
        let a = Point2::from_mm(10.0, 5.0);
        let b = Point2::from_mm(4.0, 2.0);
        let d: Vec2 = a - b;
        assert!((d.x().get::<millimeter>() - 6.0).abs() < 1e-12);
        assert!((d.y().get::<millimeter>() - 3.0).abs() < 1e-12);
    }

    #[test]
    fn point2_plus_vec2_is_point2() {
        let p = Point2::from_mm(1.0, 2.0);
        let v = Vec2::from_mm(10.0, 20.0);
        let q: Point2 = p + v;
        assert!((q.x().get::<millimeter>() - 11.0).abs() < 1e-12);
        assert!((q.y().get::<millimeter>() - 22.0).abs() < 1e-12);
    }

    #[test]
    fn vec2_perp_and_dot() {
        let v = Vec2::from_mm(3.0, 4.0);
        assert!((v.norm_mm() - 5.0).abs() < 1e-12);
        let p = v.perp_ccw();
        assert!((p.x().get::<millimeter>() + 4.0).abs() < 1e-12);
        assert!((p.y().get::<millimeter>() - 3.0).abs() < 1e-12);
        assert!(v.dot_mm2(p).abs() < 1e-12);
        assert!((v.cross_z_mm2(Vec2::from_mm(1.0, 0.0)) + 4.0).abs() < 1e-12);
    }

    #[test]
    fn vec2_try_normalize_rejects_zero() {
        let v = Vec2::zero();
        let r = v.try_normalize(Tolerance::new(1e-9));
        assert!(r.is_err());
    }

    #[test]
    fn vec2_try_normalize_unit() {
        let v = Vec2::from_mm(0.0, 7.0);
        let Ok(u) = v.try_normalize(Tolerance::new(1e-9)) else {
            panic!("nonzero");
        };
        let (ux, uy) = u.components();
        assert!(ux.abs() < 1e-12);
        assert!((uy - 1.0).abs() < 1e-12);
    }

    #[test]
    fn unit_vec2_axes_are_orthogonal() {
        let x = UnitVec2::x_axis();
        let y = UnitVec2::y_axis();
        assert!(x.dot(y).abs() < 1e-15);
        assert_eq!(x.perp_ccw().components(), y.components());
    }

    #[test]
    fn unit_vec2_rejects_zero_axis() {
        let r = UnitVec2::try_from_components(0.0, 0.0, Tolerance::new(1e-9));
        assert!(r.is_err());
    }

    #[test]
    fn unit_vec2_into_vec_scales_by_length() {
        let u = UnitVec2::x_axis();
        let v = u.into_vec(Length::new::<millimeter>(3.0));
        assert!((v.x().get::<millimeter>() - 3.0).abs() < 1e-12);
        assert!(v.y().get::<millimeter>().abs() < 1e-12);
    }

    #[test]
    fn vec2_scalar_and_negation() {
        let v = Vec2::from_mm(3.0, -4.0);
        let w = 2.0 * (-v);
        assert!((w.x().get::<millimeter>() + 6.0).abs() < 1e-12);
        assert!((w.y().get::<millimeter>() - 8.0).abs() < 1e-12);
    }

    #[test]
    fn point3_roundtrip() {
        let p = Point3::from_mm(1.5, 2.5, 3.5);
        assert!((p.x().get::<millimeter>() - 1.5).abs() < 1e-12);
        assert!((p.y().get::<millimeter>() - 2.5).abs() < 1e-12);
        assert!((p.z().get::<millimeter>() - 3.5).abs() < 1e-12);
    }

    #[test]
    fn sketch_plane_basis_accepts_orthogonal_axes() {
        let Ok(basis) = SketchPlaneBasis::new(
            Point3::origin(),
            UnitVec3::x_axis(),
            UnitVec3::y_axis(),
            Tolerance::new(1e-9),
        ) else {
            panic!("x and y unit axes are orthogonal");
        };
        let (nx, ny, nz) = basis.normal().components();
        assert!((nx).abs() < 1e-12);
        assert!((ny).abs() < 1e-12);
        assert!((nz - 1.0).abs() < 1e-12);
    }

    #[test]
    fn sketch_plane_basis_reorthonormalizes_within_tolerance() {
        let Ok(nearly) = UnitVec3::try_from_components(1e-10, 1.0, 0.0, Tolerance::new(1e-9))
        else {
            panic!("input vector is nonzero");
        };
        let Ok(basis) = SketchPlaneBasis::new(
            Point3::origin(),
            UnitVec3::x_axis(),
            nearly,
            Tolerance::new(1e-9),
        ) else {
            panic!("within tolerance");
        };
        let dot_after = basis.x_axis().dot(basis.y_axis()).abs();
        assert!(
            dot_after < 1e-15,
            "re-orthonormalized y should be orthogonal, got {dot_after}"
        );
    }

    #[test]
    fn sketch_plane_basis_rejects_non_orthogonal_axes() {
        let Ok(skew) = UnitVec3::try_from_components(1.0, 1.0, 0.0, Tolerance::new(1e-9)) else {
            panic!("input vector is nonzero");
        };
        let result = SketchPlaneBasis::new(
            Point3::origin(),
            UnitVec3::x_axis(),
            skew,
            Tolerance::new(1e-9),
        );
        assert!(result.is_err());
    }

    #[test]
    fn unit_vec3_rejects_zero_and_near_zero_within_tolerance() {
        let tol = Tolerance::new(1e-9);
        assert!(UnitVec3::try_from_components(0.0, 0.0, 0.0, tol).is_err());
        assert!(UnitVec3::try_from_components(1e-13, 0.0, 0.0, tol).is_err());
        assert!(UnitVec3::try_from_components(1.0, 0.0, 0.0, tol).is_ok());
    }

    #[test]
    fn parameter_display_and_value() {
        let p = Parameter::new(0.25);
        assert_eq!(format!("{p}"), "param=0.25");
        assert!((p.value() - 0.25).abs() < f64::EPSILON);
    }

    #[test]
    fn degrees_of_freedom_display_and_value() {
        let d = DegreesOfFreedom::new(7);
        assert_eq!(format!("{d}"), "dof=7");
        assert_eq!(d.value(), 7);
    }

    #[test]
    fn solver_residual_display_and_value() {
        let r = SolverResidual::new(1.5e-6);
        assert_eq!(format!("{r}"), "res=0.0000015");
        assert!((r.value() - 1.5e-6).abs() < f64::EPSILON);
    }

    #[test]
    fn chord_height_tolerance_display_and_value() {
        let c = ChordHeightTolerance::from_mm(0.01);
        assert!((c.millimeters() - 0.01).abs() < 1e-15);
        assert_eq!(format!("{c}"), "chord_tol=0.01 mm");
        let c2 = ChordHeightTolerance::new(Length::new::<millimeter>(0.05));
        assert!((c2.length().get::<millimeter>() - 0.05).abs() < 1e-15);
    }

    #[test]
    fn vec3_ops_and_normalize() {
        let vec = Vec3::from_mm(1.0, 2.0, 2.0);
        assert!((vec.norm_mm() - 3.0).abs() < 1e-12);
        let unit_x = Vec3::from_mm(1.0, 0.0, 0.0);
        let unit_y = Vec3::from_mm(0.0, 1.0, 0.0);
        assert!((unit_x.cross(unit_y).z().get::<millimeter>() - 1.0).abs() < 1e-12);
        assert!(unit_x.dot_mm2(unit_y).abs() < 1e-12);
        let Ok(normalized) = vec.try_normalize(Tolerance::new(1e-9)) else {
            panic!("nonzero");
        };
        let (nx, ny, nz) = normalized.components();
        assert!((nx * nx + ny * ny + nz * nz - 1.0).abs() < 1e-12);
        assert!(Vec3::zero().try_normalize(Tolerance::new(1e-9)).is_err());
        let far = Point3::from_mm(5.0, 5.0, 5.0);
        let near = Point3::from_mm(1.0, 2.0, 3.0);
        let delta: Vec3 = far - near;
        assert!((delta.x().get::<millimeter>() - 4.0).abs() < 1e-12);
        assert!(((near + delta).x().get::<millimeter>() - 5.0).abs() < 1e-12);
    }

    #[test]
    fn unit_vec3_reverse_and_into_vec() {
        let zaxis = UnitVec3::z_axis();
        assert!((zaxis.reversed().components().2 + 1.0).abs() < 1e-12);
        let scaled = zaxis.into_vec(Length::new::<millimeter>(4.0));
        assert!(scaled.x().get::<millimeter>().abs() < 1e-12);
        assert!((scaled.z().get::<millimeter>() - 4.0).abs() < 1e-12);
    }

    #[test]
    fn plane3_orthonormalizes_and_rejects_skew() {
        let origin = Point3::from_mm(1.0, 2.0, 3.0);
        let Ok(nearly) = UnitVec3::try_from_components(1e-10, 1.0, 0.0, Tolerance::new(1e-9))
        else {
            panic!("nonzero");
        };
        let Ok(plane) = Plane3::new(origin, UnitVec3::x_axis(), nearly, Tolerance::new(1e-9))
        else {
            panic!("within tolerance");
        };
        assert!(plane.x_axis().dot(plane.y_axis()).abs() < 1e-15);
        let (nx, ny, nz) = plane.normal().components();
        assert!(nx.abs() < 1e-12 && ny.abs() < 1e-12 && (nz - 1.0).abs() < 1e-12);
        let Ok(skew) = UnitVec3::try_from_components(1.0, 1.0, 0.0, Tolerance::new(1e-9)) else {
            panic!("nonzero");
        };
        assert!(Plane3::new(origin, UnitVec3::x_axis(), skew, Tolerance::new(1e-9)).is_err());
    }

    #[test]
    fn axis_angle_accessors() {
        let aa = AxisAngle::new(UnitVec3::z_axis(), Angle::new::<radian>(0.5));
        assert!((aa.angle().get::<radian>() - 0.5).abs() < 1e-15);
        assert!((aa.axis().components().2 - 1.0).abs() < 1e-12);
    }

    #[test]
    fn oriented_box_from_aabb_matches_center_and_half() {
        let aabb = Aabb3::from_corners(
            Point3::from_mm(-2.0, -4.0, 0.0),
            Point3::from_mm(2.0, 4.0, 10.0),
        );
        let obox = OrientedBox3::from_aabb(aabb);
        let (cx, cy, cz) = obox.center().coords_mm();
        assert!(cx.abs() < 1e-12 && cy.abs() < 1e-12 && (cz - 5.0).abs() < 1e-12);
        let (hx, hy, hz) = obox.half_extents().coords_mm();
        assert!((hx - 2.0).abs() < 1e-12 && (hy - 4.0).abs() < 1e-12 && (hz - 5.0).abs() < 1e-12);
        assert!((obox.frame().normal().components().2 - 1.0).abs() < 1e-12);
    }

    #[test]
    fn aabb3_from_points_union_contains() {
        let pts = [
            Point3::from_mm(0.0, 0.0, 0.0),
            Point3::from_mm(2.0, -1.0, 5.0),
            Point3::from_mm(-3.0, 4.0, 1.0),
        ];
        let Some(bb) = Aabb3::from_points(pts) else {
            panic!("nonempty");
        };
        let (lx, ly, lz) = bb.min().coords_mm();
        let (hx, hy, hz) = bb.max().coords_mm();
        assert!((lx + 3.0).abs() < 1e-12 && (ly + 1.0).abs() < 1e-12 && lz.abs() < 1e-12);
        assert!((hx - 2.0).abs() < 1e-12 && (hy - 4.0).abs() < 1e-12 && (hz - 5.0).abs() < 1e-12);
        assert!(bb.contains(Point3::from_mm(0.0, 0.0, 2.0)));
        assert!(!bb.contains(Point3::from_mm(10.0, 0.0, 0.0)));
        let (cx, cy, cz) = bb.center().coords_mm();
        assert!((cx + 0.5).abs() < 1e-12 && (cy - 1.5).abs() < 1e-12 && (cz - 2.5).abs() < 1e-12);
        assert!((bb.extent().x().get::<millimeter>() - 5.0).abs() < 1e-12);
        assert!(Aabb3::from_points(core::iter::empty()).is_none());
    }

    #[test]
    fn face_role_ord_matches_canonical_order() {
        let from = SketchEntityId::default();
        let start = FaceRole::StartCap;
        let side0 = FaceRole::Side {
            loop_index: LoopIndex::OUTER,
            from,
        };
        let side1 = FaceRole::Side {
            loop_index: LoopIndex::new(1),
            from,
        };
        let end = FaceRole::EndCap;
        let imported = FaceRole::Imported {
            ordinal: ImportOrdinal::new(0),
        };
        assert!(start < side0);
        assert!(side0 < side1);
        assert!(side1 < end);
        assert!(end < imported);
    }

    #[test]
    fn edge_role_ord_matches_canonical_order() {
        let from = SketchEntityId::default();
        let start = EdgeRole::StartCapEdge { from };
        let side = EdgeRole::SideEdge {
            from,
            side: SideKind::Corner,
        };
        let end = EdgeRole::EndCapEdge { from };
        let imported = EdgeRole::Imported {
            ordinal: ImportOrdinal::new(0),
        };
        assert!(start < side);
        assert!(side < end);
        assert!(end < imported);
    }

    #[test]
    fn vertex_role_ord_matches_canonical_order() {
        let from = SketchEntityId::default();
        let start = VertexRole::StartCapVertex {
            from,
            side: SideKind::Corner,
        };
        let end = VertexRole::EndCapVertex {
            from,
            side: SideKind::Corner,
        };
        let imported = VertexRole::Imported {
            ordinal: ImportOrdinal::new(0),
        };
        assert!(start < end);
        assert!(end < imported);
    }

    #[test]
    fn oriented_box_rejects_invalid_half_extent() {
        let frame = Plane3::new_unchecked(Point3::origin(), UnitVec3::x_axis(), UnitVec3::y_axis());
        assert!(OrientedBox3::new(frame, Vec3::from_mm(1.0, 2.0, 3.0)).is_ok());
        assert!(OrientedBox3::new(frame, Vec3::from_mm(1.0, -2.0, 3.0)).is_err());
        assert!(OrientedBox3::new(frame, Vec3::from_mm(1.0, f64::NAN, 3.0)).is_err());
        assert!(OrientedBox3::new(frame, Vec3::from_mm(f64::INFINITY, 2.0, 3.0)).is_err());
    }

    #[test]
    fn oriented_box_deserialize_rejects_invalid_half_extent() {
        let frame = Plane3::new_unchecked(Point3::origin(), UnitVec3::x_axis(), UnitVec3::y_axis());
        let Ok(obox) = OrientedBox3::new(frame, Vec3::from_mm(5.0, 5.0, 5.0)) else {
            panic!("non-negative half-extents");
        };
        let Ok(good) = ron::to_string(&obox) else {
            panic!("serialize oriented box");
        };
        assert!(ron::from_str::<OrientedBox3>(&good).is_ok());
        let bad = good.replace('5', "-5");
        assert!(ron::from_str::<OrientedBox3>(&bad).is_err());
    }

    #[test]
    fn labels_ron_roundtrip() {
        let feature = FeatureId::default();
        let from = SketchEntityId::default();
        let face = FaceLabel {
            feature,
            role: FaceRole::Side {
                loop_index: LoopIndex::OUTER,
                from,
            },
        };
        let Ok(text) = ron::to_string(&face) else {
            panic!("serialize face label");
        };
        let Ok(back) = ron::from_str::<FaceLabel>(&text) else {
            panic!("deserialize face label");
        };
        assert_eq!(face, back);

        let edge = EdgeLabel {
            feature,
            role: EdgeRole::SideEdge {
                from,
                side: SideKind::Seam,
            },
        };
        let Ok(text) = ron::to_string(&edge) else {
            panic!("serialize edge label");
        };
        let Ok(back) = ron::from_str::<EdgeLabel>(&text) else {
            panic!("deserialize edge label");
        };
        assert_eq!(edge, back);

        let vertex = VertexLabel {
            feature,
            role: VertexRole::EndCapVertex {
                from,
                side: SideKind::Corner,
            },
        };
        let Ok(text) = ron::to_string(&vertex) else {
            panic!("serialize vertex label");
        };
        let Ok(back) = ron::from_str::<VertexLabel>(&text) else {
            panic!("deserialize vertex label");
        };
        assert_eq!(vertex, back);
    }

    #[test]
    fn role_rejects_unknown_field() {
        let label = FaceLabel {
            feature: FeatureId::default(),
            role: FaceRole::Side {
                loop_index: LoopIndex::OUTER,
                from: SketchEntityId::default(),
            },
        };
        let Ok(text) = ron::to_string(&label) else {
            panic!("serialize face label");
        };
        let Some(idx) = text.find("Side(") else {
            panic!("expected Side variant in {text}");
        };
        let mut munged = text.clone();
        munged.insert_str(idx + "Side(".len(), "bogus:42,");
        assert!(ron::from_str::<FaceLabel>(&munged).is_err());
    }

    #[test]
    fn camera_new_rejects_coincident_eye_and_target() {
        let Ok(projection) = Projection::orthographic(Length::new::<millimeter>(10.0)) else {
            panic!("10 mm half-height is positive");
        };
        let coincident = Camera3::new(
            Point3::from_mm(1.0, 2.0, 3.0),
            Point3::from_mm(1.0, 2.0, 3.0),
            UnitVec3::y_axis(),
            projection,
        );
        assert!(coincident.is_err());
    }

    #[test]
    fn camera_accessors_round_trip_fields() {
        let camera = ortho_camera();
        assert!((camera.eye().z().get::<millimeter>() - 10.0).abs() < 1e-12);
        assert!(camera.target().coords_mm().0.abs() < 1e-12);
        assert!((camera.up().components().1 - 1.0).abs() < 1e-12);
        let ProjectionKind::Orthographic { half_height } = camera.projection().kind() else {
            panic!("constructed an orthographic camera");
        };
        assert!((half_height.get::<millimeter>() - 25.0).abs() < 1e-12);
    }

    #[test]
    fn camera_ron_round_trip() {
        let camera = ortho_camera();
        let Ok(text) = ron::to_string(&camera) else {
            panic!("serialize camera");
        };
        let Ok(back) = ron::from_str::<Camera3>(&text) else {
            panic!("deserialize camera");
        };
        assert_eq!(camera, back);
    }

    #[test]
    fn camera_deserialize_rejects_degenerate() {
        let Ok(text) = ron::to_string(&ortho_camera()) else {
            panic!("serialize camera");
        };
        assert!(ron::from_str::<Camera3>(&text).is_ok());
        let collapsed = text.replace("z:10.0", "z:0.0");
        assert!(ron::from_str::<Camera3>(&collapsed).is_err());
    }

    #[test]
    fn orbit_state_identity_is_unrotated() {
        let orbit = OrbitState::identity();
        assert!((orbit.rotation().angle().get::<radian>()).abs() < 1e-12);
        assert!((orbit.zoom().value() - 1.0).abs() < 1e-12);
        assert!(orbit.pan_target().coords_mm().0.abs() < 1e-12);
    }

    #[test]
    fn orbit_state_rotated_accumulates_angle() {
        let quarter = AxisAngle::new(
            UnitVec3::z_axis(),
            Angle::new::<radian>(core::f64::consts::FRAC_PI_2),
        );
        let once = OrbitState::identity().rotated(quarter);
        let twice = once.rotated(quarter);
        assert!(
            (once.rotation().angle().get::<radian>() - core::f64::consts::FRAC_PI_2).abs() < 1e-12
        );
        assert!((twice.rotation().angle().get::<radian>() - core::f64::consts::PI).abs() < 1e-12);
        assert!((twice.rotation().axis().components().2 - 1.0).abs() < 1e-12);
    }

    #[test]
    fn orbit_state_ron_round_trip() {
        let Ok(zoom) = ZoomFactor::new(2.5) else {
            panic!("positive zoom");
        };
        let Ok(orbit) = OrbitState::new(
            AxisAngle::new(
                UnitVec3::z_axis(),
                Angle::new::<radian>(core::f64::consts::FRAC_PI_2),
            ),
            zoom,
            Point3::from_mm(1.0, 2.0, 3.0),
        ) else {
            panic!("finite pan target");
        };
        let Ok(text) = ron::to_string(&orbit) else {
            panic!("serialize orbit");
        };
        let Ok(back) = ron::from_str::<OrbitState>(&text) else {
            panic!("deserialize orbit");
        };
        assert_eq!(orbit, back);
    }

    #[test]
    fn standard_view_labels_are_distinct() {
        let views = [
            StandardView::Front,
            StandardView::Back,
            StandardView::Left,
            StandardView::Right,
            StandardView::Top,
            StandardView::Bottom,
            StandardView::Isometric,
            StandardView::NormalTo,
        ];
        let labels: std::collections::BTreeSet<&str> = views.iter().map(|v| v.label()).collect();
        assert_eq!(labels.len(), views.len());
    }

    #[test]
    fn step_entity_id_display() {
        let Ok(id) = StepEntityId::new(123) else {
            panic!("123 is a positive instance name");
        };
        assert_eq!(format!("{id}"), "#123");
        let Ok(seven) = StepEntityId::new(7) else {
            panic!("7 is a positive instance name");
        };
        assert_eq!(seven.value(), 7);
    }

    #[test]
    fn step_entity_id_rejects_zero() {
        assert!(StepEntityId::new(0).is_err());
        assert!(StepEntityId::new(1).is_ok());
        assert!(ron::from_str::<StepEntityId>("0").is_err());
        assert!(ron::from_str::<StepEntityId>("123").is_ok());
    }

    #[test]
    fn step_schema_labels() {
        assert_eq!(StepSchema::Ap214.label(), "AP214");
        assert_eq!(StepSchema::Ap242E2.label(), "AP242E2");
    }

    #[test]
    fn step_file_header_ron_round_trip() {
        let header = StepFileHeader {
            schema: StepSchema::Ap214,
            originating_system: StepOriginatingSystem::new("bone 0.0.0"),
            organization: StepOrganization::new("witchcraft.systems"),
            file_name: StepFileName::new("limpet.step"),
        };
        let Ok(text) = ron::to_string(&header) else {
            panic!("serialize step header");
        };
        let Ok(back) = ron::from_str::<StepFileHeader>(&text) else {
            panic!("deserialize step header");
        };
        assert_eq!(header, back);
        assert_eq!(header.file_name.as_str(), "limpet.step");
    }

    #[test]
    fn display_mode_default_and_labels() {
        assert_eq!(DisplayMode::DEFAULT, DisplayMode::ShadedWithEdges);
        assert_eq!(DisplayMode::HiddenLineGray.label(), "hidden_line_gray");
        assert_eq!(ShadingModel::Gouraud.label(), "gouraud");
    }

    #[test]
    fn camera_rejects_non_finite_eye() {
        let Ok(projection) = Projection::orthographic(Length::new::<millimeter>(10.0)) else {
            panic!("10 mm half-height is positive");
        };
        let nan = Camera3::new(
            Point3::from_mm(f64::NAN, 0.0, 0.0),
            Point3::origin(),
            UnitVec3::y_axis(),
            projection,
        );
        assert!(nan.is_err());
        let infinite = Camera3::new(
            Point3::from_mm(f64::INFINITY, 0.0, 0.0),
            Point3::origin(),
            UnitVec3::y_axis(),
            projection,
        );
        assert!(infinite.is_err());
    }

    #[test]
    fn camera_rejects_up_parallel_to_view() {
        let Ok(projection) = Projection::orthographic(Length::new::<millimeter>(10.0)) else {
            panic!("10 mm half-height is positive");
        };
        let parallel = Camera3::new(
            Point3::from_mm(0.0, 0.0, 10.0),
            Point3::origin(),
            UnitVec3::z_axis(),
            projection,
        );
        assert!(parallel.is_err());
    }

    #[test]
    fn projection_constructors_validate() {
        let mm = |v| Length::new::<millimeter>(v);
        assert!(Projection::orthographic(mm(25.0)).is_ok());
        assert!(Projection::orthographic(mm(0.0)).is_err());
        assert!(Projection::orthographic(mm(-1.0)).is_err());
        assert!(Projection::orthographic(Length::new::<millimeter>(f64::NAN)).is_err());
        assert!(Projection::perspective(Angle::new::<degree>(60.0), mm(1.0), mm(1000.0)).is_ok());
        assert!(Projection::perspective(Angle::new::<degree>(400.0), mm(1.0), mm(1000.0)).is_err());
        assert!(Projection::perspective(Angle::new::<degree>(60.0), mm(10.0), mm(1.0)).is_err());
        assert!(Projection::perspective(Angle::new::<degree>(60.0), mm(0.0), mm(1000.0)).is_err());
    }

    #[test]
    fn projection_deserialize_rejects_degenerate() {
        let Ok(valid) = Projection::orthographic(Length::new::<millimeter>(25.0)) else {
            panic!("25 mm half-height is positive");
        };
        let Ok(text) = ron::to_string(&valid) else {
            panic!("serialize projection");
        };
        assert!(ron::from_str::<Projection>(&text).is_ok());
        let degenerate = text.replace("0.025", "-0.025");
        assert!(ron::from_str::<Projection>(&degenerate).is_err());
    }

    #[test]
    fn zoom_factor_rejects_non_positive() {
        assert!(ZoomFactor::new(2.5).is_ok());
        assert!(ZoomFactor::new(0.0).is_err());
        assert!(ZoomFactor::new(-1.0).is_err());
        assert!(ZoomFactor::new(f64::NAN).is_err());
        assert!(ZoomFactor::new(f64::INFINITY).is_err());
    }

    #[test]
    fn zoom_factor_deserialize_rejects_non_positive() {
        assert!(ron::from_str::<ZoomFactor>("2.5").is_ok());
        assert!(ron::from_str::<ZoomFactor>("0.0").is_err());
        assert!(ron::from_str::<ZoomFactor>("-1.0").is_err());
    }

    #[test]
    fn orbit_state_deserialize_normalizes_non_unit_quaternion() {
        let nonunit = "(qw:2.0,qx:0.0,qy:0.0,qz:0.0,zoom:1.0,pan_target:(x:0.0,y:0.0,z:0.0))";
        let Ok(orbit) = ron::from_str::<OrbitState>(nonunit) else {
            panic!("non-unit quaternion should normalize, not fail");
        };
        let Ok(text) = ron::to_string(&orbit) else {
            panic!("serialize orbit");
        };
        assert!(
            text.contains("qw:1"),
            "quaternion should re-normalize to unit, got {text}"
        );
    }

    #[test]
    fn orbit_state_deserialize_rejects_zero_quaternion() {
        let zero = "(qw:0.0,qx:0.0,qy:0.0,qz:0.0,zoom:1.0,pan_target:(x:0.0,y:0.0,z:0.0))";
        assert!(ron::from_str::<OrbitState>(zero).is_err());
    }

    #[test]
    fn orbit_state_rejects_non_finite_pan_target() {
        let Ok(zoom) = ZoomFactor::new(1.0) else {
            panic!("positive zoom");
        };
        let rotation = AxisAngle::new(UnitVec3::z_axis(), Angle::new::<radian>(0.0));
        assert!(OrbitState::new(rotation, zoom, Point3::from_mm(f64::NAN, 0.0, 0.0)).is_err());
        assert!(OrbitState::new(rotation, zoom, Point3::from_mm(f64::INFINITY, 0.0, 0.0)).is_err());
        assert!(OrbitState::new(rotation, zoom, Point3::origin()).is_ok());
    }

    #[test]
    fn orbit_state_deserialize_rejects_non_finite_pan_target() {
        let 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:0.0))";
        assert!(ron::from_str::<OrbitState>(finite).is_ok());
        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))";
        assert!(ron::from_str::<OrbitState>(non_finite).is_err());
    }

    #[test]
    fn camera_perspective_ron_round_trip() {
        let Ok(persp) = Projection::perspective(
            Angle::new::<degree>(60.0),
            Length::new::<millimeter>(1.0),
            Length::new::<millimeter>(1000.0),
        ) else {
            panic!("valid perspective");
        };
        let Ok(camera) = Camera3::new(
            Point3::from_mm(0.0, 0.0, 10.0),
            Point3::origin(),
            UnitVec3::y_axis(),
            persp,
        ) else {
            panic!("eye and target are 10 mm apart");
        };
        let Ok(text) = ron::to_string(&camera) else {
            panic!("serialize camera");
        };
        let Ok(back) = ron::from_str::<Camera3>(&text) else {
            panic!("deserialize camera");
        };
        assert_eq!(camera, back);
    }

    fn assert_ron_round_trip<T>(value: &T)
    where
        T: serde::Serialize + serde::de::DeserializeOwned + PartialEq + core::fmt::Debug,
    {
        let Ok(text) = ron::to_string(value) else {
            panic!("serialize {value:?}");
        };
        let Ok(back) = ron::from_str::<T>(&text) else {
            panic!("deserialize {text}");
        };
        assert_eq!(value, &back);
    }

    #[test]
    fn discrete_types_ron_round_trip() {
        [
            DisplayMode::Wireframe,
            DisplayMode::HiddenLineRemoved,
            DisplayMode::HiddenLineGray,
            DisplayMode::ShadedWithEdges,
            DisplayMode::ShadedNoEdges,
        ]
        .iter()
        .for_each(assert_ron_round_trip);
        [
            ShadingModel::Flat,
            ShadingModel::Gouraud,
            ShadingModel::Phong,
        ]
        .iter()
        .for_each(assert_ron_round_trip);
        [
            StandardView::Front,
            StandardView::Back,
            StandardView::Left,
            StandardView::Right,
            StandardView::Top,
            StandardView::Bottom,
            StandardView::Isometric,
            StandardView::NormalTo,
        ]
        .iter()
        .for_each(assert_ron_round_trip);
        [StepSchema::Ap214, StepSchema::Ap242E2]
            .iter()
            .for_each(assert_ron_round_trip);
        let Ok(entity) = StepEntityId::new(123) else {
            panic!("123 is a positive instance name");
        };
        assert_ron_round_trip(&entity);
    }
}
