import { VecN } from '@holotope/core'; import { type SupportFeatureId, type SupportShapeN } from './support-shape.js'; export type GjkSign = -1 | 0 | 1; /** One support point of the Minkowski difference A-B, with witnesses. */ export interface GjkSimplexVertexN { readonly point: VecN; readonly pointA: VecN; readonly pointB: VecN; readonly featureA: SupportFeatureId; readonly featureB: SupportFeatureId; } /** * Optional exact classifier for the barycentric signs of the origin's * projection onto one affine sub-simplex. Numerical weights are still used * for returned witness coordinates; these signs decide which face is active. */ export interface GjkBarycentricSignResult { readonly affineIndependent: boolean; readonly weightSigns: readonly GjkSign[]; /** Human-readable predicate family, e.g. `exact-ring:phi`. */ readonly source: string; } export type GjkBarycentricSignOracle = (simplex: readonly GjkSimplexVertexN[], subset: readonly number[]) => GjkBarycentricSignResult | undefined; export interface GjkFeaturePair { readonly featureA: SupportFeatureId; readonly featureB: SupportFeatureId; } /** Reusable, pose-independent seed for temporally coherent shape pairs. */ export interface GjkWarmStartN { readonly dim: number; readonly direction: VecN; readonly featurePairs: readonly GjkFeaturePair[]; } export interface GjkOptions { /** Default 32. */ maxIterations?: number; /** van den Bergen relative progress tolerance. Default 1e-12. */ relativeTolerance?: number; /** Distance/contact tolerance in world units. Default 1e-12. */ absoluteTolerance?: number; /** Floating barycentric zero band. Default 1e-12. */ barycentricTolerance?: number; /** Warm-start direction; the previous result normal is a good choice. */ initialDirection?: VecN | ArrayLike; /** Previous result seed. Explicit `initialDirection` takes axis precedence. */ warmStart?: GjkWarmStartN; /** Exact or higher-precision branch classifier for provenance-aware shapes. */ signOracle?: GjkBarycentricSignOracle; /** Retain one compact diagnostic record per support iteration. */ recordTrace?: boolean; } export type GjkTerminationReason = 'origin-within-tolerance' | 'relative-progress' /** * A proved support/projection fixpoint: the support map repeated a point it * had already returned, the accumulated support set was reprojected with * certificate-aware selection, and the state still did not change. The loop * is a deterministic function of that state, so further iterations provably * reproduce it — the query refuses immediately instead of burning the * remaining budget on an identical cycle. * * This reason accompanies status `'iteration-limit'` and is a **refusal**, * never a separation claim: it means the sampled support set cannot supply * the support-gap certificate at the required tolerance. */ | 'duplicate-support' | 'iteration-limit'; export interface GjkSimplexCertificate { /** Convex weights aligned with `result.simplex`. */ readonly weights: Float64Array; /** Predicate signs before numerical clamping. */ readonly weightSigns: Int8Array; readonly predicateSource: string; /** Pivot-ratio proxy in [0,1]; larger is better conditioned. */ readonly conditionEstimate: number; /** Determinant of the numerical affine Gram system. */ readonly determinant: number; /** Origin strictly inside a full-dimensional simplex, when provable. */ readonly strictInterior: boolean; } export interface GjkTerminationCertificate { readonly reason: GjkTerminationReason; readonly supportGap: number; readonly threshold: number; readonly exactPredicateCalls: number; readonly maxSimplexSize: number; /** Number of cached feature pairs successfully rehydrated at this pose. */ readonly warmStartSize: number; } export interface GjkTraceEntry { readonly iteration: number; readonly simplexSize: number; readonly distanceSquared: number; readonly supportGap: number; readonly predicateSource: string; } export interface GjkResult { /** * `'separated'` and `'intersecting'` are certified results; see the * function docs for the certificates behind each. `'iteration-limit'` is an * explicit refusal — no proof was obtained within the compute budget, where * the budget is either the iteration cap (`termination.reason === * 'iteration-limit'`) or a proved fixpoint that further iterations cannot * escape (`termination.reason === 'duplicate-support'`). */ readonly status: 'separated' | 'intersecting' | 'iteration-limit'; /** `null` only when the query refuses to certify either answer. */ readonly intersects: boolean | null; /** Numerical distance at convergence, or the best current upper bound at limit. */ readonly distance: number; /** Unit vector from B toward A for separated shapes; null at zero distance. */ readonly normal: VecN | null; readonly closestPointA: VecN; readonly closestPointB: VecN; readonly iterations: number; readonly simplex: readonly GjkSimplexVertexN[]; readonly simplexCertificate: GjkSimplexCertificate; readonly termination: GjkTerminationCertificate; /** Feed this into the next coherent query for the same ordered shape pair. */ readonly warmStart: GjkWarmStartN; readonly trace?: readonly GjkTraceEntry[]; } /** * Distance/intersection query for two compact convex support shapes in ℝⁿ. * The implementation is dimension-generic; in R4 the active simplex has at * most five vertices. EPA/contact response are intentionally separate APIs. * * ## A stable estimate is not a certificate * * The query distinguishes three outcomes, and the distinction is the point: * * - `separated` is returned **only** with a support-gap certificate — the * support point `w` in direction `-q` satisfies * `|q|² − q·w ≤ threshold`, which is the projection optimality condition * evaluated against the shape itself. The reported distance is then a * certified upper bound witnessed by `closestPointA`/`closestPointB`; * - `intersecting` carries the existing origin-enclosure proof; * - `iteration-limit` is a **refusal**: the query could not obtain either * proof. `intersects` is `null`, and the numerically stable `distance` that * accompanies it is the best current estimate, not a claim. A distance that * has stopped changing between iterations is *never* treated as separation. * * Equal and nearly tied support directions terminate: when the support map * repeats a point it has already returned, the accumulated support set is * reprojected with certificate-aware candidate selection, which decides * configurations whose distance improvement per step is smaller than Float64 * comparison noise while their certificate improvement is not. A proved * fixpoint — the same repeated state with nothing new learned — refuses * immediately with `termination.reason === 'duplicate-support'` instead of * spending the remaining budget on an identical cycle. * * @example * A separation is a certificate, not a small number that stopped moving — * and a refusal says so plainly instead of guessing: * ```ts * const box = new ConvexHullSupportShapeN(4, Float64Array.from([ * 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, * 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0 * ])); * const probe = new ConvexHullSupportShapeN(4, Float64Array.from([ * 0.25, 0.5, 0.5, 2 * ])); * * const decided = gjkDistance(probe, box); * decided.status; // 'separated' * decided.distance; // 2 — the probe's height over the box * decided.termination.supportGap <= decided.termination.threshold; // true * * const refused = gjkDistance(probe, box, { maxIterations: 1 }); * refused.status; // 'iteration-limit' — a refusal, not a miss * refused.intersects; // null — no separation claim without the certificate * ``` */ export declare function gjkDistance(shapeA: SupportShapeN, shapeB: SupportShapeN, options?: GjkOptions): GjkResult; //# sourceMappingURL=gjk.d.ts.map