import { VecN } from '@holotope/core'; /** * Orientation-neutral fold coordinate for two adjacent source simplices. * * Two `d`-simplices sharing a `(d-1)`-face, embedded in RN with `1 <= d < N`. * With `Q` an orthonormal basis for the shared face's edge span and * `P = I - Q Qᵀ`: * * ```text * rA = P (a - f0), rB = P (b - f0), uA = rA/‖rA‖, uB = rB/‖rB‖ * c = -uA · uB * ``` * * `c` is the cosine of the fold measured from flat: `1` flat, `-1` fully * folded. For an R3 triangle hinge it equals `-cos(φ)` for the conventional * interior dihedral `φ`, verified to 2.22e-16 in the P48 measurement. * * This is deliberately **unsigned**. It cannot distinguish a mountain fold * from a valley fold, and must never be presented as a signed dihedral angle: * the sign convention that would give it one is specific to R3. * * Avoiding `acos` is not a micro-optimization. The derivative of `acos` is * singular at both endpoints, exactly where a flat or fully folded hinge sits, * so the cosine is the coordinate that stays differentiable where meshes * actually live. */ export interface SimplexHingeCosineNOptions { /** Shared-face positions in the caller's source order; `d` of them. */ readonly sharedFace: readonly VecN[]; /** Vertex of the first incident simplex opposite the shared face. */ readonly oppositeA: VecN; /** Vertex of the second incident simplex opposite the shared face. */ readonly oppositeB: VecN; /** * Relative tolerance for shared-face rank and conormal height. Default * `1e-10`. Scaled by the local geometry, never absolute. */ readonly tolerance?: number; } /** Why a hinge has no defined fold coordinate. */ export type SimplexHingeCosineRefusalReasonN = 'rank-deficient-shared-face' | 'vanishing-conormal-height'; /** Evidence available before a refusal, so the caller can see how close it was. */ export interface SimplexHingeCosineRefusalN { /** Discriminant; no coordinate or gradient exists on this branch. */ readonly status: 'refused'; /** Which degeneracy was found. */ readonly reason: SimplexHingeCosineRefusalReasonN; /** Ambient dimension `N` the hinge was evaluated in. */ readonly ambientDimension: number; /** Intrinsic simplex dimension `d`, the shared-face vertex count. */ readonly simplexDimension: number; /** Independent shared-face edge directions actually found. */ readonly rank: number; /** Rank the shared face must have for the coordinate to exist. */ readonly requiredRank: number; /** Smallest Gram-Schmidt residual relative to the largest edge. */ readonly conditioning: number; /** First apex height, or `null` when rank failed before it was reachable. */ readonly heightA: number | null; /** Second apex height, or `null` for the same reason. */ readonly heightB: number | null; /** Local length scale the relative tolerance was applied against. */ readonly scale: number; /** The relative tolerance in force; the threshold is `tolerance * scale`. */ readonly tolerance: number; } /** A defined fold coordinate with its complete first-derivative evidence. */ export interface SimplexHingeCosineEvaluationN { /** Discriminant; the coordinate and gradient are present on this branch. */ readonly status: 'evaluated'; /** Ambient dimension `N` the hinge was evaluated in. */ readonly ambientDimension: number; /** Intrinsic simplex dimension `d`, the shared-face vertex count. */ readonly simplexDimension: number; /** `-uA · uB`, in `[-1, 1]`; `1` is flat. */ readonly coordinate: number; /** Independent shared-face edge directions found; equals `d - 1` when valid. */ readonly rank: number; /** Smallest Gram-Schmidt residual over the largest edge; small is ill-posed. */ readonly conditioning: number; /** Distance from `oppositeA` to the shared face's affine hull. */ readonly heightA: number; /** Distance from `oppositeB` to the same affine hull. */ readonly heightB: number; /** Unit conormal at `oppositeA`, orthogonal to the shared-face span. */ readonly conormalA: VecN; /** Unit conormal at `oppositeB`; `c` is the negated dot of the two. */ readonly conormalB: VecN; /** * `∂c/∂vertex`, one per hinge vertex, in the caller's input order * `[...sharedFace, oppositeA, oppositeB]`. * * Translation is a null mode by construction: the `f_0` slot is written as * the exact algebraic complement of the others, so the entries cancel * symbolically rather than by cancelling error. * * That is a statement about the algebra, not about the returned Float64 * values. The complement is one rounded expression and the slots it cancels * are another, so summing them lands at roundoff — order-dependently, since * no summation order is privileged. Compare against a tolerance near * `1e-12`, never against zero. */ readonly gradient: readonly VecN[]; } /** Either a defined fold coordinate or a typed geometric refusal. */ export type SimplexHingeCosineResultN = SimplexHingeCosineEvaluationN | SimplexHingeCosineRefusalN; /** * Evaluates the fold coordinate and its exact gradient for one hinge. * * Pure: it reads the supplied positions and writes nothing. Gradient slots * follow the caller's input order, so a family that permutes a shared face * gets its own ordering back rather than a canonical one. * * The gradient is closed-form, not finite-differenced. Because both * sensitivity vectors are orthogonal to the shared-face span, the projector's * own derivative cancels out of `w · δr`, leaving four short terms: * * ```text * wA = (I - uA uAᵀ) uB / ‖rA‖ wB = (I - uB uBᵀ) uA / ‖rB‖ * ∂c/∂a = -wA ∂c/∂b = -wB * ∂c/∂f_j = αA[j-1] wA + αB[j-1] wB (j >= 1) * ∂c/∂f_0 = -[(Σ αA - 1) wA + (Σ αB - 1) wB] * ``` * * The `f_0` slot is the algebraic complement of the rest, so the slots cancel * symbolically. In Float64 they cancel to roundoff instead, because the * complement and the terms it cancels are separately rounded expressions. * * Verified against central differences to 1.16e-10 over 320 coordinates * sampling `(N, d)` pairs from R2 to R7 in the P48 measurement, whose net-force * residual was 5.55e-17 and whose rotational first-moment residual came out at * 0.00e+00. Those are that fixture's numbers, not guarantees — a bitwise zero * there does not generalize, and other geometry puts both at roundoff scale. * * @param options - Shared-face positions in source order, the two opposite * positions, and an optional positive relative tolerance. * @returns A frozen discriminated union: `'evaluated'` with the coordinate, * conditioning, heights, conormals, and per-vertex gradient, or `'refused'` * with the reason and the evidence available before refusal. * @throws If the options are malformed — unknown keys, wrong dimensions, * non-finite coordinates, too few shared-face vertices, or `d >= N`. A * geometric degeneracy is a refusal, not a throw. * * @example * One R3 triangle hinge folded 60° from flat: * ```ts * const half = Math.PI / 3; * const result = evaluateSimplexHingeCosineN({ * sharedFace: [new VecN([0, 0, 0]), new VecN([0, 1, 0])], * oppositeA: new VecN([-1, 0, 0]), * oppositeB: new VecN([Math.cos(half), 0, Math.sin(half)]) * }); * * if (result.status === 'refused') throw new Error(result.reason); * log(result.coordinate); // cos(60°) = 0.5000000000000001 * log(result.gradient.length); // 4 — shared face, then both apexes * ``` */ export declare function evaluateSimplexHingeCosineN(options: SimplexHingeCosineNOptions): SimplexHingeCosineResultN; //# sourceMappingURL=simplex-hinge-cosine.d.ts.map