import { VecN } from '@holotope/core'; import type { SourceSimplexCoordinateN } from '@holotope/core'; import { XpbdSourceSimplexPairBarrierN } from './xpbd-source-simplex-pair-barrier.js'; import { XpbdParticleN, type XpbdConservativeForceProviderEvaluationN, type XpbdConservativeForceProviderN, type XpbdParticlePositionQueryN } from './xpbd-world.js'; /** Why a friction lag could not be prepared from a contact base state. */ export type XpbdSourceSimplexPairFrictionPrepareRefusalN = 'tied-witness-no-unique-gradient' | 'zero-or-intersecting' | 'uncertified-distance' | 'at-or-below-minimum-distance' | 'retired-source'; /** Where one prepared lag is in its single-use lifecycle. */ export type XpbdSourceSimplexPairFrictionLagStateN = 'prepared' | 'consumed'; /** Which side of the regularized Coulomb law a candidate slip falls on. */ export type XpbdSourceSimplexPairFrictionRegimeN = 'sticking' | 'transition' | 'sliding'; /** * The immutable evidence frozen at one accepted base state. * * Everything a friction force needs — the contact frame, the weights, and the * normal magnitude — is captured here **once**, which is exactly what makes * the potential conservative while this snapshot is held. Nothing in it is * recomputed per candidate, and nothing in it can be mutated: an Armijo trial * cannot move the lag underneath the objective it is minimizing. */ export interface XpbdSourceSimplexPairFrictionLagN { /** Ambient dimension. */ readonly dimension: number; /** Source-ordered closest-pair weights on the deforming side. */ readonly coordinateA: SourceSimplexCoordinateN; /** Source-ordered closest-pair weights on the opposing side. */ readonly coordinateB: SourceSimplexCoordinateN; /** Certified unit normal from the pair query, pointing B toward A. */ readonly normal: VecN; /** Accepted-base witness point on side A. */ readonly basePointA: VecN; /** Accepted-base witness point on side B. */ readonly basePointB: VecN; /** Certified pair distance at the accepted base. */ readonly baseDistance: number; /** Non-negative lagged normal-force magnitude, `-b'(baseDistance)`. */ readonly laggedNormalForce: number; /** * The resolved regularization length this lag was frozen with, in world * length units. * * Frozen here rather than read per evaluation because a length that moved * during the solve would stop the term being a potential: the Armijo search * would be minimizing a function whose own shape changed under it. Under an * authored length this is that length; under an authored slip velocity it is * `velocity * deltaTime`, resolved once at {@link XpbdSourceSimplexPairFrictionN.prepare}. */ readonly regularizationLength: number; /** The measured P56 uniqueness margin that justified this snapshot. */ readonly uniquenessGap: number; /** Single-use state; a consumed lag must be refreshed, never reused. */ readonly state: XpbdSourceSimplexPairFrictionLagStateN; } /** Options for {@link XpbdSourceSimplexPairFrictionN}. */ export interface XpbdSourceSimplexPairFrictionNOptions { /** Stable provider identity, distinct from every authored world provider. */ readonly id: string; /** The paired barrier supplying the contact, its features, and its particles. */ readonly barrier: XpbdSourceSimplexPairBarrierN; /** Isotropic Coulomb coefficient; `0` disables the term exactly. */ readonly frictionCoefficient: number; /** * The scale below which the Coulomb law is regularized. * * A bare number is a **world length**, exactly as it has always been: not a * velocity threshold and not scaled by the timestep. That spelling is * unchanged and is never reinterpreted. * * A fixed length does not survive timestep refinement. Per-step slip is * `‖tangential velocity‖ · deltaTime`, so once the slip falls inside the * regularized branch the force is `forceLimit · slip / length ∝ deltaTime`, * one step's impulse is `∝ deltaTime²`, and a fixed horizon of `T/deltaTime` * steps totals `∝ T · deltaTime`. Friction therefore **vanishes** as the * timestep shrinks. Measured over an eight-fold refinement, the tangential * impulse falls to 0.133 of its coarse value, with the last halving at a * ratio of 1.98 against the 2.00 that scaling predicts. * * `{ kind: 'slip-velocity', velocity }` resolves the length as * `velocity · deltaTime` once per {@link XpbdSourceSimplexPairFrictionN.prepare}, * which cancels the timestep out of `slip / length` exactly. Under the same * refinement the impulse holds to 1.06 of its coarse value, last halving * 0.99. Author it as the slip speed below which contact should be treated as * stuck. */ readonly slipRegularization: XpbdSourceSimplexPairSlipRegularizationN; } /** * How the regularized-Coulomb scale is authored. * * The bare number is the legacy spelling and means a world length. The two * discriminated forms are additive: the numeric form is never reinterpreted as * a velocity, because the two carry different units and a scene authored * against one would be silently rescaled by the other. */ export type XpbdSourceSimplexPairSlipRegularizationN = number | { readonly kind: 'slip-length'; /** World length below which the law is regularized. */ readonly length: number; } | { readonly kind: 'slip-velocity'; /** * Slip speed below which contact is treated as stuck, in length/time. * * Resolved to a length as `velocity * deltaTime` at `prepare`, so the * regularized branch covers the same *speed* range at every timestep * rather than the same distance. */ readonly velocity: number; }; /** Conservative-for-one-lag friction evidence at one candidate placement. */ export interface XpbdSourceSimplexPairFrictionEvaluationN extends XpbdConservativeForceProviderEvaluationN { /** The frozen snapshot this evaluation was taken against. */ readonly lag: XpbdSourceSimplexPairFrictionLagN; /** Tangential slip relative to the lag base. */ readonly slip: VecN; /** `‖slip‖`. */ readonly slipMagnitude: number; /** * Which side of the regularization the slip falls on. * * This is a statement about **slip only**, and says nothing about whether * the term can exert any force at all. A term whose lag carries no normal * force still reports a regime, because it still has a slip. Read * {@link contactActive} for that, and never infer one axis from the other: * in the sheet probe 144 of 192 evaluations read `'sliding'` while exerting * exactly zero force. */ readonly regime: XpbdSourceSimplexPairFrictionRegimeN; /** * Whether the frozen lag can exert any tangential force. * * Exactly `forceLimit > 0`, and orthogonal to {@link regime}: activity is * decided by `frictionCoefficient * laggedNormalForce`, regime by the slip. A * term that is not active contributes exactly zero force and zero potential * energy whatever its slip says, so a population statistic that does not * separate the two is reporting mostly about terms that are not touching * anything. * * It describes **this term**, not the contact. At `frictionCoefficient: 0` the * paired barrier can be pressing hard and the lag can carry a large * `laggedNormalForce` while this reads `false`, because the product is zero. * So it is neither a non-penetration nor a retention certificate; read the * barrier's own distance for the contact itself. */ readonly contactActive: boolean; /** `frictionCoefficient * laggedNormalForce`; the force may not exceed it. */ readonly forceLimit: number; /** The common tangential force; side A receives `-g`, side B `+g`. */ readonly tangentForce: VecN; /** One force per provider particle: side A slots first, then side B's. */ readonly forces: readonly VecN[]; } /** The authored regularization scale after the numeric form is normalized. */ export type XpbdSourceSimplexPairResolvedSlipRegularizationN = { readonly kind: 'slip-length'; readonly length: number; } | { readonly kind: 'slip-velocity'; readonly velocity: number; }; /** Options for {@link XpbdSourceSimplexPairFrictionN.prepare}. */ export interface XpbdSourceSimplexPairFrictionPrepareNOptions { /** * The timestep this lag will be minimized against. * * Required when the term authors `slipRegularization` as a slip velocity, * and refused otherwise: supplying it under an authored length would suggest * the length responds to the timestep, which is exactly the belief this * distinction exists to prevent. */ readonly deltaTime: number; } /** * Turns either authored spelling into the discriminated form. * * The numeric spelling keeps its exact value and becomes a length, so nothing * an existing scene authored changes by a single bit. */ export declare function normalizeXpbdSourceSimplexPairSlipRegularizationN(authored: XpbdSourceSimplexPairSlipRegularizationN, caller: string): XpbdSourceSimplexPairResolvedSlipRegularizationN; /** * Lagged tangential friction as a term **inside** the incremental objective. * * This is not the post-projection Coulomb velocity response * (`XpbdParticleHyperplaneFrictionN`) and cannot be substituted for it: that * one corrects velocities after a position solve, while this one contributes * an energy and a force to the objective the minimizer is descending. The * world-scoped optimizer refuses velocity responses on purpose, and this term * does not route around that refusal — it satisfies the conservative-provider * contract honestly, **for one frozen lag**. * * ## Conservative with one frozen lag, dissipative across accepted states * * The contact frame, the source-ordered weights, and the normal-force * magnitude are captured once at an accepted base state by {@link prepare}. * While that snapshot is held, the potential * * ```text * D(x) = mu * lambdaLag * s(‖u(x)‖), u(x) = (I - n nᵀ)(r(x) - r0) * ``` * * is an ordinary function of position with an exact gradient, so every * line-search trial sees one consistent objective. Dissipation appears * **between** accepted states, when the lag is refreshed. Calling this a * globally conservative physical force would be wrong, and the vocabulary * here says so. * * `s` is the regularized norm: exact above `slipRegularization`, quadratic * below it. The potential is C¹ and the force is C⁰ everywhere — including * through zero slip, where the force is linear in the slip and `u/‖u‖` is * never evaluated. It is deliberately **not** C², which matters only to a * curvature policy. The force magnitude satisfies `‖f‖ ≤ mu·lambdaLag` by * construction rather than by clamping, with equality exactly when sliding. * * ## What may create a lag * * Only a P56 `separated-unique` pair with a positive open distance: that is * the one branch carrying a certified normal, a unique witness, and a * measured margin justifying a derivative. Tied witnesses, certified zero * distance, uncertified comparisons and sub-minimum distances are refused by * type. A refusal never yields a zero-magnitude term, because "no contact to * rub" and "a term that happens to be zero" are different claims. * * Prior art: Li et al., *Incremental Potential Contact* (SIGGRAPH 2020) for * evaluating friction as a lagged, smoothed potential inside the incremental * objective. The implementation is original to this repository. * * @example * Prepare a lag at an accepted state, minimize against it, then refresh: * ```ts * const complex = new CellComplex(3, Float64Array.from([ * -1, 0.1, -1, * 1, 0.1, 1, * -1, 0, 0, * 1, 0, 0 * ]), [ * { dim: 1, verticesPerCell: 2, kind: 'simplex', indices: Uint32Array.from([0, 1]) }, * { dim: 1, verticesPerCell: 2, kind: 'simplex', indices: Uint32Array.from([2, 3]) } * ]); * const moverGroup = complex.groups[0]; * const staticGroup = complex.groups[1]; * if (moverGroup === undefined || staticGroup === undefined) { * throw new Error('expected both authored groups'); * } * const particles = [0, 1].map((vertex) => new XpbdParticleN({ * id: `a-${vertex}`, * position: new VecN(Array.from( * complex.positions.subarray(vertex * 3, (vertex + 1) * 3) * )), * inverseMass: 1 * })); * const barrier = new XpbdSourceSimplexPairBarrierN({ * id: 'contact', * particlesA: particles, * featureA: createSourceSimplexReferenceN( * createSourceCellReferenceN(complex, moverGroup, 0) * ), * featureB: createSourceSimplexReferenceN( * createSourceCellReferenceN(complex, staticGroup, 0) * ), * activationDistance: 0.5, * stiffness: 4 * }); * * const friction = new XpbdSourceSimplexPairFrictionN({ * id: 'slide', barrier, frictionCoefficient: 0.4, slipRegularization: 1e-3 * }); * const prepared = friction.prepare(); // freezes one lag * log('regime at rest', prepared.evaluate().regime); // 'sticking' * log('limit', prepared.evaluate().forceLimit); // mu * lagged normal force * * // …one world step later, from the newly accepted state: * const refreshed = friction.prepare(); * log('refreshed', refreshed.lag.state); // 'prepared' * ``` */ export declare class XpbdSourceSimplexPairFrictionN { /** Stable provider identity. */ readonly id: string; /** Ambient dimension, inherited from the paired barrier. */ readonly dimension: number; /** The paired barrier supplying contact, features, and particle identity. */ readonly barrier: XpbdSourceSimplexPairBarrierN; /** Isotropic Coulomb coefficient. */ readonly frictionCoefficient: number; /** * The authored regularization scale, normalized to its discriminated form. * * A numeric option is normalized to `{ kind: 'slip-length' }`; the number * itself is preserved exactly, so a legacy scene resolves to the identical * length it always did. */ readonly slipRegularization: XpbdSourceSimplexPairResolvedSlipRegularizationN; /** Creates a friction term paired with one contact barrier. */ constructor(options: XpbdSourceSimplexPairFrictionNOptions); /** * Resolves the authored scale to the length this lag will be frozen with. * * The timestep is required under a slip velocity and refused under a slip * length. Refusing it in the second case is deliberate: accepting and * ignoring it would leave an author believing their length tracked the * timestep, which is the belief that makes friction silently vanish under * refinement. */ private resolveRegularizationLength; /** * Freezes one lag at the current (accepted) state and returns the immutable * provider that may be minimized against exactly once. * * @param options carries the timestep this lag will be minimized against, * required exactly when `slipRegularization` is authored as a slip velocity * and refused when it is authored as a length. * @throws {XpbdPotentialDomainErrorN} when the contact cannot justify a * friction term: tied witnesses, certified zero distance, an uncertified * comparison, a sub-minimum distance, or a retired source. */ prepare(options?: XpbdSourceSimplexPairFrictionPrepareNOptions): XpbdPreparedSourceSimplexPairFrictionN; /** Freezes one lag at an explicit accepted base placement. */ prepareAt(positionOf: XpbdParticlePositionQueryN, options?: XpbdSourceSimplexPairFrictionPrepareNOptions): XpbdPreparedSourceSimplexPairFrictionN; } /** * One immutable prepared friction term, valid for **exactly one** minimization. * * It satisfies `XpbdConservativeForceProviderN` for its frozen lag, so the * world-scoped optimizer can evaluate it many times during a line search and * always see one consistent objective. Reusing a consumed lag is a named * failure rather than an implicit refresh, because a silently refreshed lag * would make the objective move between Armijo trials. */ export declare class XpbdPreparedSourceSimplexPairFrictionN implements XpbdConservativeForceProviderN { /** Stable provider identity, inherited from the preparing term. */ readonly id: string; /** Ambient dimension. */ readonly dimension: number; /** Side-A particles then side-B particles, matching `forces`. */ readonly particles: readonly XpbdParticleN[]; /** The friction term that prepared this lag. */ readonly source: XpbdSourceSimplexPairFrictionN; private lagState; /** @internal Prepared through {@link XpbdSourceSimplexPairFrictionN.prepare}. */ constructor(source: XpbdSourceSimplexPairFrictionN, lag: XpbdSourceSimplexPairFrictionLagN); /** The frozen snapshot, including its single-use lifecycle state. */ get lag(): XpbdSourceSimplexPairFrictionLagN; /** Evaluates from live particle positions without mutating anything. */ evaluate(): XpbdSourceSimplexPairFrictionEvaluationN; /** Evaluates one candidate placement against the frozen lag. */ evaluateAt(positionOf: XpbdParticlePositionQueryN): XpbdSourceSimplexPairFrictionEvaluationN; /** * Marks this lag consumed after an accepted, applied step. * * Called by the world transaction; a caller doing its own orchestration * calls it once the step it minimized has actually been applied. * * @throws If the lag was already consumed — never an implicit refresh. */ markConsumed(): void; /** Restores the prepared state after a refused or failed transaction. */ rollback(): void; /** Refuses to evaluate a consumed lag, by name. */ assertUsable(): void; } //# sourceMappingURL=xpbd-source-simplex-pair-friction.d.ts.map