Skip to main content

oxigdal_noalloc/
lib.rs

1//! # oxigdal-noalloc
2//!
3//! `no_std`, `no_alloc` fixed-size geometry primitives for OxiGDAL.
4//!
5//! This crate provides zero-allocation geometry types suitable for embedded
6//! and RISC-V environments where heap allocation is unavailable or undesirable.
7//!
8//! ## Types
9//!
10//! - [`Point2D`] — 2D point with distance and midpoint operations
11//! - [`Point3D`] — 3D point
12//! - [`BBox2D`] — 2D axis-aligned bounding box
13//! - [`BBox3D`] — 3D axis-aligned bounding box
14//! - [`LineSegment2D`] — 2D line segment with intersection support
15//! - [`Triangle2D`] — 2D triangle with area, containment, and centroid
16//! - [`FixedPolygon`] — Fixed-capacity polygon backed by an inline array
17//! - [`FixedLineString`] — Fixed-capacity polyline backed by an inline array
18//! - [`FixedRing`] — Fixed-capacity closed ring with area and containment
19//! - [`CoordTransform`] — 2D affine transform (2×3 matrix)
20//! - [`GeoHashFixed`] — Geohash encoding stored as `[u8; 12]`
21//! - [`NoAllocError`] — Error enum for no-alloc operations
22//!
23//! ## Projections
24//!
25//! - [`mercator_forward`] — Web Mercator (EPSG:3857) forward projection
26//! - [`mercator_inverse`] — Web Mercator (EPSG:3857) inverse projection
27
28#![no_std]
29#![warn(missing_docs)]
30#![deny(unsafe_code)]
31
32pub mod bbox3d;
33pub mod geohash;
34pub mod linestring;
35pub mod mercator;
36pub mod ring;
37
38pub use bbox3d::BBox3D;
39pub use geohash::GeoHashFixed;
40pub use linestring::FixedLineString;
41pub use mercator::{mercator_forward, mercator_inverse};
42pub use ring::FixedRing;
43
44// ── Error type ────────────────────────────────────────────────────────────────
45
46/// Errors that can occur in no-alloc geometry operations.
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum NoAllocError {
49    /// The fixed-capacity container is full.
50    CapacityExceeded,
51    /// Geohash precision is outside the range 1–12.
52    InvalidPrecision,
53    /// Geometry contains no vertices.
54    EmptyGeometry,
55}
56
57// ── Point2D ───────────────────────────────────────────────────────────────────
58
59/// A 2-dimensional point.
60#[derive(Debug, Clone, Copy, PartialEq)]
61pub struct Point2D {
62    /// X coordinate.
63    pub x: f64,
64    /// Y coordinate.
65    pub y: f64,
66}
67
68impl Point2D {
69    /// Creates a new `Point2D`.
70    #[must_use]
71    #[inline]
72    pub const fn new(x: f64, y: f64) -> Self {
73        Self { x, y }
74    }
75
76    /// Computes the Euclidean distance to another point.
77    #[must_use]
78    #[inline]
79    pub fn distance_to(&self, other: &Point2D) -> f64 {
80        let dx = self.x - other.x;
81        let dy = self.y - other.y;
82        libm_sqrt(dx * dx + dy * dy)
83    }
84
85    /// Returns the midpoint between this point and another.
86    #[must_use]
87    #[inline]
88    pub fn midpoint(&self, other: &Point2D) -> Point2D {
89        Point2D::new((self.x + other.x) * 0.5, (self.y + other.y) * 0.5)
90    }
91}
92
93// ── Point3D ───────────────────────────────────────────────────────────────────
94
95/// A 3-dimensional point.
96#[derive(Debug, Clone, Copy, PartialEq)]
97pub struct Point3D {
98    /// X coordinate.
99    pub x: f64,
100    /// Y coordinate.
101    pub y: f64,
102    /// Z coordinate.
103    pub z: f64,
104}
105
106impl Point3D {
107    /// Creates a new `Point3D`.
108    #[must_use]
109    #[inline]
110    pub const fn new(x: f64, y: f64, z: f64) -> Self {
111        Self { x, y, z }
112    }
113
114    /// Computes the Euclidean distance to another point.
115    #[must_use]
116    #[inline]
117    pub fn distance_to(&self, other: &Point3D) -> f64 {
118        let dx = self.x - other.x;
119        let dy = self.y - other.y;
120        let dz = self.z - other.z;
121        libm_sqrt(dx * dx + dy * dy + dz * dz)
122    }
123
124    /// Projects to a 2D point by dropping the Z component.
125    #[must_use]
126    #[inline]
127    pub const fn to_2d(&self) -> Point2D {
128        Point2D::new(self.x, self.y)
129    }
130}
131
132// ── BBox2D ────────────────────────────────────────────────────────────────────
133
134/// A 2-dimensional axis-aligned bounding box.
135#[derive(Debug, Clone, Copy, PartialEq)]
136pub struct BBox2D {
137    /// Minimum X.
138    pub min_x: f64,
139    /// Minimum Y.
140    pub min_y: f64,
141    /// Maximum X.
142    pub max_x: f64,
143    /// Maximum Y.
144    pub max_y: f64,
145}
146
147impl BBox2D {
148    /// Creates a new `BBox2D`.
149    #[must_use]
150    #[inline]
151    pub const fn new(min_x: f64, min_y: f64, max_x: f64, max_y: f64) -> Self {
152        Self {
153            min_x,
154            min_y,
155            max_x,
156            max_y,
157        }
158    }
159
160    /// Returns `true` if the bounding box is geometrically valid (min ≤ max).
161    #[must_use]
162    #[inline]
163    pub fn is_valid(&self) -> bool {
164        self.min_x <= self.max_x && self.min_y <= self.max_y
165    }
166
167    /// Returns `true` if the point lies inside (or on the boundary of) this box.
168    #[must_use]
169    #[inline]
170    pub fn contains_point(&self, p: Point2D) -> bool {
171        p.x >= self.min_x && p.x <= self.max_x && p.y >= self.min_y && p.y <= self.max_y
172    }
173
174    /// Returns `true` if this bounding box overlaps with another.
175    #[must_use]
176    #[inline]
177    pub fn intersects(&self, other: &BBox2D) -> bool {
178        self.min_x <= other.max_x
179            && self.max_x >= other.min_x
180            && self.min_y <= other.max_y
181            && self.max_y >= other.min_y
182    }
183
184    /// Computes the smallest bounding box containing both boxes.
185    #[must_use]
186    #[inline]
187    pub fn union(&self, other: &BBox2D) -> BBox2D {
188        BBox2D::new(
189            f64_min(self.min_x, other.min_x),
190            f64_min(self.min_y, other.min_y),
191            f64_max(self.max_x, other.max_x),
192            f64_max(self.max_y, other.max_y),
193        )
194    }
195
196    /// Computes the area of the bounding box.
197    #[must_use]
198    #[inline]
199    pub fn area(&self) -> f64 {
200        let w = self.max_x - self.min_x;
201        let h = self.max_y - self.min_y;
202        if w < 0.0 || h < 0.0 { 0.0 } else { w * h }
203    }
204}
205
206// ── LineSegment2D ─────────────────────────────────────────────────────────────
207
208/// A directed 2D line segment from `start` to `end`.
209#[derive(Debug, Clone, Copy, PartialEq)]
210pub struct LineSegment2D {
211    /// Start point.
212    pub start: Point2D,
213    /// End point.
214    pub end: Point2D,
215}
216
217impl LineSegment2D {
218    /// Creates a new `LineSegment2D`.
219    #[must_use]
220    #[inline]
221    pub const fn new(start: Point2D, end: Point2D) -> Self {
222        Self { start, end }
223    }
224
225    /// Returns the length of the segment.
226    #[must_use]
227    #[inline]
228    pub fn length(&self) -> f64 {
229        self.start.distance_to(&self.end)
230    }
231
232    /// Returns the midpoint of the segment.
233    #[must_use]
234    #[inline]
235    pub fn midpoint(&self) -> Point2D {
236        self.start.midpoint(&self.end)
237    }
238
239    /// Returns the point at parameter `t` along the segment.
240    ///
241    /// `t = 0.0` returns `start`, `t = 1.0` returns `end`.
242    #[must_use]
243    #[inline]
244    pub fn point_on_segment(&self, t: f64) -> Point2D {
245        Point2D::new(
246            self.start.x + t * (self.end.x - self.start.x),
247            self.start.y + t * (self.end.y - self.start.y),
248        )
249    }
250
251    /// Computes the intersection point of two line segments, if it exists.
252    ///
253    /// Uses parametric form. Returns `None` for parallel/coincident segments.
254    #[must_use]
255    pub fn intersects(&self, other: &LineSegment2D) -> Option<Point2D> {
256        let dx1 = self.end.x - self.start.x;
257        let dy1 = self.end.y - self.start.y;
258        let dx2 = other.end.x - other.start.x;
259        let dy2 = other.end.y - other.start.y;
260
261        let denom = dx1 * dy2 - dy1 * dx2;
262
263        if denom.abs() < f64::EPSILON * 1e6 {
264            return None; // parallel or coincident
265        }
266
267        let ox = other.start.x - self.start.x;
268        let oy = other.start.y - self.start.y;
269
270        let t = (ox * dy2 - oy * dx2) / denom;
271        let u = (ox * dy1 - oy * dx1) / denom;
272
273        if (0.0..=1.0).contains(&t) && (0.0..=1.0).contains(&u) {
274            Some(self.point_on_segment(t))
275        } else {
276            None
277        }
278    }
279}
280
281// ── Triangle2D ────────────────────────────────────────────────────────────────
282
283/// A triangle defined by three 2D vertices.
284#[derive(Debug, Clone, Copy, PartialEq)]
285pub struct Triangle2D {
286    /// First vertex.
287    pub a: Point2D,
288    /// Second vertex.
289    pub b: Point2D,
290    /// Third vertex.
291    pub c: Point2D,
292}
293
294impl Triangle2D {
295    /// Creates a new `Triangle2D`.
296    #[must_use]
297    #[inline]
298    pub const fn new(a: Point2D, b: Point2D, c: Point2D) -> Self {
299        Self { a, b, c }
300    }
301
302    /// Computes the signed area using the shoelace formula.
303    ///
304    /// Positive for counter-clockwise, negative for clockwise.
305    #[must_use]
306    #[inline]
307    pub fn signed_area(&self) -> f64 {
308        0.5 * ((self.b.x - self.a.x) * (self.c.y - self.a.y)
309            - (self.c.x - self.a.x) * (self.b.y - self.a.y))
310    }
311
312    /// Computes the absolute area.
313    #[must_use]
314    #[inline]
315    pub fn area(&self) -> f64 {
316        self.signed_area().abs()
317    }
318
319    /// Returns `true` if the vertices are arranged clockwise.
320    #[must_use]
321    #[inline]
322    pub fn is_clockwise(&self) -> bool {
323        self.signed_area() < 0.0
324    }
325
326    /// Computes the centroid (arithmetic mean of the three vertices).
327    #[must_use]
328    #[inline]
329    pub fn centroid(&self) -> Point2D {
330        Point2D::new(
331            (self.a.x + self.b.x + self.c.x) / 3.0,
332            (self.a.y + self.b.y + self.c.y) / 3.0,
333        )
334    }
335
336    /// Returns the perimeter (sum of side lengths).
337    #[must_use]
338    pub fn perimeter(&self) -> f64 {
339        self.a.distance_to(&self.b) + self.b.distance_to(&self.c) + self.c.distance_to(&self.a)
340    }
341
342    /// Tests whether a point is inside or on the boundary of the triangle.
343    ///
344    /// Uses barycentric coordinates.
345    #[must_use]
346    pub fn contains_point(&self, p: Point2D) -> bool {
347        // Barycentric coordinates via signed areas
348        let s_abc = self.signed_area();
349        if s_abc.abs() < f64::EPSILON {
350            return false; // degenerate triangle
351        }
352
353        let s_pbc = Triangle2D::new(p, self.b, self.c).signed_area();
354        let s_apc = Triangle2D::new(self.a, p, self.c).signed_area();
355        let s_abp = Triangle2D::new(self.a, self.b, p).signed_area();
356
357        let same_sign = |a: f64, b: f64| (a >= 0.0) == (b >= 0.0);
358
359        same_sign(s_abc, s_pbc) && same_sign(s_abc, s_apc) && same_sign(s_abc, s_abp)
360    }
361}
362
363// ── FixedPolygon ──────────────────────────────────────────────────────────────
364
365/// A polygon with a statically-allocated vertex array of capacity `N`.
366///
367/// Vertices are stored inline; no heap allocation is performed.
368pub struct FixedPolygon<const N: usize> {
369    vertices: [Point2D; N],
370    len: usize,
371}
372
373impl<const N: usize> FixedPolygon<N> {
374    /// Creates an empty `FixedPolygon`.
375    #[must_use]
376    #[inline]
377    pub fn new() -> Self {
378        Self {
379            vertices: [Point2D::new(0.0, 0.0); N],
380            len: 0,
381        }
382    }
383
384    /// Attempts to push a vertex.  Returns `false` if the polygon is full.
385    #[inline]
386    pub fn try_push(&mut self, p: Point2D) -> bool {
387        if self.len >= N {
388            return false;
389        }
390        self.vertices[self.len] = p;
391        self.len += 1;
392        true
393    }
394
395    /// Returns the number of vertices.
396    #[must_use]
397    #[inline]
398    pub fn len(&self) -> usize {
399        self.len
400    }
401
402    /// Returns `true` if the polygon has no vertices.
403    #[must_use]
404    #[inline]
405    pub fn is_empty(&self) -> bool {
406        self.len == 0
407    }
408
409    /// Returns a slice of the current vertices.
410    #[must_use]
411    #[inline]
412    pub fn vertices(&self) -> &[Point2D] {
413        &self.vertices[..self.len]
414    }
415
416    /// Computes the signed shoelace area (positive for CCW).
417    #[must_use]
418    pub fn signed_area(&self) -> f64 {
419        if self.len < 3 {
420            return 0.0;
421        }
422        let mut sum = 0.0_f64;
423        let verts = self.vertices();
424        for i in 0..self.len {
425            let j = (i + 1) % self.len;
426            sum += verts[i].x * verts[j].y;
427            sum -= verts[j].x * verts[i].y;
428        }
429        sum * 0.5
430    }
431
432    /// Computes the absolute area using the shoelace formula.
433    #[must_use]
434    #[inline]
435    pub fn area(&self) -> f64 {
436        self.signed_area().abs()
437    }
438
439    /// Computes the perimeter (sum of edge lengths).
440    #[must_use]
441    pub fn perimeter(&self) -> f64 {
442        if self.len < 2 {
443            return 0.0;
444        }
445        let verts = self.vertices();
446        let mut total = 0.0_f64;
447        for i in 0..self.len {
448            let j = (i + 1) % self.len;
449            total += verts[i].distance_to(&verts[j]);
450        }
451        total
452    }
453
454    /// Returns the axis-aligned bounding box, or `None` if the polygon is empty.
455    #[must_use]
456    pub fn bbox(&self) -> Option<BBox2D> {
457        if self.is_empty() {
458            return None;
459        }
460        let verts = self.vertices();
461        let mut min_x = verts[0].x;
462        let mut min_y = verts[0].y;
463        let mut max_x = verts[0].x;
464        let mut max_y = verts[0].y;
465        for v in verts.iter().skip(1) {
466            min_x = f64_min(min_x, v.x);
467            min_y = f64_min(min_y, v.y);
468            max_x = f64_max(max_x, v.x);
469            max_y = f64_max(max_y, v.y);
470        }
471        Some(BBox2D::new(min_x, min_y, max_x, max_y))
472    }
473
474    /// Computes the centroid, or `None` if the polygon is empty.
475    #[must_use]
476    pub fn centroid(&self) -> Option<Point2D> {
477        if self.is_empty() {
478            return None;
479        }
480        let verts = self.vertices();
481        let mut cx = 0.0_f64;
482        let mut cy = 0.0_f64;
483        for v in verts {
484            cx += v.x;
485            cy += v.y;
486        }
487        let n = self.len as f64;
488        Some(Point2D::new(cx / n, cy / n))
489    }
490}
491
492impl<const N: usize> Default for FixedPolygon<N> {
493    fn default() -> Self {
494        Self::new()
495    }
496}
497
498// ── CoordTransform ────────────────────────────────────────────────────────────
499
500/// A 2D affine transformation stored as a 2×3 matrix.
501///
502/// The transformation is applied as:
503/// ```text
504/// x' = a*x + b*y + c
505/// y' = d*x + e*y + f
506/// ```
507/// where `matrix = [a, b, c, d, e, f]`.
508#[derive(Debug, Clone, Copy, PartialEq)]
509pub struct CoordTransform {
510    matrix: [f64; 6],
511}
512
513impl CoordTransform {
514    /// Creates the identity transform.
515    #[must_use]
516    #[inline]
517    pub const fn identity() -> Self {
518        Self {
519            matrix: [1.0, 0.0, 0.0, 0.0, 1.0, 0.0],
520        }
521    }
522
523    /// Creates a uniform scale transform.
524    #[must_use]
525    #[inline]
526    pub const fn scale(sx: f64, sy: f64) -> Self {
527        Self {
528            matrix: [sx, 0.0, 0.0, 0.0, sy, 0.0],
529        }
530    }
531
532    /// Creates a translation transform.
533    #[must_use]
534    #[inline]
535    pub const fn translate(tx: f64, ty: f64) -> Self {
536        Self {
537            matrix: [1.0, 0.0, tx, 0.0, 1.0, ty],
538        }
539    }
540
541    /// Creates a counter-clockwise rotation transform.
542    #[must_use]
543    pub fn rotate(angle_radians: f64) -> Self {
544        let cos_a = libm_cos(angle_radians);
545        let sin_a = libm_sin(angle_radians);
546        Self {
547            matrix: [cos_a, -sin_a, 0.0, sin_a, cos_a, 0.0],
548        }
549    }
550
551    /// Composes `self` followed by `other` into a single transform.
552    ///
553    /// The result applies `self` first, then `other`.
554    /// Mathematically this is `other_matrix × self_matrix` because affine
555    /// transforms are applied right-to-left in column-vector notation.
556    #[must_use]
557    pub fn compose(self, other: &CoordTransform) -> Self {
558        // self  = T1: applied first
559        // other = T2: applied second
560        // Combined = T2 × T1
561        //
562        // [a2 b2 c2]   [a1 b1 c1]
563        // [d2 e2 f2] × [d1 e1 f1]
564        // [0  0  1 ]   [0  0  1 ]
565        let [a1, b1, c1, d1, e1, f1] = self.matrix;
566        let [a2, b2, c2, d2, e2, f2] = other.matrix;
567        Self {
568            matrix: [
569                a2 * a1 + b2 * d1,
570                a2 * b1 + b2 * e1,
571                a2 * c1 + b2 * f1 + c2,
572                d2 * a1 + e2 * d1,
573                d2 * b1 + e2 * e1,
574                d2 * c1 + e2 * f1 + f2,
575            ],
576        }
577    }
578
579    /// Applies the transform to a 2D point.
580    #[must_use]
581    #[inline]
582    pub fn apply(&self, p: Point2D) -> Point2D {
583        let [a, b, c, d, e, f] = self.matrix;
584        Point2D::new(a * p.x + b * p.y + c, d * p.x + e * p.y + f)
585    }
586
587    /// Applies the 2D transform to a 3D point, passing the Z coordinate through unchanged.
588    #[must_use]
589    #[inline]
590    pub fn apply3d(&self, p: Point3D) -> Point3D {
591        let p2 = self.apply(p.to_2d());
592        Point3D::new(p2.x, p2.y, p.z)
593    }
594}
595
596// ── Internal float helpers ────────────────────────────────────────────────────
597// In no_std, f64 transcendental functions are not available from core.
598// We implement them using a simple call to our own no_std libm wrappers.
599//
600// The workspace has `libm = "0.2"` available as a workspace dep.
601// However, this crate intentionally avoids even alloc, so we inline
602// simple software implementations for sqrt, sin, cos using compiler intrinsics
603// which ARE available on supported targets (aarch64, x86_64, riscv).
604//
605// On hosted no_std targets the compiler lowers f64::sqrt() to hardware if
606// the -C target-feature includes the FPU.  On soft-float targets one must
607// link a libm; that is the deployer's responsibility (standard practice).
608//
609// We use the `unsafe` intrinsic approach through a shim that compiles cleanly.
610
611/// Portable f64 sqrt using Newton-Raphson iteration.
612///
613/// Initial guess: halve the IEEE 754 biased exponent to get an approximation
614/// of 2^(e/2), which is a good starting point for sqrt.
615/// Five iterations are sufficient for full f64 precision.
616#[inline(always)]
617pub(crate) fn libm_sqrt(x: f64) -> f64 {
618    if x <= 0.0 {
619        return if x == 0.0 { 0.0 } else { f64::NAN };
620    }
621    // Initial guess via IEEE-754 exponent bisection.
622    // For x = m * 2^e, sqrt(x) ≈ sqrt(m) * 2^(e/2).
623    // We approximate sqrt(m) ≈ 1.0 and halve the exponent.
624    // bits of f64: [sign(1)] [exponent(11)] [mantissa(52)]
625    // bias = 1023; to halve: new_exp = (old_exp + 1023) / 2 → add 1023, halve, subtract 1023
626    let bits = x.to_bits();
627    // Extract biased exponent (bits 62..52), halve it (rounding toward even), reconstruct
628    let biased_exp = (bits >> 52) & 0x7FF;
629    let new_biased_exp = (biased_exp + 1023) >> 1;
630    // Reconstruct with zero mantissa (approximation)
631    let r_bits = new_biased_exp << 52;
632    let mut r = f64::from_bits(r_bits);
633    // Newton-Raphson: r = (r + x/r) / 2, 6 iterations → ~18 correct digits
634    r = (r + x / r) * 0.5;
635    r = (r + x / r) * 0.5;
636    r = (r + x / r) * 0.5;
637    r = (r + x / r) * 0.5;
638    r = (r + x / r) * 0.5;
639    r = (r + x / r) * 0.5;
640    r
641}
642
643/// Portable f64 sin using Taylor series with argument reduction to [-π/4, π/4].
644///
645/// Uses quadrant-based reduction for maximum accuracy across the full range.
646#[inline(always)]
647pub(crate) fn libm_sin(x: f64) -> f64 {
648    let (s, c) = sin_cos_core(x);
649    let _ = c;
650    s
651}
652
653/// Portable f64 cos using Taylor series with argument reduction to [-π/4, π/4].
654#[inline(always)]
655pub(crate) fn libm_cos(x: f64) -> f64 {
656    let (s, c) = sin_cos_core(x);
657    let _ = s;
658    c
659}
660
661/// Core sin/cos computation via quadrant reduction + Horner-form Taylor series.
662///
663/// Reduces `x` to `[-π/4, π/4]` using `k = round(x / (π/2))` and then
664/// routes between sin-series and cos-series based on the quadrant parity.
665#[rustfmt::skip]
666#[inline(always)]
667pub(crate) fn sin_cos_core(x: f64) -> (f64, f64) {
668    let pi = core::f64::consts::PI;
669    let two_pi = 2.0 * pi;
670    let half_pi = pi * 0.5;
671    let quarter_pi = pi * 0.25;
672
673    // Reduce to [-pi, pi]
674    let mut x = x % two_pi;
675    if x > pi {
676        x -= two_pi;
677    }
678    if x < -pi {
679        x += two_pi;
680    }
681
682    // Determine quadrant: k in {0, 1, 2, 3}
683    // round(x / (π/2)) gives the nearest multiple of π/2
684    let ratio = x / half_pi;
685    // Manual round: add 0.5 with sign matching, then truncate
686    let k_f = if ratio >= 0.0 {
687        (ratio + 0.5) as i64 as f64
688    } else {
689        (ratio - 0.5) as i64 as f64
690    };
691    let k = k_f as i64;
692    let r = x - k_f * half_pi; // |r| ≤ π/4
693
694    // Ignore unused warning suppression: quarter_pi is used conceptually
695    let _ = quarter_pi;
696
697    // Taylor series for sin(r) and cos(r) — both accurate on [-π/4, π/4]
698    let r2 = r * r;
699    // sin(r) = r * (1 - r²/6 + r⁴/120 - r⁶/5040 + r⁸/362880 - r¹⁰/39916800 + r¹²/6227020800)
700    let sin_r = r
701        * (1.0
702            + r2 * (-1.0 / 6.0
703                + r2 * (1.0 / 120.0
704                    + r2 * (-1.0 / 5040.0
705                        + r2 * (1.0 / 362_880.0
706                            + r2 * (-1.0 / 39_916_800.0 + r2 * (1.0 / 6_227_020_800.0)))))));
707    // cos(r) = 1 - r²/2 + r⁴/24 - r⁶/720 + r⁸/40320 - r¹⁰/3628800 + r¹²/479001600
708    let cos_r = 1.0
709        + r2 * (-1.0 / 2.0
710            + r2 * (1.0 / 24.0
711                + r2 * (-1.0 / 720.0
712                    + r2 * (1.0 / 40_320.0
713                        + r2 * (-1.0 / 3_628_800.0 + r2 * (1.0 / 479_001_600.0))))));
714
715    // Reconstruct sin(x) and cos(x) from quadrant and reduced values
716    // k mod 4 determines which quadrant:
717    //   0: sin(x) =  sin(r),  cos(x) =  cos(r)
718    //   1: sin(x) =  cos(r),  cos(x) = -sin(r)
719    //   2: sin(x) = -sin(r),  cos(x) = -cos(r)
720    //   3: sin(x) = -cos(r),  cos(x) =  sin(r)
721    let kmod = ((k % 4) + 4) as u64 % 4;
722    match kmod {
723        0 => (sin_r, cos_r),
724        1 => (cos_r, -sin_r),
725        2 => (-sin_r, -cos_r),
726        _ => (-cos_r, sin_r),
727    }
728}
729
730/// f64 min without std.
731#[inline(always)]
732pub(crate) fn f64_min(a: f64, b: f64) -> f64 {
733    if a < b { a } else { b }
734}
735
736/// f64 max without std.
737#[inline(always)]
738pub(crate) fn f64_max(a: f64, b: f64) -> f64 {
739    if a > b { a } else { b }
740}
741
742/// f64 abs without std.
743#[inline(always)]
744pub(crate) fn f64_abs(x: f64) -> f64 {
745    if x < 0.0 { -x } else { x }
746}
747
748/// f64 floor without std — truncate toward negative infinity.
749#[inline(always)]
750pub(crate) fn f64_floor(x: f64) -> f64 {
751    let t = x as i64 as f64;
752    if t > x { t - 1.0 } else { t }
753}
754
755/// Portable natural logarithm (ln) using IEEE-754 exponent extraction
756/// and a Padé approximant on the mantissa range [1, 2).
757///
758/// Returns `NAN` for `x < 0`, `NEG_INFINITY` for `x == 0`, `0.0` for `x == 1`.
759#[rustfmt::skip]
760#[inline(always)]
761pub(crate) fn libm_ln(x: f64) -> f64 {
762    if x.is_nan() {
763        return f64::NAN; // NaN input
764    }
765    if x < 0.0 {
766        return f64::NAN;
767    }
768    if x == 0.0 {
769        return f64::NEG_INFINITY;
770    }
771    if x == f64::INFINITY {
772        return f64::INFINITY;
773    }
774
775    // Extract IEEE-754 components
776    let bits = x.to_bits();
777    let biased_exp = ((bits >> 52) & 0x7FF) as i64;
778    let mantissa_bits = bits & 0x000F_FFFF_FFFF_FFFF;
779
780    // Reconstruct mantissa in [1.0, 2.0): set exponent to 1023 (bias)
781    let m = f64::from_bits((1023_u64 << 52) | mantissa_bits);
782    let e = biased_exp - 1023; // true exponent
783
784    // Compute ln(m) for m in [1, 2) using the substitution t = (m-1)/(m+1)
785    // ln(m) = 2 * (t + t^3/3 + t^5/5 + ... + t^(2k+1)/(2k+1))
786    // For m in [1,2), |t| < 1/3, so the series converges rapidly.
787    // 12 terms (up to t^23) give full f64 precision.
788    let t = (m - 1.0) / (m + 1.0);
789    let t2 = t * t;
790    let ln_m = 2.0
791        * t
792        * (1.0
793            + t2 * (1.0 / 3.0
794                + t2 * (1.0 / 5.0
795                    + t2 * (1.0 / 7.0
796                        + t2 * (1.0 / 9.0
797                            + t2 * (1.0 / 11.0
798                                + t2 * (1.0 / 13.0
799                                    + t2 * (1.0 / 15.0
800                                        + t2 * (1.0 / 17.0
801                                            + t2 * (1.0 / 19.0
802                                                + t2 * (1.0 / 21.0 + t2 * (1.0 / 23.0))))))))))));
803
804    // ln(x) = e * ln(2) + ln(m)
805    e as f64 * core::f64::consts::LN_2 + ln_m
806}
807
808/// Portable exp(x) using range reduction via ln(2) and Taylor series.
809///
810/// Decomposes `x = n * ln(2) + r` where `|r| <= ln(2)/2`,
811/// then `exp(x) = 2^n * exp(r)` with Taylor series for `exp(r)`.
812#[rustfmt::skip]
813#[inline(always)]
814pub(crate) fn libm_exp(x: f64) -> f64 {
815    if x.is_nan() {
816        return f64::NAN;
817    }
818    if x == f64::INFINITY {
819        return f64::INFINITY;
820    }
821    if x == f64::NEG_INFINITY {
822        return 0.0;
823    }
824
825    // Range reduction: n = round(x / ln(2))
826    let n_f = f64_floor(x * core::f64::consts::LOG2_E + 0.5);
827    let n = n_f as i64;
828    let r = x - n_f * core::f64::consts::LN_2;
829
830    // Taylor series for exp(r), 13 terms for full f64 precision on |r| <= ln(2)/2
831    let r2 = r * r;
832    let exp_r = 1.0
833        + r * (1.0
834            + r * (1.0 / 2.0
835                + r * (1.0 / 6.0
836                    + r * (1.0 / 24.0
837                        + r * (1.0 / 120.0
838                            + r * (1.0 / 720.0
839                                + r * (1.0 / 5_040.0
840                                    + r * (1.0 / 40_320.0
841                                        + r * (1.0 / 362_880.0
842                                            + r * (1.0 / 3_628_800.0
843                                                + r * (1.0 / 39_916_800.0
844                                                    + r * (1.0 / 479_001_600.0))))))))))));
845    let _ = r2; // used conceptually for series convergence analysis
846
847    // Reconstruct 2^n * exp(r) via IEEE-754 exponent manipulation
848    // Clamp n to avoid overflow/underflow in the bit representation
849    if n > 1023 {
850        return f64::INFINITY;
851    }
852    if n < -1074 {
853        return 0.0;
854    }
855
856    // For large |n|, split the multiplication to avoid subnormal intermediate
857    if n >= -1022 {
858        let pow2 = f64::from_bits(((n + 1023) as u64) << 52);
859        exp_r * pow2
860    } else {
861        // Subnormal range: split into two multiplications
862        let pow2a = f64::from_bits(1_u64 << 52); // 2^(-1022)
863        let pow2b = f64::from_bits(((n + 1023 + 1022) as u64) << 52);
864        exp_r * pow2a * pow2b
865    }
866}
867
868/// Portable atan(x) using argument reduction and polynomial approximation.
869///
870/// Reduces `|x| > 1` via `atan(x) = pi/2 - atan(1/x)`.
871/// Further reduces `|x| > tan(pi/12)` via
872/// `atan(x) = pi/6 + atan((x - 1/sqrt(3)) / (1 + x/sqrt(3)))`.
873/// Uses a degree-13 minimax polynomial on `[0, tan(pi/12)]`.
874#[rustfmt::skip]
875#[inline(always)]
876pub(crate) fn libm_atan(x: f64) -> f64 {
877    if x.is_nan() {
878        return f64::NAN;
879    }
880
881    let neg = x < 0.0;
882    let mut a = f64_abs(x);
883    let mut hi = 0.0_f64;
884
885    // Reduce range: if a > 1, use atan(a) = pi/2 - atan(1/a)
886    let reciprocal = a > 1.0;
887    if reciprocal {
888        a = 1.0 / a;
889        hi = core::f64::consts::FRAC_PI_2;
890    }
891
892    // Further reduce: if a > tan(pi/12) ≈ 0.2679, use
893    // atan(a) = pi/6 + atan((a - 1/sqrt(3)) / (1 + a/sqrt(3)))
894    const TAN_PI_12: f64 = 0.267_949_192_431_122_7; // tan(pi/12)
895    const INV_SQRT3: f64 = 0.577_350_269_189_625_8; // 1/sqrt(3)
896    if a > TAN_PI_12 {
897        let a_new = (a - INV_SQRT3) / (1.0 + a * INV_SQRT3);
898        if reciprocal {
899            hi -= core::f64::consts::FRAC_PI_6;
900        } else {
901            hi += core::f64::consts::FRAC_PI_6;
902        }
903        a = a_new;
904    }
905
906    // Polynomial approximation: atan(a) ≈ a - a^3/3 + a^5/5 - a^7/7 + ...
907    // for |a| <= tan(pi/12) ≈ 0.268
908    // 10 terms (up to a^19) give full f64 precision on this reduced range.
909    let a2 = a * a;
910    let result = a
911        * (1.0
912            + a2 * (-1.0 / 3.0
913                + a2 * (1.0 / 5.0
914                    + a2 * (-1.0 / 7.0
915                        + a2 * (1.0 / 9.0
916                            + a2 * (-1.0 / 11.0
917                                + a2 * (1.0 / 13.0
918                                    + a2 * (-1.0 / 15.0
919                                        + a2 * (1.0 / 17.0 + a2 * (-1.0 / 19.0))))))))));
920
921    let val = if reciprocal { hi - result } else { hi + result };
922
923    if neg { -val } else { val }
924}