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//! - [`LineSegment2D`] — 2D line segment with intersection support
14//! - [`Triangle2D`] — 2D triangle with area, containment, and centroid
15//! - [`FixedPolygon`] — Fixed-capacity polygon backed by an inline array
16//! - [`CoordTransform`] — 2D affine transform (2×3 matrix)
17//! - [`GeoHashFixed`] — Geohash encoding stored as `[u8; 12]`
18//! - [`NoAllocError`] — Error enum for no-alloc operations
19
20#![no_std]
21#![warn(missing_docs)]
22#![deny(unsafe_code)]
23
24pub mod geohash;
25
26pub use geohash::GeoHashFixed;
27
28// ── Error type ────────────────────────────────────────────────────────────────
29
30/// Errors that can occur in no-alloc geometry operations.
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum NoAllocError {
33    /// The fixed-capacity container is full.
34    CapacityExceeded,
35    /// Geohash precision is outside the range 1–12.
36    InvalidPrecision,
37    /// Geometry contains no vertices.
38    EmptyGeometry,
39}
40
41// ── Point2D ───────────────────────────────────────────────────────────────────
42
43/// A 2-dimensional point.
44#[derive(Debug, Clone, Copy, PartialEq)]
45pub struct Point2D {
46    /// X coordinate.
47    pub x: f64,
48    /// Y coordinate.
49    pub y: f64,
50}
51
52impl Point2D {
53    /// Creates a new `Point2D`.
54    #[must_use]
55    #[inline]
56    pub const fn new(x: f64, y: f64) -> Self {
57        Self { x, y }
58    }
59
60    /// Computes the Euclidean distance to another point.
61    #[must_use]
62    #[inline]
63    pub fn distance_to(&self, other: &Point2D) -> f64 {
64        let dx = self.x - other.x;
65        let dy = self.y - other.y;
66        libm_sqrt(dx * dx + dy * dy)
67    }
68
69    /// Returns the midpoint between this point and another.
70    #[must_use]
71    #[inline]
72    pub fn midpoint(&self, other: &Point2D) -> Point2D {
73        Point2D::new((self.x + other.x) * 0.5, (self.y + other.y) * 0.5)
74    }
75}
76
77// ── Point3D ───────────────────────────────────────────────────────────────────
78
79/// A 3-dimensional point.
80#[derive(Debug, Clone, Copy, PartialEq)]
81pub struct Point3D {
82    /// X coordinate.
83    pub x: f64,
84    /// Y coordinate.
85    pub y: f64,
86    /// Z coordinate.
87    pub z: f64,
88}
89
90impl Point3D {
91    /// Creates a new `Point3D`.
92    #[must_use]
93    #[inline]
94    pub const fn new(x: f64, y: f64, z: f64) -> Self {
95        Self { x, y, z }
96    }
97
98    /// Computes the Euclidean distance to another point.
99    #[must_use]
100    #[inline]
101    pub fn distance_to(&self, other: &Point3D) -> f64 {
102        let dx = self.x - other.x;
103        let dy = self.y - other.y;
104        let dz = self.z - other.z;
105        libm_sqrt(dx * dx + dy * dy + dz * dz)
106    }
107
108    /// Projects to a 2D point by dropping the Z component.
109    #[must_use]
110    #[inline]
111    pub const fn to_2d(&self) -> Point2D {
112        Point2D::new(self.x, self.y)
113    }
114}
115
116// ── BBox2D ────────────────────────────────────────────────────────────────────
117
118/// A 2-dimensional axis-aligned bounding box.
119#[derive(Debug, Clone, Copy, PartialEq)]
120pub struct BBox2D {
121    /// Minimum X.
122    pub min_x: f64,
123    /// Minimum Y.
124    pub min_y: f64,
125    /// Maximum X.
126    pub max_x: f64,
127    /// Maximum Y.
128    pub max_y: f64,
129}
130
131impl BBox2D {
132    /// Creates a new `BBox2D`.
133    #[must_use]
134    #[inline]
135    pub const fn new(min_x: f64, min_y: f64, max_x: f64, max_y: f64) -> Self {
136        Self {
137            min_x,
138            min_y,
139            max_x,
140            max_y,
141        }
142    }
143
144    /// Returns `true` if the bounding box is geometrically valid (min ≤ max).
145    #[must_use]
146    #[inline]
147    pub fn is_valid(&self) -> bool {
148        self.min_x <= self.max_x && self.min_y <= self.max_y
149    }
150
151    /// Returns `true` if the point lies inside (or on the boundary of) this box.
152    #[must_use]
153    #[inline]
154    pub fn contains_point(&self, p: Point2D) -> bool {
155        p.x >= self.min_x && p.x <= self.max_x && p.y >= self.min_y && p.y <= self.max_y
156    }
157
158    /// Returns `true` if this bounding box overlaps with another.
159    #[must_use]
160    #[inline]
161    pub fn intersects(&self, other: &BBox2D) -> bool {
162        self.min_x <= other.max_x
163            && self.max_x >= other.min_x
164            && self.min_y <= other.max_y
165            && self.max_y >= other.min_y
166    }
167
168    /// Computes the smallest bounding box containing both boxes.
169    #[must_use]
170    #[inline]
171    pub fn union(&self, other: &BBox2D) -> BBox2D {
172        BBox2D::new(
173            f64_min(self.min_x, other.min_x),
174            f64_min(self.min_y, other.min_y),
175            f64_max(self.max_x, other.max_x),
176            f64_max(self.max_y, other.max_y),
177        )
178    }
179
180    /// Computes the area of the bounding box.
181    #[must_use]
182    #[inline]
183    pub fn area(&self) -> f64 {
184        let w = self.max_x - self.min_x;
185        let h = self.max_y - self.min_y;
186        if w < 0.0 || h < 0.0 { 0.0 } else { w * h }
187    }
188}
189
190// ── LineSegment2D ─────────────────────────────────────────────────────────────
191
192/// A directed 2D line segment from `start` to `end`.
193#[derive(Debug, Clone, Copy, PartialEq)]
194pub struct LineSegment2D {
195    /// Start point.
196    pub start: Point2D,
197    /// End point.
198    pub end: Point2D,
199}
200
201impl LineSegment2D {
202    /// Creates a new `LineSegment2D`.
203    #[must_use]
204    #[inline]
205    pub const fn new(start: Point2D, end: Point2D) -> Self {
206        Self { start, end }
207    }
208
209    /// Returns the length of the segment.
210    #[must_use]
211    #[inline]
212    pub fn length(&self) -> f64 {
213        self.start.distance_to(&self.end)
214    }
215
216    /// Returns the midpoint of the segment.
217    #[must_use]
218    #[inline]
219    pub fn midpoint(&self) -> Point2D {
220        self.start.midpoint(&self.end)
221    }
222
223    /// Returns the point at parameter `t` along the segment.
224    ///
225    /// `t = 0.0` returns `start`, `t = 1.0` returns `end`.
226    #[must_use]
227    #[inline]
228    pub fn point_on_segment(&self, t: f64) -> Point2D {
229        Point2D::new(
230            self.start.x + t * (self.end.x - self.start.x),
231            self.start.y + t * (self.end.y - self.start.y),
232        )
233    }
234
235    /// Computes the intersection point of two line segments, if it exists.
236    ///
237    /// Uses parametric form. Returns `None` for parallel/coincident segments.
238    #[must_use]
239    pub fn intersects(&self, other: &LineSegment2D) -> Option<Point2D> {
240        let dx1 = self.end.x - self.start.x;
241        let dy1 = self.end.y - self.start.y;
242        let dx2 = other.end.x - other.start.x;
243        let dy2 = other.end.y - other.start.y;
244
245        let denom = dx1 * dy2 - dy1 * dx2;
246
247        if denom.abs() < f64::EPSILON * 1e6 {
248            return None; // parallel or coincident
249        }
250
251        let ox = other.start.x - self.start.x;
252        let oy = other.start.y - self.start.y;
253
254        let t = (ox * dy2 - oy * dx2) / denom;
255        let u = (ox * dy1 - oy * dx1) / denom;
256
257        if (0.0..=1.0).contains(&t) && (0.0..=1.0).contains(&u) {
258            Some(self.point_on_segment(t))
259        } else {
260            None
261        }
262    }
263}
264
265// ── Triangle2D ────────────────────────────────────────────────────────────────
266
267/// A triangle defined by three 2D vertices.
268#[derive(Debug, Clone, Copy, PartialEq)]
269pub struct Triangle2D {
270    /// First vertex.
271    pub a: Point2D,
272    /// Second vertex.
273    pub b: Point2D,
274    /// Third vertex.
275    pub c: Point2D,
276}
277
278impl Triangle2D {
279    /// Creates a new `Triangle2D`.
280    #[must_use]
281    #[inline]
282    pub const fn new(a: Point2D, b: Point2D, c: Point2D) -> Self {
283        Self { a, b, c }
284    }
285
286    /// Computes the signed area using the shoelace formula.
287    ///
288    /// Positive for counter-clockwise, negative for clockwise.
289    #[must_use]
290    #[inline]
291    pub fn signed_area(&self) -> f64 {
292        0.5 * ((self.b.x - self.a.x) * (self.c.y - self.a.y)
293            - (self.c.x - self.a.x) * (self.b.y - self.a.y))
294    }
295
296    /// Computes the absolute area.
297    #[must_use]
298    #[inline]
299    pub fn area(&self) -> f64 {
300        self.signed_area().abs()
301    }
302
303    /// Returns `true` if the vertices are arranged clockwise.
304    #[must_use]
305    #[inline]
306    pub fn is_clockwise(&self) -> bool {
307        self.signed_area() < 0.0
308    }
309
310    /// Computes the centroid (arithmetic mean of the three vertices).
311    #[must_use]
312    #[inline]
313    pub fn centroid(&self) -> Point2D {
314        Point2D::new(
315            (self.a.x + self.b.x + self.c.x) / 3.0,
316            (self.a.y + self.b.y + self.c.y) / 3.0,
317        )
318    }
319
320    /// Returns the perimeter (sum of side lengths).
321    #[must_use]
322    pub fn perimeter(&self) -> f64 {
323        self.a.distance_to(&self.b) + self.b.distance_to(&self.c) + self.c.distance_to(&self.a)
324    }
325
326    /// Tests whether a point is inside or on the boundary of the triangle.
327    ///
328    /// Uses barycentric coordinates.
329    #[must_use]
330    pub fn contains_point(&self, p: Point2D) -> bool {
331        // Barycentric coordinates via signed areas
332        let s_abc = self.signed_area();
333        if s_abc.abs() < f64::EPSILON {
334            return false; // degenerate triangle
335        }
336
337        let s_pbc = Triangle2D::new(p, self.b, self.c).signed_area();
338        let s_apc = Triangle2D::new(self.a, p, self.c).signed_area();
339        let s_abp = Triangle2D::new(self.a, self.b, p).signed_area();
340
341        let same_sign = |a: f64, b: f64| (a >= 0.0) == (b >= 0.0);
342
343        same_sign(s_abc, s_pbc) && same_sign(s_abc, s_apc) && same_sign(s_abc, s_abp)
344    }
345}
346
347// ── FixedPolygon ──────────────────────────────────────────────────────────────
348
349/// A polygon with a statically-allocated vertex array of capacity `N`.
350///
351/// Vertices are stored inline; no heap allocation is performed.
352pub struct FixedPolygon<const N: usize> {
353    vertices: [Point2D; N],
354    len: usize,
355}
356
357impl<const N: usize> FixedPolygon<N> {
358    /// Creates an empty `FixedPolygon`.
359    #[must_use]
360    #[inline]
361    pub fn new() -> Self {
362        Self {
363            vertices: [Point2D::new(0.0, 0.0); N],
364            len: 0,
365        }
366    }
367
368    /// Attempts to push a vertex.  Returns `false` if the polygon is full.
369    #[inline]
370    pub fn try_push(&mut self, p: Point2D) -> bool {
371        if self.len >= N {
372            return false;
373        }
374        self.vertices[self.len] = p;
375        self.len += 1;
376        true
377    }
378
379    /// Returns the number of vertices.
380    #[must_use]
381    #[inline]
382    pub fn len(&self) -> usize {
383        self.len
384    }
385
386    /// Returns `true` if the polygon has no vertices.
387    #[must_use]
388    #[inline]
389    pub fn is_empty(&self) -> bool {
390        self.len == 0
391    }
392
393    /// Returns a slice of the current vertices.
394    #[must_use]
395    #[inline]
396    pub fn vertices(&self) -> &[Point2D] {
397        &self.vertices[..self.len]
398    }
399
400    /// Computes the signed shoelace area (positive for CCW).
401    #[must_use]
402    pub fn signed_area(&self) -> f64 {
403        if self.len < 3 {
404            return 0.0;
405        }
406        let mut sum = 0.0_f64;
407        let verts = self.vertices();
408        for i in 0..self.len {
409            let j = (i + 1) % self.len;
410            sum += verts[i].x * verts[j].y;
411            sum -= verts[j].x * verts[i].y;
412        }
413        sum * 0.5
414    }
415
416    /// Computes the absolute area using the shoelace formula.
417    #[must_use]
418    #[inline]
419    pub fn area(&self) -> f64 {
420        self.signed_area().abs()
421    }
422
423    /// Computes the perimeter (sum of edge lengths).
424    #[must_use]
425    pub fn perimeter(&self) -> f64 {
426        if self.len < 2 {
427            return 0.0;
428        }
429        let verts = self.vertices();
430        let mut total = 0.0_f64;
431        for i in 0..self.len {
432            let j = (i + 1) % self.len;
433            total += verts[i].distance_to(&verts[j]);
434        }
435        total
436    }
437
438    /// Returns the axis-aligned bounding box, or `None` if the polygon is empty.
439    #[must_use]
440    pub fn bbox(&self) -> Option<BBox2D> {
441        if self.is_empty() {
442            return None;
443        }
444        let verts = self.vertices();
445        let mut min_x = verts[0].x;
446        let mut min_y = verts[0].y;
447        let mut max_x = verts[0].x;
448        let mut max_y = verts[0].y;
449        for v in verts.iter().skip(1) {
450            min_x = f64_min(min_x, v.x);
451            min_y = f64_min(min_y, v.y);
452            max_x = f64_max(max_x, v.x);
453            max_y = f64_max(max_y, v.y);
454        }
455        Some(BBox2D::new(min_x, min_y, max_x, max_y))
456    }
457
458    /// Computes the centroid, or `None` if the polygon is empty.
459    #[must_use]
460    pub fn centroid(&self) -> Option<Point2D> {
461        if self.is_empty() {
462            return None;
463        }
464        let verts = self.vertices();
465        let mut cx = 0.0_f64;
466        let mut cy = 0.0_f64;
467        for v in verts {
468            cx += v.x;
469            cy += v.y;
470        }
471        let n = self.len as f64;
472        Some(Point2D::new(cx / n, cy / n))
473    }
474}
475
476impl<const N: usize> Default for FixedPolygon<N> {
477    fn default() -> Self {
478        Self::new()
479    }
480}
481
482// ── CoordTransform ────────────────────────────────────────────────────────────
483
484/// A 2D affine transformation stored as a 2×3 matrix.
485///
486/// The transformation is applied as:
487/// ```text
488/// x' = a*x + b*y + c
489/// y' = d*x + e*y + f
490/// ```
491/// where `matrix = [a, b, c, d, e, f]`.
492#[derive(Debug, Clone, Copy, PartialEq)]
493pub struct CoordTransform {
494    matrix: [f64; 6],
495}
496
497impl CoordTransform {
498    /// Creates the identity transform.
499    #[must_use]
500    #[inline]
501    pub const fn identity() -> Self {
502        Self {
503            matrix: [1.0, 0.0, 0.0, 0.0, 1.0, 0.0],
504        }
505    }
506
507    /// Creates a uniform scale transform.
508    #[must_use]
509    #[inline]
510    pub const fn scale(sx: f64, sy: f64) -> Self {
511        Self {
512            matrix: [sx, 0.0, 0.0, 0.0, sy, 0.0],
513        }
514    }
515
516    /// Creates a translation transform.
517    #[must_use]
518    #[inline]
519    pub const fn translate(tx: f64, ty: f64) -> Self {
520        Self {
521            matrix: [1.0, 0.0, tx, 0.0, 1.0, ty],
522        }
523    }
524
525    /// Creates a counter-clockwise rotation transform.
526    #[must_use]
527    pub fn rotate(angle_radians: f64) -> Self {
528        let cos_a = libm_cos(angle_radians);
529        let sin_a = libm_sin(angle_radians);
530        Self {
531            matrix: [cos_a, -sin_a, 0.0, sin_a, cos_a, 0.0],
532        }
533    }
534
535    /// Composes `self` followed by `other` into a single transform.
536    ///
537    /// The result applies `self` first, then `other`.
538    /// Mathematically this is `other_matrix × self_matrix` because affine
539    /// transforms are applied right-to-left in column-vector notation.
540    #[must_use]
541    pub fn compose(self, other: &CoordTransform) -> Self {
542        // self  = T1: applied first
543        // other = T2: applied second
544        // Combined = T2 × T1
545        //
546        // [a2 b2 c2]   [a1 b1 c1]
547        // [d2 e2 f2] × [d1 e1 f1]
548        // [0  0  1 ]   [0  0  1 ]
549        let [a1, b1, c1, d1, e1, f1] = self.matrix;
550        let [a2, b2, c2, d2, e2, f2] = other.matrix;
551        Self {
552            matrix: [
553                a2 * a1 + b2 * d1,
554                a2 * b1 + b2 * e1,
555                a2 * c1 + b2 * f1 + c2,
556                d2 * a1 + e2 * d1,
557                d2 * b1 + e2 * e1,
558                d2 * c1 + e2 * f1 + f2,
559            ],
560        }
561    }
562
563    /// Applies the transform to a 2D point.
564    #[must_use]
565    #[inline]
566    pub fn apply(&self, p: Point2D) -> Point2D {
567        let [a, b, c, d, e, f] = self.matrix;
568        Point2D::new(a * p.x + b * p.y + c, d * p.x + e * p.y + f)
569    }
570
571    /// Applies the 2D transform to a 3D point, passing the Z coordinate through unchanged.
572    #[must_use]
573    #[inline]
574    pub fn apply3d(&self, p: Point3D) -> Point3D {
575        let p2 = self.apply(p.to_2d());
576        Point3D::new(p2.x, p2.y, p.z)
577    }
578}
579
580// ── Internal float helpers ────────────────────────────────────────────────────
581// In no_std, f64 transcendental functions are not available from core.
582// We implement them using a simple call to our own no_std libm wrappers.
583//
584// The workspace has `libm = "0.2"` available as a workspace dep.
585// However, this crate intentionally avoids even alloc, so we inline
586// simple software implementations for sqrt, sin, cos using compiler intrinsics
587// which ARE available on supported targets (aarch64, x86_64, riscv).
588//
589// On hosted no_std targets the compiler lowers f64::sqrt() to hardware if
590// the -C target-feature includes the FPU.  On soft-float targets one must
591// link a libm; that is the deployer's responsibility (standard practice).
592//
593// We use the `unsafe` intrinsic approach through a shim that compiles cleanly.
594
595/// Portable f64 sqrt using Newton-Raphson iteration.
596///
597/// Initial guess: halve the IEEE 754 biased exponent to get an approximation
598/// of 2^(e/2), which is a good starting point for sqrt.
599/// Five iterations are sufficient for full f64 precision.
600#[inline(always)]
601fn libm_sqrt(x: f64) -> f64 {
602    if x <= 0.0 {
603        return if x == 0.0 { 0.0 } else { f64::NAN };
604    }
605    // Initial guess via IEEE-754 exponent bisection.
606    // For x = m * 2^e, sqrt(x) ≈ sqrt(m) * 2^(e/2).
607    // We approximate sqrt(m) ≈ 1.0 and halve the exponent.
608    // bits of f64: [sign(1)] [exponent(11)] [mantissa(52)]
609    // bias = 1023; to halve: new_exp = (old_exp + 1023) / 2 → add 1023, halve, subtract 1023
610    let bits = x.to_bits();
611    // Extract biased exponent (bits 62..52), halve it (rounding toward even), reconstruct
612    let biased_exp = (bits >> 52) & 0x7FF;
613    let new_biased_exp = (biased_exp + 1023) >> 1;
614    // Reconstruct with zero mantissa (approximation)
615    let r_bits = new_biased_exp << 52;
616    let mut r = f64::from_bits(r_bits);
617    // Newton-Raphson: r = (r + x/r) / 2, 6 iterations → ~18 correct digits
618    r = (r + x / r) * 0.5;
619    r = (r + x / r) * 0.5;
620    r = (r + x / r) * 0.5;
621    r = (r + x / r) * 0.5;
622    r = (r + x / r) * 0.5;
623    r = (r + x / r) * 0.5;
624    r
625}
626
627/// Portable f64 sin using Taylor series with argument reduction to [-π/4, π/4].
628///
629/// Uses quadrant-based reduction for maximum accuracy across the full range.
630#[inline(always)]
631fn libm_sin(x: f64) -> f64 {
632    let (s, c) = sin_cos_core(x);
633    let _ = c;
634    s
635}
636
637/// Portable f64 cos using Taylor series with argument reduction to [-π/4, π/4].
638#[inline(always)]
639fn libm_cos(x: f64) -> f64 {
640    let (s, c) = sin_cos_core(x);
641    let _ = s;
642    c
643}
644
645/// Core sin/cos computation via quadrant reduction + Horner-form Taylor series.
646///
647/// Reduces `x` to `[-π/4, π/4]` using `k = round(x / (π/2))` and then
648/// routes between sin-series and cos-series based on the quadrant parity.
649#[inline(always)]
650fn sin_cos_core(x: f64) -> (f64, f64) {
651    let pi = core::f64::consts::PI;
652    let two_pi = 2.0 * pi;
653    let half_pi = pi * 0.5;
654    let quarter_pi = pi * 0.25;
655
656    // Reduce to [-pi, pi]
657    let mut x = x % two_pi;
658    if x > pi {
659        x -= two_pi;
660    }
661    if x < -pi {
662        x += two_pi;
663    }
664
665    // Determine quadrant: k in {0, 1, 2, 3}
666    // round(x / (π/2)) gives the nearest multiple of π/2
667    let ratio = x / half_pi;
668    // Manual round: add 0.5 with sign matching, then truncate
669    let k_f = if ratio >= 0.0 {
670        (ratio + 0.5) as i64 as f64
671    } else {
672        (ratio - 0.5) as i64 as f64
673    };
674    let k = k_f as i64;
675    let r = x - k_f * half_pi; // |r| ≤ π/4
676
677    // Ignore unused warning suppression: quarter_pi is used conceptually
678    let _ = quarter_pi;
679
680    // Taylor series for sin(r) and cos(r) — both accurate on [-π/4, π/4]
681    let r2 = r * r;
682    // sin(r) = r * (1 - r²/6 + r⁴/120 - r⁶/5040 + r⁸/362880 - r¹⁰/39916800 + r¹²/6227020800)
683    let sin_r = r
684        * (1.0
685            + r2 * (-1.0 / 6.0
686                + r2 * (1.0 / 120.0
687                    + r2 * (-1.0 / 5040.0
688                        + r2 * (1.0 / 362_880.0
689                            + r2 * (-1.0 / 39_916_800.0 + r2 * (1.0 / 6_227_020_800.0)))))));
690    // cos(r) = 1 - r²/2 + r⁴/24 - r⁶/720 + r⁸/40320 - r¹⁰/3628800 + r¹²/479001600
691    let cos_r = 1.0
692        + r2 * (-1.0 / 2.0
693            + r2 * (1.0 / 24.0
694                + r2 * (-1.0 / 720.0
695                    + r2 * (1.0 / 40_320.0
696                        + r2 * (-1.0 / 3_628_800.0 + r2 * (1.0 / 479_001_600.0))))));
697
698    // Reconstruct sin(x) and cos(x) from quadrant and reduced values
699    // k mod 4 determines which quadrant:
700    //   0: sin(x) =  sin(r),  cos(x) =  cos(r)
701    //   1: sin(x) =  cos(r),  cos(x) = -sin(r)
702    //   2: sin(x) = -sin(r),  cos(x) = -cos(r)
703    //   3: sin(x) = -cos(r),  cos(x) =  sin(r)
704    let kmod = ((k % 4) + 4) as u64 % 4;
705    match kmod {
706        0 => (sin_r, cos_r),
707        1 => (cos_r, -sin_r),
708        2 => (-sin_r, -cos_r),
709        _ => (-cos_r, sin_r),
710    }
711}
712
713/// f64 min without std.
714#[inline(always)]
715fn f64_min(a: f64, b: f64) -> f64 {
716    if a < b { a } else { b }
717}
718
719/// f64 max without std.
720#[inline(always)]
721fn f64_max(a: f64, b: f64) -> f64 {
722    if a > b { a } else { b }
723}