import { BivectorN } from './bivector.js'; import { MatN, type PlaneRotation } from './matn.js'; import { VecN } from './vecn.js'; /** * A 4D rotation as a pair of unit quaternions — the fast path for SO(4). * * SO(4) is (up to double cover) S³ × S³: identify a point * p = (x₀, x₁, x₂, x₃) with the quaternion P = x₃ + x₀i + x₁j + x₂k, and * every 4D rotation is p ↦ q_L · P · q_R for unit quaternions q_L, q_R. * Applying a rotor is two quaternion products (~2× cheaper than a dense * 4×4 multiply), composition never drifts off the rotation manifold the * way accumulated matrix products do, and renormalization is two vector * normalizations instead of Gram–Schmidt. * * The bivector → (q_L, q_R) split is the so(4) ≅ so(3) ⊕ so(3) * decomposition; the sign conventions are pinned by tests against * `expBivector` and `MatN.rotationInPlane`. * * Quaternions are stored as Float64Array [i, j, k, real] so that a point's * coordinate layout (x₀, x₁, x₂, x₃) is itself a valid quaternion. */ export declare class Rotor4 { readonly left: Float64Array; readonly right: Float64Array; private constructor(); static identity(): Rotor4; /** * Factors an SO(4) matrix into the paired-quaternion cover used by Rotor4. * The factorization is convention-derived: the sixteen maps * `p ↦ e_i p e_j` form an orthogonal matrix basis, so their coefficients * form the rank-one outer product `q_L q_Rᵀ`. */ static fromMatrix(matrix: MatN, tolerance?: number): Rotor4; /** Exponential of a 4D bivector, split into left/right isoclinic parts. */ static fromBivector(b: BivectorN): Rotor4; /** Rotation of axis i toward axis j by `angle`, as a rotor. */ static fromPlane(i: number, j: number, angle: number): Rotor4; static fromPlanes(planes: readonly PlaneRotation[]): Rotor4; /** * Haar-uniform random rotation on SO(4). * * Each paired factor is sampled independently and uniformly on S³, then * the pair descends through Spin(4)'s two-to-one cover. This is not the same * as exponentiating six independently uniform bivector coefficients, which * clusters rotations around coordinate-dependent regions. * * @param rng - Source of independent values in `[0, 1)`. Defaults to * `Math.random`; pass a seeded generator for reproducible scenes or tests. */ static random(rng?: () => number): Rotor4; /** * The two invariant-plane angles of the shortest relative SO(4) rotation. * * The result is `[major, minor]`, both in `[0, π]`. A single-plane rotation * by `theta` reports `[|theta|, 0]`; an isoclinic rotation reports equal * angles; central inversion reports `[π, π]`. The simultaneous cover sign * is selected once for the pair, never independently per quaternion. * Unlike `log()`, this spectrum remains unique at the geodesic cut locus. */ static principalAnglesBetween(a: Rotor4, b: Rotor4): readonly [number, number]; /** * Bi-invariant SO(4) geodesic distance between two rotations. * * This is the Euclidean norm of `principalAnglesBetween(a, b)`, normalized * so distance from identity to a single-plane rotation by `theta` is * `|theta|`. It is finite at the cut locus even when `log()` is non-unique. */ static geodesicDistanceBetween(a: Rotor4, b: Rotor4): number; clone(): Rotor4; /** Returns `this ∘ r` (r applied first). */ multiply(r: Rotor4): Rotor4; /** Rescales both quaternions to unit length (drift repair). */ normalize(): this; /** The inverse rotation (conjugates of both unit quaternions). */ conjugate(): Rotor4; /** * Principal paired-quaternion logarithm in the kernel's bivector basis. * * The double-cover sign is chosen once for the pair, matching `slerp`. * A relative central inversion has no unique logarithm and is rejected; * coherent animation/kinematic samples should subdivide before that point. */ log(): BivectorN; /** * Isoclinic interpolation: SLERP applied to the left and right * quaternions with one shared cover choice. Because a geodesic of SO(4) * (in the bi-invariant metric) is exactly a pair of quaternion geodesics, * this is the geodesic from `a` to `b`: slerp(I, exp(B), t) = exp(t·B) * for every bivector B. The 4D generalization of quaternion slerp for * animation and camera tours. * * The double cover is pair-level: (l, r) and (−l, −r) are the same SO(4) * element, but flipping only one factor negates the rotation — so the * shorter-path sign must be chosen once for the pair, never per * quaternion. One factor may therefore legitimately travel an arc * longer than π. */ static slerp(a: Rotor4, b: Rotor4, t: number): Rotor4; applyToPoint(v: VecN, out?: VecN): VecN; /** Applies the rotor to `count` packed 4-vectors (src and dst may alias). */ applyToPositions(src: Float64Array, dst: Float64Array, count: number): void; /** Dense matrix form (columns are rotated basis vectors). */ toMatrix(): MatN; } //# sourceMappingURL=rotor4.d.ts.map