Skip to main content

affn/cartesian/
position.rs

1//! # Cartesian Position (Affine Points)
2//!
3//! This module defines [`Position<C, F, U>`], an **affine point** in 3D space.
4//!
5//! ## Mathematical Model
6//!
7//! A position is a point in affine space, not a vector. It represents a location
8//! relative to an origin (the reference center). Key properties:
9//!
10//! - **Center-dependent**: Position is measured from a specific origin `C`
11//! - **Frame-dependent**: The orientation is relative to a reference frame `F`
12//! - **Dimensioned**: Has a length unit `U`
13//!
14//! ## Affine Space Operations
15//!
16//! Positions do **not** form a vector space. The only valid operations are:
17//!
18//! | Operation | Result | Meaning |
19//! |-----------|--------|---------|
20//! | `Position - Position` | `Displacement` | Displacement between points |
21//! | `Position + Displacement` | `Position` | Translate point by displacement |
22//! | `Position - Displacement` | `Position` | Translate point backwards |
23//!
24//! ## Forbidden Operations
25//!
26//! The following operations are **mathematically invalid** and do not compile:
27//!
28//! - `Position + Position` — Adding points has no geometric meaning
29//! - `Position * scalar` — Scaling a point makes no sense without an origin
30//!
31//! ## Example
32//!
33//! ```rust
34//! use affn::cartesian::{Position, Displacement};
35//! use affn::centers::ReferenceCenter;
36//! use affn::frames::ReferenceFrame;
37//! use qtty::units::Meter;
38//!
39//! // Define custom center and frame (astronomy types are in downstream crates)
40//! #[derive(Debug, Copy, Clone)]
41//! struct MyCenter;
42//! impl ReferenceCenter for MyCenter {
43//!     type Params = ();
44//!     fn center_name() -> &'static str { "MyCenter" }
45//! }
46//!
47//! #[derive(Debug, Copy, Clone)]
48//! struct MyFrame;
49//! impl ReferenceFrame for MyFrame {
50//!     fn frame_name() -> &'static str { "MyFrame" }
51//! }
52//!
53//! // Two positions in the custom coordinate system
54//! let pos1 = Position::<MyCenter, MyFrame, Meter>::new(1.0, 0.0, 0.0);
55//! let pos2 = Position::<MyCenter, MyFrame, Meter>::new(1.5, 0.0, 0.0);
56//!
57//! // Displacement between positions
58//! let displacement: Displacement<MyFrame, Meter> = pos2 - pos1;
59//! assert!((displacement.x().value() - 0.5).abs() < 1e-12);
60//!
61//! // Translate pos1 by the displacement to get pos2
62//! let result = pos1 + displacement;
63//! assert!((result.x().value() - 1.5).abs() < 1e-12);
64//! ```
65
66use super::vector::Displacement;
67use super::xyz::XYZ;
68use crate::centers::ReferenceCenter;
69use crate::frames::ReferenceFrame;
70use qtty::length::LengthUnit;
71use qtty::Quantity;
72
73use std::marker::PhantomData;
74use std::ops::{Add, Sub};
75
76// Serde implementations in separate module
77#[cfg(feature = "serde")]
78#[path = "position_serde.rs"]
79mod position_serde;
80
81// =============================================================================
82// Error Types
83// =============================================================================
84
85/// Error returned when an operation requires matching center parameters
86/// but the two positions have different ones.
87///
88/// This occurs with **parameterized centers** (e.g., `Topocentric` with `ObserverSite`)
89/// when two positions reference different observer sites.
90///
91/// For centers with `Params = ()` (e.g., `Geocentric`, `Heliocentric`), this error
92/// can never occur because all instances share the same (empty) parameters.
93#[derive(Debug, Clone)]
94pub struct CenterParamsMismatchError {
95    /// The operation that detected the mismatch.
96    pub operation: &'static str,
97}
98
99impl std::fmt::Display for CenterParamsMismatchError {
100    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101        write!(
102            f,
103            "center parameter mismatch in `{}`: \
104             positions reference different parameterized centers \
105             (e.g., different observer sites)",
106            self.operation
107        )
108    }
109}
110
111impl std::error::Error for CenterParamsMismatchError {}
112
113/// An affine point in 3D Cartesian coordinates.
114///
115/// Positions represent locations in space relative to a reference center (origin).
116/// Unlike vectors, positions do not form a vector space.
117///
118/// # Type Parameters
119/// - `C`: The reference center (e.g., `Heliocentric`, `Geocentric`, `Topocentric`)
120/// - `F`: The reference frame (e.g., `ICRS`, `EclipticMeanJ2000`, `Equatorial`)
121/// - `U`: The length unit (e.g., `AstronomicalUnit`, `Kilometer`)
122///
123/// # Center Parameters
124///
125/// Some centers (like `Topocentric`) require runtime parameters stored in `C::Params`.
126/// For most centers, `Params = ()` (zero overhead).
127#[derive(Debug, Clone, Copy)]
128pub struct Position<C: ReferenceCenter, F: ReferenceFrame, U: LengthUnit> {
129    xyz: XYZ<Quantity<U>>,
130    center_params: C::Params,
131    _frame: PhantomData<F>,
132}
133
134// =============================================================================
135// Constructors with explicit center parameters
136// =============================================================================
137
138impl<C: ReferenceCenter, F: ReferenceFrame, U: LengthUnit> Position<C, F, U> {
139    /// Creates a new position with explicit center parameters.
140    ///
141    /// # Arguments
142    /// - `center_params`: Runtime parameters for the center (e.g., `ObserverSite` for Topocentric)
143    /// - `x`, `y`, `z`: Component values (converted to `Quantity<U>`)
144    #[inline]
145    pub fn new_with_params<T: Into<Quantity<U>>>(
146        center_params: C::Params,
147        x: T,
148        y: T,
149        z: T,
150    ) -> Self {
151        Self {
152            xyz: XYZ::new(x.into(), y.into(), z.into()),
153            center_params,
154            _frame: PhantomData,
155        }
156    }
157
158    /// Creates a position from internal storage with explicit center parameters.
159    #[inline]
160    pub(crate) fn from_xyz_with_params(center_params: C::Params, xyz: XYZ<Quantity<U>>) -> Self {
161        Self {
162            xyz,
163            center_params,
164            _frame: PhantomData,
165        }
166    }
167
168    /// Creates a position from a `[Quantity<U>; 3]` array with explicit center parameters.
169    #[inline]
170    pub fn from_array(center_params: C::Params, arr: [Quantity<U>; 3]) -> Self {
171        Self {
172            xyz: XYZ::from_array(arr),
173            center_params,
174            _frame: PhantomData,
175        }
176    }
177
178    /// Const constructor for use in const contexts.
179    #[inline]
180    pub const fn new_const(
181        center_params: C::Params,
182        x: Quantity<U>,
183        y: Quantity<U>,
184        z: Quantity<U>,
185    ) -> Self {
186        Self {
187            xyz: XYZ::new(x, y, z),
188            center_params,
189            _frame: PhantomData,
190        }
191    }
192
193    /// Returns a reference to the center parameters.
194    #[inline]
195    pub fn center_params(&self) -> &C::Params {
196        &self.center_params
197    }
198}
199
200// =============================================================================
201// Convenience constructors for centers with Params = ()
202// =============================================================================
203
204impl<C, F, U> Position<C, F, U>
205where
206    C: ReferenceCenter<Params = ()>,
207    F: ReferenceFrame,
208    U: LengthUnit,
209{
210    /// Creates a new position for centers with `Params = ()`.
211    ///
212    /// This is a convenience constructor that doesn't require passing `()` explicitly.
213    ///
214    /// # Example
215    /// ```rust
216    /// use affn::cartesian::Position;
217    /// use affn::frames::ReferenceFrame;
218    /// use affn::centers::ReferenceCenter;
219    /// use qtty::units::Meter;
220    ///
221    /// #[derive(Debug, Copy, Clone)]
222    /// struct WorldFrame;
223    /// impl ReferenceFrame for WorldFrame {
224    ///     fn frame_name() -> &'static str { "WorldFrame" }
225    /// }
226    ///
227    /// #[derive(Debug, Copy, Clone)]
228    /// struct WorldOrigin;
229    /// impl ReferenceCenter for WorldOrigin {
230    ///     type Params = ();
231    ///     fn center_name() -> &'static str { "WorldOrigin" }
232    /// }
233    ///
234    /// let pos = Position::<WorldOrigin, WorldFrame, Meter>::new(1.0, 0.0, 0.0);
235    /// ```
236    #[inline]
237    pub fn new<T: Into<Quantity<U>>>(x: T, y: T, z: T) -> Self {
238        Self::new_with_params((), x, y, z)
239    }
240
241    /// Creates a position from a `[Quantity<U>; 3]` array for centers with `Params = ()`.
242    #[inline]
243    pub fn from_array_origin(arr: [Quantity<U>; 3]) -> Self {
244        Self::from_array((), arr)
245    }
246
247    /// The origin of this coordinate system (all coordinates zero).
248    pub const CENTER: Self = Self::new_const(
249        (),
250        Quantity::<U>::new(0.0),
251        Quantity::<U>::new(0.0),
252        Quantity::<U>::new(0.0),
253    );
254}
255
256// =============================================================================
257// Component Access
258// =============================================================================
259
260impl<C: ReferenceCenter, F: ReferenceFrame, U: LengthUnit> Position<C, F, U> {
261    /// Returns the x-component.
262    #[inline]
263    pub fn x(&self) -> Quantity<U> {
264        self.xyz.x()
265    }
266
267    /// Returns the y-component.
268    #[inline]
269    pub fn y(&self) -> Quantity<U> {
270        self.xyz.y()
271    }
272
273    /// Returns the z-component.
274    #[inline]
275    pub fn z(&self) -> Quantity<U> {
276        self.xyz.z()
277    }
278
279    /// Returns a reference to the underlying `[Quantity<U>; 3]` array.
280    #[inline]
281    pub fn as_array(&self) -> &[Quantity<U>; 3] {
282        self.xyz.as_array()
283    }
284
285    /// Converts this position to another length unit.
286    ///
287    /// The center and frame are preserved while each Cartesian component is
288    /// converted independently via `qtty::Quantity::to`.
289    #[inline]
290    pub fn to_unit<U2: LengthUnit>(&self) -> Position<C, F, U2>
291    where
292        C::Params: Clone,
293    {
294        Position::<C, F, U2>::new_with_params(
295            self.center_params.clone(),
296            self.x().to::<U2>(),
297            self.y().to::<U2>(),
298            self.z().to::<U2>(),
299        )
300    }
301
302    /// Reinterprets this position as belonging to a different reference frame.
303    ///
304    /// This is a **zero-cost** operation: the Cartesian components and center
305    /// are preserved unchanged; only the compile-time frame tag `F` is replaced
306    /// by `F2`.
307    ///
308    /// # When to use
309    ///
310    /// After applying a mathematical rotation (`Rotation3 * position`) whose
311    /// result carries the *original* frame tag, call this method to assign the
312    /// correct *target* frame tag.
313    ///
314    /// ```text
315    /// // Rotate from EclipticMeanJ2000 into ICRS coordinates:
316    /// let rotated = rot * pos_ecl;           // still tagged EclipticMeanJ2000
317    /// let pos_icrs = rotated.reinterpret_frame::<ICRS>(); // now tagged ICRS
318    /// ```
319    #[inline]
320    pub fn reinterpret_frame<F2: ReferenceFrame>(self) -> Position<C, F2, U>
321    where
322        C::Params: Clone,
323    {
324        Position::new_with_params(self.center_params.clone(), self.x(), self.y(), self.z())
325    }
326}
327
328// =============================================================================
329// Geometric Operations
330// =============================================================================
331
332impl<C: ReferenceCenter, F: ReferenceFrame, U: LengthUnit> Position<C, F, U> {
333    /// Computes the distance from the reference center.
334    #[inline]
335    pub fn distance(&self) -> Quantity<U> {
336        self.xyz.magnitude()
337    }
338
339    /// Computes the distance to another position in the same center and frame.
340    ///
341    /// # Panics
342    ///
343    /// Panics if the two positions carry different `center_params` values.
344    /// The panic message includes the center type name and a debug print of
345    /// both `center_params` to aid diagnosis. For a non-panicking alternative,
346    /// use [`try_distance_to`](Self::try_distance_to), which returns a
347    /// [`CenterParamsMismatchError`] instead.
348    ///
349    /// # Note on Parameterized Centers
350    ///
351    /// This method is only available when the center has no runtime parameters
352    /// (`C::Params = ()`). For parameterized centers use the fallible
353    /// [`try_distance_to`](Self::try_distance_to) instead.
354    #[inline]
355    pub fn distance_to(&self, other: &Self) -> Quantity<U>
356    where
357        C: ReferenceCenter<Params = ()>,
358    {
359        (self.xyz - other.xyz).magnitude()
360    }
361
362    /// Checked version of [`distance_to`](Self::distance_to) that returns `Err`
363    /// instead of panicking when center parameters don't match.
364    ///
365    /// For centers with `Params = ()`, this always succeeds.
366    #[inline]
367    pub fn try_distance_to(&self, other: &Self) -> Result<Quantity<U>, CenterParamsMismatchError>
368    where
369        C::Params: PartialEq,
370    {
371        if self.center_params != other.center_params {
372            return Err(CenterParamsMismatchError {
373                operation: "distance_to",
374            });
375        }
376        Ok((self.xyz - other.xyz).magnitude())
377    }
378
379    /// Returns the direction (unit vector) from the center to this position.
380    ///
381    /// Note: Directions are frame-only types (no center). This extracts the
382    /// normalized direction of the position vector.
383    ///
384    /// Returns `None` if the position is at the origin.
385    #[inline]
386    pub fn direction(&self) -> Option<super::Direction<F>> {
387        self.xyz
388            .to_raw()
389            .try_normalize()
390            .map(super::Direction::from_xyz_unchecked)
391    }
392
393    /// Returns the direction, assuming non-zero distance from origin.
394    ///
395    /// # Panics
396    /// May produce NaN if the position is at the origin.
397    #[inline]
398    pub fn direction_unchecked(&self) -> super::Direction<F> {
399        super::Direction::from_xyz_unchecked(self.xyz.to_raw().normalize_unchecked())
400    }
401
402    /// Converts this Cartesian position to spherical coordinates.
403    ///
404    /// Returns a spherical position with the same center and frame,
405    /// with (polar, azimuth, distance) computed from (x, y, z).
406    #[must_use]
407    #[inline]
408    pub fn to_spherical(&self) -> crate::spherical::Position<C, F, U> {
409        crate::spherical::Position::from_cartesian(self)
410    }
411
412    /// Constructs a Cartesian position from spherical coordinates.
413    ///
414    /// This is equivalent to `spherical_pos.to_cartesian()`.
415    #[must_use]
416    #[inline]
417    pub fn from_spherical(sph: &crate::spherical::Position<C, F, U>) -> Self {
418        sph.to_cartesian()
419    }
420}
421
422// =============================================================================
423// Affine Space Operations: Position - Position -> Vector
424// =============================================================================
425
426impl<C, F, U> Sub for Position<C, F, U>
427where
428    C: ReferenceCenter<Params = ()>,
429    F: ReferenceFrame,
430    U: LengthUnit,
431{
432    type Output = Displacement<F, U>;
433
434    /// Computes the displacement vector from `other` to `self`.
435    ///
436    /// This operator is only available when the center has no runtime
437    /// parameters (`C::Params = ()`), which is the case for the standard
438    /// centers `Geocentric`, `Heliocentric`, `Barycentric`, etc.
439    /// For parameterized centers (e.g., `Topocentric`) use the fallible
440    /// [`Position::checked_sub`] instead.
441    #[inline]
442    fn sub(self, other: Self) -> Self::Output {
443        Displacement::from_xyz(self.xyz - other.xyz)
444    }
445}
446
447impl<C, F, U> Sub<&Position<C, F, U>> for &Position<C, F, U>
448where
449    C: ReferenceCenter<Params = ()>,
450    F: ReferenceFrame,
451    U: LengthUnit,
452{
453    type Output = Displacement<F, U>;
454
455    /// Computes the displacement vector from `other` to `self`.
456    ///
457    /// Only available when `C::Params = ()`. Use [`Position::checked_sub`]
458    /// for parameterized centers.
459    #[inline]
460    fn sub(self, other: &Position<C, F, U>) -> Self::Output {
461        Displacement::from_xyz(self.xyz - other.xyz)
462    }
463}
464
465// `&Position - &Position` is hand-written above (it does not require
466// `C::Params: Copy`); the macro only supplies the remaining two reference
467// variants, which forward to the by-value impl and therefore inherit its
468// `Copy` requirement on `C::Params`.
469forward_ref_binop_lhs! {
470    impl[C, F, U] Sub, sub for Position<C, F, U>, Position<C, F, U>
471    where (
472        C: ReferenceCenter<Params = ()>,
473        F: ReferenceFrame,
474        U: LengthUnit,
475    )
476}
477forward_ref_binop_rhs! {
478    impl[C, F, U] Sub, sub for Position<C, F, U>, Position<C, F, U>
479    where (
480        C: ReferenceCenter<Params = ()>,
481        F: ReferenceFrame,
482        U: LengthUnit,
483    )
484}
485
486impl<C: ReferenceCenter, F: ReferenceFrame, U: LengthUnit> Position<C, F, U> {
487    /// Checked subtraction that returns `Err` instead of panicking when
488    /// center parameters don't match.
489    ///
490    /// This is the safe alternative to the `Sub` operator (`pos_a - pos_b`).
491    /// For centers with `Params = ()`, this always succeeds.
492    #[inline]
493    pub fn checked_sub(&self, other: &Self) -> Result<Displacement<F, U>, CenterParamsMismatchError>
494    where
495        C::Params: PartialEq,
496    {
497        if self.center_params != other.center_params {
498            return Err(CenterParamsMismatchError {
499                operation: "sub (Position - Position)",
500            });
501        }
502        Ok(Displacement::from_xyz(self.xyz - other.xyz))
503    }
504}
505
506// =============================================================================
507// Affine Space Operations: Position + Vector -> Position
508// =============================================================================
509
510impl<C, F, U> Add<Displacement<F, U>> for Position<C, F, U>
511where
512    C: ReferenceCenter,
513    F: ReferenceFrame,
514    U: LengthUnit,
515{
516    type Output = Self;
517
518    /// Translates the position by a displacement vector.
519    #[inline]
520    fn add(self, displacement: Displacement<F, U>) -> Self::Output {
521        Self::from_xyz_with_params(
522            self.center_params.clone(),
523            self.xyz + XYZ::from_array(*displacement.as_array()),
524        )
525    }
526}
527
528impl<C, F, U> Sub<Displacement<F, U>> for Position<C, F, U>
529where
530    C: ReferenceCenter,
531    F: ReferenceFrame,
532    U: LengthUnit,
533{
534    type Output = Self;
535
536    /// Translates the position backwards by a displacement vector.
537    #[inline]
538    fn sub(self, displacement: Displacement<F, U>) -> Self::Output {
539        Self::from_xyz_with_params(
540            self.center_params.clone(),
541            self.xyz - XYZ::from_array(*displacement.as_array()),
542        )
543    }
544}
545
546forward_ref_binop! {
547    impl[C, F, U] Add, add for Position<C, F, U>, Displacement<F, U>
548    where (
549        C: ReferenceCenter,
550        F: ReferenceFrame,
551        U: LengthUnit,
552        C::Params: Copy,
553    )
554}
555
556forward_ref_binop! {
557    impl[C, F, U] Sub, sub for Position<C, F, U>, Displacement<F, U>
558    where (
559        C: ReferenceCenter,
560        F: ReferenceFrame,
561        U: LengthUnit,
562        C::Params: Copy,
563    )
564}
565
566// =============================================================================
567// PartialEq
568// =============================================================================
569
570impl<C, F, U> PartialEq for Position<C, F, U>
571where
572    C: ReferenceCenter,
573    C::Params: PartialEq,
574    F: ReferenceFrame,
575    U: LengthUnit,
576{
577    fn eq(&self, other: &Self) -> bool {
578        self.xyz == other.xyz && self.center_params == other.center_params
579    }
580}
581
582// =============================================================================
583// Display
584// =============================================================================
585
586impl_quantity_fmt_triplet! {
587    impl[C, F, U] for Position<C, F, U>
588    where {
589        C: ReferenceCenter,
590        F: ReferenceFrame,
591        U: LengthUnit,
592    },
593    fmt_each: { Quantity<U>, },
594    |this, f, FmtOne| {
595        write!(
596            f,
597            "Center: {}, Frame: {}, X: ",
598            C::center_name(),
599            F::frame_name()
600        )?;
601        FmtOne::fmt(&this.x(), f)?;
602        write!(f, ", Y: ")?;
603        FmtOne::fmt(&this.y(), f)?;
604        write!(f, ", Z: ")?;
605        FmtOne::fmt(&this.z(), f)
606    }
607}
608
609#[cfg(test)]
610mod tests {
611    use super::*;
612    // Import the derives
613    use crate::{DeriveReferenceCenter as ReferenceCenter, DeriveReferenceFrame as ReferenceFrame};
614
615    #[allow(unused_imports)]
616    use qtty::angular::{Degrees, Radians};
617    #[allow(unused_imports)]
618    use qtty::length::{Kilometers, Meters};
619    use qtty::units::{Kilometer, Meter};
620    use qtty::M;
621    // Define test-specific frame and center
622    #[derive(Debug, Copy, Clone, ReferenceFrame)]
623    struct TestFrame;
624    #[derive(Debug, Copy, Clone, ReferenceCenter)]
625    struct TestCenter;
626
627    #[derive(Clone, Debug, Default, PartialEq)]
628    struct TestParams {
629        id: i32,
630    }
631
632    #[derive(Debug, Copy, Clone, ReferenceCenter)]
633    #[center(params = TestParams)]
634    struct ParamCenter;
635
636    type TestPos = Position<TestCenter, TestFrame, Meter>;
637    type TestDisp = Displacement<TestFrame, Meter>;
638
639    #[test]
640    fn test_position_minus_position_gives_vector() {
641        let a = TestPos::new(1.0, 2.0, 3.0);
642        let b = TestPos::new(4.0, 5.0, 6.0);
643
644        let displacement: TestDisp = b - a;
645        assert!((displacement.x().value() - 3.0).abs() < 1e-12);
646        assert!((displacement.y().value() - 3.0).abs() < 1e-12);
647        assert!((displacement.z().value() - 3.0).abs() < 1e-12);
648    }
649
650    #[test]
651    fn test_position_plus_vector_gives_position() {
652        let pos = TestPos::new(1.0, 2.0, 3.0);
653        let vec = TestDisp::new(1.0, 1.0, 1.0);
654
655        let result: TestPos = pos + vec;
656        assert!((result.x().value() - 2.0).abs() < 1e-12);
657        assert!((result.y().value() - 3.0).abs() < 1e-12);
658        assert!((result.z().value() - 4.0).abs() < 1e-12);
659    }
660
661    #[test]
662    fn test_position_roundtrip() {
663        let a = TestPos::new(1.0, 2.0, 3.0);
664        let b = TestPos::new(4.0, 5.0, 6.0);
665
666        // a + (b - a) == b
667        let displacement = b - a;
668        let result = a + displacement;
669        assert!((result.x().value() - b.x().value()).abs() < 1e-12);
670        assert!((result.y().value() - b.y().value()).abs() < 1e-12);
671        assert!((result.z().value() - b.z().value()).abs() < 1e-12);
672    }
673
674    #[test]
675    fn test_position_distance() {
676        let pos = TestPos::new(3.0, 4.0, 0.0);
677        assert!((pos.distance().value() - 5.0).abs() < 1e-12);
678    }
679
680    #[test]
681    fn test_position_direction() {
682        let pos = TestPos::new(3.0, 4.0, 0.0);
683        let dir = pos.direction().expect("non-zero position");
684        let norm = (dir.x() * dir.x() + dir.y() * dir.y() + dir.z() * dir.z()).sqrt();
685        assert!((norm - 1.0).abs() < 1e-12);
686        assert!((dir.x() - 0.6).abs() < 1e-12);
687        assert!((dir.y() - 0.8).abs() < 1e-12);
688    }
689
690    #[test]
691    fn test_position_with_params_and_from_array() {
692        let params = TestParams { id: 42 };
693        let pos = Position::<ParamCenter, TestFrame, Meter>::new_with_params(
694            params.clone(),
695            1.0,
696            2.0,
697            3.0,
698        );
699        assert_eq!(pos.center_params(), &params);
700
701        let arr = [1.0 * M, 2.0 * M, 3.0 * M];
702        let pos_from_arr =
703            Position::<ParamCenter, TestFrame, Meter>::from_array(params.clone(), arr);
704        assert_eq!(pos_from_arr.center_params(), &params);
705        assert!((pos_from_arr.z().value() - 3.0).abs() < 1e-12);
706    }
707
708    #[test]
709    fn test_position_from_array_origin_and_center() {
710        let arr = [1.0 * M, 2.0 * M, 3.0 * M];
711        let pos = Position::<TestCenter, TestFrame, Meter>::from_array_origin(arr);
712        assert!((pos.x().value() - 1.0).abs() < 1e-12);
713
714        let origin = Position::<TestCenter, TestFrame, Meter>::CENTER;
715        assert!(origin.distance().value().abs() < 1e-12);
716    }
717
718    #[test]
719    fn test_position_distance_to_and_sub_methods() {
720        let a = TestPos::new(0.0, 0.0, 0.0);
721        let b = TestPos::new(0.0, 3.0, 4.0);
722        let dist = a.distance_to(&b);
723        assert!((dist.value() - 5.0).abs() < 1e-12);
724
725        let disp: TestDisp = b - a;
726        assert!((disp.y().value() - 3.0).abs() < 1e-12);
727    }
728
729    #[test]
730    fn test_position_direction_unchecked_and_sub_displacement() {
731        let pos = TestPos::new(0.0, 3.0, 4.0);
732        let dir = pos.direction_unchecked();
733        assert!((dir.y() - 0.6).abs() < 1e-12);
734        assert!((dir.z() - 0.8).abs() < 1e-12);
735
736        let disp = TestDisp::new(1.0, 1.0, 1.0);
737        let moved = pos - disp;
738        assert!((moved.y().value() - 2.0).abs() < 1e-12);
739    }
740
741    #[test]
742    fn test_position_spherical_roundtrip() {
743        let pos = TestPos::new(1.0, 1.0, 1.0);
744        let sph = pos.to_spherical();
745        let back = TestPos::from_spherical(&sph);
746        assert!((back.x().value() - pos.x().value()).abs() < 1e-12);
747        assert!((back.y().value() - pos.y().value()).abs() < 1e-12);
748        assert!((back.z().value() - pos.z().value()).abs() < 1e-12);
749    }
750
751    #[test]
752    fn test_position_const_vec3_and_display() {
753        let pos =
754            Position::<TestCenter, TestFrame, Meter>::new_const((), 1.0 * M, 2.0 * M, 3.0 * M);
755        let vec3 = pos.as_array();
756        assert!((vec3[0].value() - 1.0).abs() < 1e-12);
757        assert!((vec3[1].value() - 2.0).abs() < 1e-12);
758        assert!((vec3[2].value() - 3.0).abs() < 1e-12);
759
760        let text = pos.to_string();
761        assert!(text.contains("Center: TestCenter"));
762        assert!(text.contains("Frame: TestFrame"));
763    }
764
765    #[test]
766    fn test_position_display_respects_format_specifiers() {
767        let pos = TestPos::new(1.234_567, -2.0, 3.5);
768
769        let text_prec = format!("{:.2}", pos);
770        let expected_x_prec = format!("{:.2}", pos.x());
771        assert!(text_prec.contains(&format!("X: {expected_x_prec}")));
772
773        let text_exp = format!("{:.3e}", pos);
774        let expected_z_exp = format!("{:.3e}", pos.z());
775        assert!(text_exp.contains(&format!("Z: {expected_z_exp}")));
776    }
777
778    #[test]
779    fn test_position_sub_ref_ref() {
780        let a = TestPos::new(1.0, 2.0, 3.0);
781        let b = TestPos::new(4.0, 6.0, 9.0);
782        let displacement: TestDisp = b - a;
783        assert!((displacement.x().value() - 3.0).abs() < 1e-12);
784        assert!((displacement.y().value() - 4.0).abs() < 1e-12);
785        assert!((displacement.z().value() - 6.0).abs() < 1e-12);
786    }
787
788    // =========================================================================
789    // Parameterized-center runtime invariant tests
790    // =========================================================================
791
792    type ParamPos = Position<ParamCenter, TestFrame, Meter>;
793
794    #[test]
795    fn test_distance_to_same_params_succeeds() {
796        // Parameterized centers must use try_distance_to; distance_to requires Params = ().
797        let params = TestParams { id: 1 };
798        let a = ParamPos::new_with_params(params.clone(), 0.0, 0.0, 0.0);
799        let b = ParamPos::new_with_params(params, 3.0, 4.0, 0.0);
800        let d = a.try_distance_to(&b).unwrap();
801        assert!((d.value() - 5.0).abs() < 1e-12);
802    }
803
804    #[test]
805    fn test_distance_to_mismatched_params_err() {
806        // Parameterized centers use the fallible try_distance_to; mismatching
807        // params produce Err rather than a panic. Using Sub (operator -) for
808        // a parameterized center is a compile-time error since affn 0.6.
809        let a = ParamPos::new_with_params(TestParams { id: 1 }, 0.0, 0.0, 0.0);
810        let b = ParamPos::new_with_params(TestParams { id: 2 }, 3.0, 4.0, 0.0);
811        assert!(a.try_distance_to(&b).is_err());
812    }
813
814    #[test]
815    fn test_try_distance_to_same_params_ok() {
816        let params = TestParams { id: 1 };
817        let a = ParamPos::new_with_params(params.clone(), 0.0, 0.0, 0.0);
818        let b = ParamPos::new_with_params(params, 3.0, 4.0, 0.0);
819        let result = a.try_distance_to(&b);
820        assert!(result.is_ok());
821        assert!((result.unwrap().value() - 5.0).abs() < 1e-12);
822    }
823
824    #[test]
825    fn test_try_distance_to_mismatched_params_err() {
826        let a = ParamPos::new_with_params(TestParams { id: 1 }, 0.0, 0.0, 0.0);
827        let b = ParamPos::new_with_params(TestParams { id: 2 }, 3.0, 4.0, 0.0);
828        let result = a.try_distance_to(&b);
829        assert!(result.is_err());
830        let err = result.unwrap_err();
831        assert!(err.to_string().contains("center parameter mismatch"));
832    }
833
834    // NOTE: `test_sub_mismatched_params_panics` removed — since affn 0.6 the
835    // `Sub` operator is only available when `C::Params = ()`, so attempting
836    // `ParamPos - ParamPos` is a *compile-time* error. Mismatched-param
837    // subtraction must go through `checked_sub` (see tests below).
838
839    #[test]
840    fn test_checked_sub_same_params_ok() {
841        let params = TestParams { id: 1 };
842        let a = ParamPos::new_with_params(params.clone(), 0.0, 0.0, 0.0);
843        let b = ParamPos::new_with_params(params, 3.0, 4.0, 0.0);
844        let result = b.checked_sub(&a);
845        assert!(result.is_ok());
846        let disp = result.unwrap();
847        assert!((disp.x().value() - 3.0).abs() < 1e-12);
848        assert!((disp.y().value() - 4.0).abs() < 1e-12);
849    }
850
851    #[test]
852    fn test_checked_sub_mismatched_params_err() {
853        let a = ParamPos::new_with_params(TestParams { id: 1 }, 0.0, 0.0, 0.0);
854        let b = ParamPos::new_with_params(TestParams { id: 2 }, 3.0, 4.0, 0.0);
855        let result = b.checked_sub(&a);
856        assert!(result.is_err());
857        let err = result.unwrap_err();
858        assert!(err.to_string().contains("center parameter mismatch"));
859    }
860
861    #[test]
862    fn test_unit_params_operations_always_succeed() {
863        // For Params = (), operations are trivially valid (compile-time guarantee)
864        let a = TestPos::new(0.0, 0.0, 0.0);
865        let b = TestPos::new(3.0, 4.0, 0.0);
866        assert!((a.distance_to(&b).value() - 5.0).abs() < 1e-12);
867        assert!(a.try_distance_to(&b).is_ok());
868        assert!(b.checked_sub(&a).is_ok());
869    }
870
871    #[test]
872    fn test_center_params_mismatch_error_display() {
873        let err = CenterParamsMismatchError {
874            operation: "test_op",
875        };
876        let msg = err.to_string();
877        assert!(msg.contains("test_op"));
878        assert!(msg.contains("center parameter mismatch"));
879
880        // Verify it implements std::error::Error
881        let _: &dyn std::error::Error = &err;
882    }
883
884    #[test]
885    fn test_position_to_unit_roundtrip() {
886        let p_m = Position::<TestCenter, TestFrame, Meter>::new(1.0, -0.5, 2.25);
887        let p_km: Position<TestCenter, TestFrame, Kilometer> = p_m.to_unit();
888        let back: Position<TestCenter, TestFrame, Meter> = p_km.to_unit();
889
890        assert!((back.x().value() - p_m.x().value()).abs() < 1e-12);
891        assert!((back.y().value() - p_m.y().value()).abs() < 1e-12);
892        assert!((back.z().value() - p_m.z().value()).abs() < 1e-12);
893    }
894
895    #[test]
896    fn test_position_to_unit_preserves_center_params() {
897        let p_m = ParamPos::new_with_params(TestParams { id: 7 }, 1.0, 2.0, 3.0);
898        let p_km: Position<ParamCenter, TestFrame, Kilometer> = p_m.to_unit();
899
900        assert_eq!(p_km.center_params().id, 7);
901        assert!((p_km.x().value() - 0.001).abs() < 1e-12);
902        assert!((p_km.y().value() - 0.002).abs() < 1e-12);
903        assert!((p_km.z().value() - 0.003).abs() < 1e-12);
904    }
905}