bireactive 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (225) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +81 -0
  3. package/dist/animation/anim.d.ts +57 -0
  4. package/dist/animation/anim.js +318 -0
  5. package/dist/animation/combinators.d.ts +39 -0
  6. package/dist/animation/combinators.js +113 -0
  7. package/dist/animation/easings.d.ts +5 -0
  8. package/dist/animation/easings.js +5 -0
  9. package/dist/animation/index.d.ts +3 -0
  10. package/dist/animation/index.js +3 -0
  11. package/dist/assert/algebra.d.ts +20 -0
  12. package/dist/assert/algebra.js +79 -0
  13. package/dist/assert/claim.d.ts +40 -0
  14. package/dist/assert/claim.js +129 -0
  15. package/dist/assert/index.d.ts +7 -0
  16. package/dist/assert/index.js +19 -0
  17. package/dist/assert/predicates.d.ts +18 -0
  18. package/dist/assert/predicates.js +43 -0
  19. package/dist/assert/record.d.ts +20 -0
  20. package/dist/assert/record.js +78 -0
  21. package/dist/assert/scope.d.ts +42 -0
  22. package/dist/assert/scope.js +233 -0
  23. package/dist/assert/span.d.ts +37 -0
  24. package/dist/assert/span.js +68 -0
  25. package/dist/assert/tree.d.ts +22 -0
  26. package/dist/assert/tree.js +65 -0
  27. package/dist/code/code.d.ts +70 -0
  28. package/dist/code/code.js +361 -0
  29. package/dist/code/index.d.ts +2 -0
  30. package/dist/code/index.js +9 -0
  31. package/dist/code/morph.d.ts +5 -0
  32. package/dist/code/morph.js +194 -0
  33. package/dist/code/tokenize.d.ts +8 -0
  34. package/dist/code/tokenize.js +51 -0
  35. package/dist/constraints/cluster.d.ts +83 -0
  36. package/dist/constraints/cluster.js +213 -0
  37. package/dist/constraints/drivers.d.ts +15 -0
  38. package/dist/constraints/drivers.js +40 -0
  39. package/dist/constraints/factories.d.ts +73 -0
  40. package/dist/constraints/factories.js +248 -0
  41. package/dist/constraints/index.d.ts +11 -0
  42. package/dist/constraints/index.js +39 -0
  43. package/dist/constraints/interaction.d.ts +21 -0
  44. package/dist/constraints/interaction.js +148 -0
  45. package/dist/constraints/linalg.d.ts +18 -0
  46. package/dist/constraints/linalg.js +141 -0
  47. package/dist/constraints/phases.d.ts +21 -0
  48. package/dist/constraints/phases.js +60 -0
  49. package/dist/constraints/physics.d.ts +34 -0
  50. package/dist/constraints/physics.js +128 -0
  51. package/dist/constraints/rigid.d.ts +210 -0
  52. package/dist/constraints/rigid.js +835 -0
  53. package/dist/constraints/solver.d.ts +107 -0
  54. package/dist/constraints/solver.js +510 -0
  55. package/dist/constraints/term.d.ts +50 -0
  56. package/dist/constraints/term.js +80 -0
  57. package/dist/constraints/terms.d.ts +80 -0
  58. package/dist/constraints/terms.js +302 -0
  59. package/dist/constraints/world.d.ts +31 -0
  60. package/dist/constraints/world.js +245 -0
  61. package/dist/core/aggregates.d.ts +64 -0
  62. package/dist/core/aggregates.js +198 -0
  63. package/dist/core/anim.d.ts +84 -0
  64. package/dist/core/anim.js +301 -0
  65. package/dist/core/index.d.ts +38 -0
  66. package/dist/core/index.js +38 -0
  67. package/dist/core/introspect.d.ts +5 -0
  68. package/dist/core/introspect.js +31 -0
  69. package/dist/core/lenses/closed-form-policies.d.ts +64 -0
  70. package/dist/core/lenses/closed-form-policies.js +452 -0
  71. package/dist/core/lenses/domain-aggregates.d.ts +54 -0
  72. package/dist/core/lenses/domain-aggregates.js +259 -0
  73. package/dist/core/lenses/factor-lens.d.ts +42 -0
  74. package/dist/core/lenses/factor-lens.js +419 -0
  75. package/dist/core/lenses/index.d.ts +5 -0
  76. package/dist/core/lenses/index.js +16 -0
  77. package/dist/core/lenses/memory.d.ts +47 -0
  78. package/dist/core/lenses/memory.js +102 -0
  79. package/dist/core/lenses/typed-factor.d.ts +45 -0
  80. package/dist/core/lenses/typed-factor.js +376 -0
  81. package/dist/core/network-utils.d.ts +14 -0
  82. package/dist/core/network-utils.js +62 -0
  83. package/dist/core/new-primitives.d.ts +33 -0
  84. package/dist/core/new-primitives.js +113 -0
  85. package/dist/core/signal.d.ts +254 -0
  86. package/dist/core/signal.js +1349 -0
  87. package/dist/core/traits.d.ts +61 -0
  88. package/dist/core/traits.js +56 -0
  89. package/dist/core/tree.d.ts +23 -0
  90. package/dist/core/tree.js +62 -0
  91. package/dist/core/values/anchor.d.ts +23 -0
  92. package/dist/core/values/anchor.js +23 -0
  93. package/dist/core/values/audio.d.ts +33 -0
  94. package/dist/core/values/audio.js +107 -0
  95. package/dist/core/values/bool.d.ts +37 -0
  96. package/dist/core/values/bool.js +75 -0
  97. package/dist/core/values/box.d.ts +77 -0
  98. package/dist/core/values/box.js +211 -0
  99. package/dist/core/values/canvas.d.ts +71 -0
  100. package/dist/core/values/canvas.js +495 -0
  101. package/dist/core/values/color.d.ts +49 -0
  102. package/dist/core/values/color.js +106 -0
  103. package/dist/core/values/flags.d.ts +18 -0
  104. package/dist/core/values/flags.js +50 -0
  105. package/dist/core/values/gpu.d.ts +74 -0
  106. package/dist/core/values/gpu.js +426 -0
  107. package/dist/core/values/matrix.d.ts +53 -0
  108. package/dist/core/values/matrix.js +140 -0
  109. package/dist/core/values/num.d.ts +62 -0
  110. package/dist/core/values/num.js +166 -0
  111. package/dist/core/values/pose.d.ts +31 -0
  112. package/dist/core/values/pose.js +83 -0
  113. package/dist/core/values/range.d.ts +83 -0
  114. package/dist/core/values/range.js +167 -0
  115. package/dist/core/values/str.d.ts +76 -0
  116. package/dist/core/values/str.js +346 -0
  117. package/dist/core/values/template.d.ts +49 -0
  118. package/dist/core/values/template.js +148 -0
  119. package/dist/core/values/transform.d.ts +49 -0
  120. package/dist/core/values/transform.js +115 -0
  121. package/dist/core/values/tri.d.ts +31 -0
  122. package/dist/core/values/tri.js +95 -0
  123. package/dist/core/values/vec.d.ts +72 -0
  124. package/dist/core/values/vec.js +219 -0
  125. package/dist/core/writable.d.ts +15 -0
  126. package/dist/core/writable.js +29 -0
  127. package/dist/ext/events.d.ts +10 -0
  128. package/dist/ext/events.js +31 -0
  129. package/dist/ext/index.d.ts +4 -0
  130. package/dist/ext/index.js +4 -0
  131. package/dist/ext/snapshot.d.ts +8 -0
  132. package/dist/ext/snapshot.js +29 -0
  133. package/dist/ext/timeline.d.ts +56 -0
  134. package/dist/ext/timeline.js +94 -0
  135. package/dist/ext/waapi.d.ts +25 -0
  136. package/dist/ext/waapi.js +198 -0
  137. package/dist/index.d.ts +8 -0
  138. package/dist/index.js +10 -0
  139. package/dist/propagators/index.d.ts +6 -0
  140. package/dist/propagators/index.js +6 -0
  141. package/dist/propagators/layout.d.ts +68 -0
  142. package/dist/propagators/layout.js +336 -0
  143. package/dist/propagators/network.d.ts +52 -0
  144. package/dist/propagators/network.js +185 -0
  145. package/dist/propagators/propagator.d.ts +12 -0
  146. package/dist/propagators/propagator.js +16 -0
  147. package/dist/propagators/range.d.ts +45 -0
  148. package/dist/propagators/range.js +147 -0
  149. package/dist/propagators/relations.d.ts +60 -0
  150. package/dist/propagators/relations.js +343 -0
  151. package/dist/shapes/annular-sector.d.ts +15 -0
  152. package/dist/shapes/annular-sector.js +64 -0
  153. package/dist/shapes/button.d.ts +14 -0
  154. package/dist/shapes/button.js +31 -0
  155. package/dist/shapes/choreographers.d.ts +22 -0
  156. package/dist/shapes/choreographers.js +69 -0
  157. package/dist/shapes/circle.d.ts +17 -0
  158. package/dist/shapes/circle.js +57 -0
  159. package/dist/shapes/clip.d.ts +5 -0
  160. package/dist/shapes/clip.js +31 -0
  161. package/dist/shapes/connect.d.ts +16 -0
  162. package/dist/shapes/connect.js +70 -0
  163. package/dist/shapes/curve.d.ts +60 -0
  164. package/dist/shapes/curve.js +285 -0
  165. package/dist/shapes/dashed.d.ts +16 -0
  166. package/dist/shapes/dashed.js +142 -0
  167. package/dist/shapes/debug.d.ts +43 -0
  168. package/dist/shapes/debug.js +97 -0
  169. package/dist/shapes/group.d.ts +5 -0
  170. package/dist/shapes/group.js +10 -0
  171. package/dist/shapes/handle.d.ts +32 -0
  172. package/dist/shapes/handle.js +88 -0
  173. package/dist/shapes/index.d.ts +23 -0
  174. package/dist/shapes/index.js +23 -0
  175. package/dist/shapes/interaction.d.ts +32 -0
  176. package/dist/shapes/interaction.js +187 -0
  177. package/dist/shapes/label.d.ts +20 -0
  178. package/dist/shapes/label.js +42 -0
  179. package/dist/shapes/layout.d.ts +29 -0
  180. package/dist/shapes/layout.js +74 -0
  181. package/dist/shapes/line.d.ts +21 -0
  182. package/dist/shapes/line.js +79 -0
  183. package/dist/shapes/list.d.ts +18 -0
  184. package/dist/shapes/list.js +51 -0
  185. package/dist/shapes/mount.d.ts +7 -0
  186. package/dist/shapes/mount.js +10 -0
  187. package/dist/shapes/path.d.ts +77 -0
  188. package/dist/shapes/path.js +227 -0
  189. package/dist/shapes/rect.d.ts +30 -0
  190. package/dist/shapes/rect.js +131 -0
  191. package/dist/shapes/shape.d.ts +132 -0
  192. package/dist/shapes/shape.js +306 -0
  193. package/dist/shapes/text.d.ts +24 -0
  194. package/dist/shapes/text.js +53 -0
  195. package/dist/shapes/tokens.d.ts +28 -0
  196. package/dist/shapes/tokens.js +27 -0
  197. package/dist/shapes/transitions.d.ts +23 -0
  198. package/dist/shapes/transitions.js +62 -0
  199. package/dist/tex/decorations.d.ts +26 -0
  200. package/dist/tex/decorations.js +116 -0
  201. package/dist/tex/index.d.ts +5 -0
  202. package/dist/tex/index.js +5 -0
  203. package/dist/tex/marker.d.ts +17 -0
  204. package/dist/tex/marker.js +63 -0
  205. package/dist/tex/motion.d.ts +43 -0
  206. package/dist/tex/motion.js +290 -0
  207. package/dist/tex/parts.d.ts +65 -0
  208. package/dist/tex/parts.js +149 -0
  209. package/dist/tex/tex.d.ts +45 -0
  210. package/dist/tex/tex.js +244 -0
  211. package/dist/web/attr.d.ts +16 -0
  212. package/dist/web/attr.js +98 -0
  213. package/dist/web/diagram.d.ts +49 -0
  214. package/dist/web/diagram.js +260 -0
  215. package/dist/web/index.d.ts +6 -0
  216. package/dist/web/index.js +6 -0
  217. package/dist/web/md-marker.d.ts +6 -0
  218. package/dist/web/md-marker.js +39 -0
  219. package/dist/web/md-tex.d.ts +6 -0
  220. package/dist/web/md-tex.js +61 -0
  221. package/dist/web/raf.d.ts +6 -0
  222. package/dist/web/raf.js +24 -0
  223. package/dist/web/viewport.d.ts +7 -0
  224. package/dist/web/viewport.js +13 -0
  225. package/package.json +87 -0
@@ -0,0 +1,107 @@
1
+ import { type Term } from "./term.js";
2
+ export interface SolverOpts {
3
+ /** Primal+dual iterations per solve. Default 10. */
4
+ iterations?: number;
5
+ /** Stabilisation α ∈ [0, 1]. With `postStabilize` off: applied
6
+ * every iteration as `C(x) − α·C(x⁻)` and as the λ warm-start
7
+ * factor `α·γ`. With it on: regular iters force α = 1 and the
8
+ * post-stab iter uses α = 0, but the field still controls
9
+ * inter-frame λ decay. Paper: `0.99` physics, `0` static. */
10
+ alpha?: number;
11
+ /** Penalty ramp β. Default 1e5. */
12
+ beta?: number;
13
+ /** Warm-start decay γ ∈ [0, 1). Default 0.99. */
14
+ gamma?: number;
15
+ /** Run a final primal-only `α = 0` iteration per `solve(dt)` (and
16
+ * force `α = 1` on the regular iters — drift-tolerant: λ still
17
+ * grows when violated but the primal step doesn't unwind existing
18
+ * residual). AVBD's physics default. Default `false` so static
19
+ * `solve()` keeps its iter-N Newton behaviour. */
20
+ postStabilize?: boolean;
21
+ /** Initial buffer capacity in scalar slots; doubles on demand.
22
+ * Default 64. */
23
+ initialCapacity?: number;
24
+ }
25
+ export declare class Solver {
26
+ iterations: number;
27
+ alpha: number;
28
+ beta: number;
29
+ gamma: number;
30
+ postStabilize: boolean;
31
+ /** Packed positions; `positions[offsets[id] + k]` is component k
32
+ * of cell `id`. */
33
+ positions: Float64Array;
34
+ /** Positions at start of step (`x⁻`). */
35
+ initials: Float64Array;
36
+ /** Anchor positions (`y`) the regularizer pulls toward. Defaults to
37
+ * `initials` after `prepare()`; integrating factories overwrite
38
+ * before `solve(dt)`. */
39
+ anchors: Float64Array;
40
+ /** Anchor weight per scalar slot (mass/inertia, stickiness, or
41
+ * preconditioner). Slot-0 `0` means pinned (primal update skipped).
42
+ * Set via `setMass` (uniform) or `setMassDiag` (per-DOF). */
43
+ masses: Float64Array;
44
+ dims: Uint8Array;
45
+ /** Start of each cell in `positions` (cumulative `dims`). */
46
+ offsets: Uint32Array;
47
+ /** Per-cell incident terms. */
48
+ cellTerms: Term[][];
49
+ /** `cellTermIdx[id][k]` = this cell's index within
50
+ * `cellTerms[id][k].cells`; avoids an `indexOf` per visit. */
51
+ cellTermIdx: number[][];
52
+ private readonly _terms;
53
+ /** Read-only view of registered terms. */
54
+ get terms(): readonly Term[];
55
+ private _capacity;
56
+ private _totalDof;
57
+ private _cellCount;
58
+ private _maxDim;
59
+ private _lhs;
60
+ private _rhs;
61
+ get cellCount(): number;
62
+ constructor(opts?: SolverOpts);
63
+ /** Add a cell with the given dim. Optionally seed initial value
64
+ * via `init`. Returns the cell's integer id. */
65
+ addCell(dim: number, init?: ArrayLike<number>): number;
66
+ addTerm(term: Term): void;
67
+ removeTerm(term: Term): void;
68
+ /** @internal — wire cell ↔ term adjacency (from `Term` ctor). */
69
+ _connectTerm(term: Term, cellId: number, cellIndex: number): void;
70
+ /** Read cell `id`'s position into `out` (or a fresh array). */
71
+ read(id: number, out?: number[]): number[];
72
+ /** Write cell `id`'s position from `value`. */
73
+ write(id: number, value: ArrayLike<number>): void;
74
+ /** Get the (slot-0) mass of cell `id`. `0` means pinned. For
75
+ * cells with non-uniform mass (`setMassDiag`), this returns the
76
+ * first DOF's mass; the others may differ. */
77
+ massOf(id: number): number;
78
+ /** Set every DOF of cell `id` to the same mass. Call with `0`
79
+ * to pin (skip in primal sweep, value preserved). */
80
+ setMass(id: number, m: number): void;
81
+ /** Set per-DOF mass (`m.length` must equal the cell's dim). For
82
+ * rigid bodies with differing linear/rotational inertia, e.g.
83
+ * `[mass, mass, moment]`. All-zero pins. */
84
+ setMassDiag(id: number, m: ArrayLike<number>): void;
85
+ /** Snapshot `initials = anchors = positions` and warm-start each
86
+ * term. Callers may overwrite `anchors` before `solve(dt)`. */
87
+ prepare(): void;
88
+ /** Run the iteration loop against the current `anchors`. `dt`
89
+ * scales the regularizer `M / dt²` vs the constraints (default 1,
90
+ * static editing). With `postStabilize`, regular iters use α = 1
91
+ * and a final α = 0 iter zeros the residual; otherwise every iter
92
+ * uses `this.alpha`.
93
+ *
94
+ * `beforePostStab` fires at the regular/post-stab boundary (or
95
+ * once after the final iter when post-stab is off). Physics hooks
96
+ * it to read velocity from the physical trajectory, before the
97
+ * post-stab projection unwinds drift. */
98
+ solve(dt?: number, beforePostStab?: () => void): void;
99
+ /** Compute `‖C‖` for diagnostics. */
100
+ residualNorm(): number;
101
+ /** Forward Gauss-Seidel sweep over cells. Hot path. */
102
+ private _primalSweep;
103
+ /** Dual update over all terms. */
104
+ private _dualPass;
105
+ private _growCellArrays;
106
+ private _growScalarBuffers;
107
+ }
@@ -0,0 +1,510 @@
1
+ // solver.ts — Augmented Vertex Block Descent (AVBD) numerical kernel.
2
+ //
3
+ // SOA layout: per-cell state in packed Float64/Uint buffers indexed by
4
+ // integer cell id; no per-cell allocation. Cells are integer handles
5
+ // from `addCell`; terms read positions via `positions[offsets[id]+k]`.
6
+ //
7
+ // Core problem: given anchor `y` and constraints, find `x` near `y`
8
+ // (weighted by `M/dt²`) that satisfies them. Generic, not physics-
9
+ // specific:
10
+ // - `y` (anchors): regularizer reference (inertial extrapolation,
11
+ // `x⁻`, or Adam's `x − lr·m̂/√v̂`).
12
+ // - `M` (masses): per-DOF anchor weight (inertia, stickiness, or
13
+ // `1/√v̂` preconditioner).
14
+ // - `dt`: regularizer-vs-constraints scale. dt → 0 hard projection,
15
+ // dt → ∞ stay-put.
16
+ import { clamp, solveSPD } from "./linalg.js";
17
+ import { LAMBDA_MAX, PENALTY_MAX, PENALTY_MIN } from "./term.js";
18
+ const TINY = 1e-14;
19
+ /** O(1) remove-by-swap; does not preserve element ordering. */
20
+ function swapPop(arr, idx) {
21
+ if (idx < 0 || idx >= arr.length)
22
+ return;
23
+ const last = arr.length - 1;
24
+ if (idx !== last)
25
+ arr[idx] = arr[last];
26
+ arr.pop();
27
+ }
28
+ export class Solver {
29
+ iterations;
30
+ alpha;
31
+ beta;
32
+ gamma;
33
+ postStabilize;
34
+ /** Packed positions; `positions[offsets[id] + k]` is component k
35
+ * of cell `id`. */
36
+ positions;
37
+ /** Positions at start of step (`x⁻`). */
38
+ initials;
39
+ /** Anchor positions (`y`) the regularizer pulls toward. Defaults to
40
+ * `initials` after `prepare()`; integrating factories overwrite
41
+ * before `solve(dt)`. */
42
+ anchors;
43
+ /** Anchor weight per scalar slot (mass/inertia, stickiness, or
44
+ * preconditioner). Slot-0 `0` means pinned (primal update skipped).
45
+ * Set via `setMass` (uniform) or `setMassDiag` (per-DOF). */
46
+ masses;
47
+ dims;
48
+ /** Start of each cell in `positions` (cumulative `dims`). */
49
+ offsets;
50
+ /** Per-cell incident terms. */
51
+ cellTerms = [];
52
+ /** `cellTermIdx[id][k]` = this cell's index within
53
+ * `cellTerms[id][k].cells`; avoids an `indexOf` per visit. */
54
+ cellTermIdx = [];
55
+ _terms = [];
56
+ /** Read-only view of registered terms. */
57
+ get terms() {
58
+ return this._terms;
59
+ }
60
+ _capacity; // current buffer length in scalar slots
61
+ _totalDof = 0; // current used scalar slots
62
+ _cellCount = 0;
63
+ _maxDim = 0;
64
+ _lhs = new Float64Array(0); // dim×dim local Newton scratch
65
+ _rhs = new Float64Array(0);
66
+ get cellCount() {
67
+ return this._cellCount;
68
+ }
69
+ constructor(opts = {}) {
70
+ this.iterations = opts.iterations ?? 10;
71
+ this.alpha = opts.alpha ?? 0;
72
+ this.beta = opts.beta ?? 1e5;
73
+ this.gamma = opts.gamma ?? 0.99;
74
+ this.postStabilize = opts.postStabilize ?? false;
75
+ const cap = opts.initialCapacity ?? 64;
76
+ this._capacity = cap;
77
+ this.positions = new Float64Array(cap);
78
+ this.initials = new Float64Array(cap);
79
+ this.anchors = new Float64Array(cap);
80
+ // Per-DOF mass — same length as positions; dims/offsets are per-cell.
81
+ this.masses = new Float64Array(cap);
82
+ this.dims = new Uint8Array(16);
83
+ this.offsets = new Uint32Array(16);
84
+ }
85
+ /** Add a cell with the given dim. Optionally seed initial value
86
+ * via `init`. Returns the cell's integer id. */
87
+ addCell(dim, init) {
88
+ const id = this._cellCount;
89
+ if (id >= this.dims.length)
90
+ this._growCellArrays();
91
+ if (this._totalDof + dim > this._capacity)
92
+ this._growScalarBuffers(dim);
93
+ const off = this._totalDof;
94
+ this.dims[id] = dim;
95
+ this.offsets[id] = off;
96
+ for (let k = 0; k < dim; k++) {
97
+ const v = init?.[k] ?? 0;
98
+ this.positions[off + k] = v;
99
+ this.initials[off + k] = v;
100
+ this.anchors[off + k] = v;
101
+ this.masses[off + k] = 1;
102
+ }
103
+ this._totalDof += dim;
104
+ this._cellCount++;
105
+ this.cellTerms.push([]);
106
+ this.cellTermIdx.push([]);
107
+ if (dim > this._maxDim) {
108
+ this._maxDim = dim;
109
+ this._lhs = new Float64Array(dim * dim);
110
+ this._rhs = new Float64Array(dim);
111
+ }
112
+ return id;
113
+ }
114
+ addTerm(term) {
115
+ this._terms.push(term);
116
+ }
117
+ removeTerm(term) {
118
+ swapPop(this._terms, this._terms.indexOf(term));
119
+ const cells = term.cells;
120
+ for (let ci = 0; ci < cells.length; ci++) {
121
+ const cid = cells[ci];
122
+ const list = this.cellTerms[cid];
123
+ const idxList = this.cellTermIdx[cid];
124
+ const k = list.indexOf(term);
125
+ if (k < 0)
126
+ continue;
127
+ swapPop(list, k);
128
+ swapPop(idxList, k);
129
+ }
130
+ }
131
+ /** @internal — wire cell ↔ term adjacency (from `Term` ctor). */
132
+ _connectTerm(term, cellId, cellIndex) {
133
+ this.cellTerms[cellId].push(term);
134
+ this.cellTermIdx[cellId].push(cellIndex);
135
+ }
136
+ /** Read cell `id`'s position into `out` (or a fresh array). */
137
+ read(id, out = []) {
138
+ const off = this.offsets[id];
139
+ const dim = this.dims[id];
140
+ for (let k = 0; k < dim; k++)
141
+ out[k] = this.positions[off + k];
142
+ out.length = dim;
143
+ return out;
144
+ }
145
+ /** Write cell `id`'s position from `value`. */
146
+ write(id, value) {
147
+ const off = this.offsets[id];
148
+ const dim = this.dims[id];
149
+ for (let k = 0; k < dim; k++)
150
+ this.positions[off + k] = value[k] ?? 0;
151
+ }
152
+ /** Get the (slot-0) mass of cell `id`. `0` means pinned. For
153
+ * cells with non-uniform mass (`setMassDiag`), this returns the
154
+ * first DOF's mass; the others may differ. */
155
+ massOf(id) {
156
+ return this.masses[this.offsets[id]];
157
+ }
158
+ /** Set every DOF of cell `id` to the same mass. Call with `0`
159
+ * to pin (skip in primal sweep, value preserved). */
160
+ setMass(id, m) {
161
+ const off = this.offsets[id];
162
+ const dim = this.dims[id];
163
+ for (let k = 0; k < dim; k++)
164
+ this.masses[off + k] = m;
165
+ }
166
+ /** Set per-DOF mass (`m.length` must equal the cell's dim). For
167
+ * rigid bodies with differing linear/rotational inertia, e.g.
168
+ * `[mass, mass, moment]`. All-zero pins. */
169
+ setMassDiag(id, m) {
170
+ const off = this.offsets[id];
171
+ const dim = this.dims[id];
172
+ for (let k = 0; k < dim; k++)
173
+ this.masses[off + k] = m[k] ?? 0;
174
+ }
175
+ /** Snapshot `initials = anchors = positions` and warm-start each
176
+ * term. Callers may overwrite `anchors` before `solve(dt)`. */
177
+ prepare() {
178
+ for (let fi = this._terms.length - 1; fi >= 0; fi--) {
179
+ const t = this._terms[fi];
180
+ if (t.disabled) {
181
+ this.removeTerm(t);
182
+ continue;
183
+ }
184
+ if (!t.initialize()) {
185
+ this.removeTerm(t);
186
+ continue;
187
+ }
188
+ t.computeConstraint(0);
189
+ for (let r = 0; r < t.rows; r++)
190
+ t.C0[r] = t.C[r];
191
+ // λ + penalty warm-start with AVBD §3.7 forgetting factor γ.
192
+ // Must decay every frame regardless of mode: otherwise stacked /
193
+ // sliding contacts accumulate dual impulse forever (rest jitter,
194
+ // oscillation under perturbation).
195
+ const ag = this.alpha * this.gamma;
196
+ for (let r = 0; r < t.rows; r++) {
197
+ t.lambda[r] *= ag;
198
+ t.penalty[r] = clamp(t.penalty[r] * this.gamma, PENALTY_MIN, PENALTY_MAX);
199
+ const k = t.stiffness[r];
200
+ if (Number.isFinite(k) && t.penalty[r] > k)
201
+ t.penalty[r] = k;
202
+ }
203
+ }
204
+ // Cell warm-start: y = x⁻ by default.
205
+ const N = this._totalDof;
206
+ for (let i = 0; i < N; i++) {
207
+ this.initials[i] = this.positions[i];
208
+ this.anchors[i] = this.positions[i];
209
+ }
210
+ }
211
+ /** Run the iteration loop against the current `anchors`. `dt`
212
+ * scales the regularizer `M / dt²` vs the constraints (default 1,
213
+ * static editing). With `postStabilize`, regular iters use α = 1
214
+ * and a final α = 0 iter zeros the residual; otherwise every iter
215
+ * uses `this.alpha`.
216
+ *
217
+ * `beforePostStab` fires at the regular/post-stab boundary (or
218
+ * once after the final iter when post-stab is off). Physics hooks
219
+ * it to read velocity from the physical trajectory, before the
220
+ * post-stab projection unwinds drift. */
221
+ solve(dt = 1, beforePostStab) {
222
+ const inv_dt2 = 1 / (dt * dt);
223
+ if (this.postStabilize) {
224
+ for (let it = 0; it < this.iterations; it++) {
225
+ this._primalSweep(1, inv_dt2);
226
+ this._dualPass(1);
227
+ }
228
+ if (beforePostStab)
229
+ beforePostStab();
230
+ this._primalSweep(0, inv_dt2);
231
+ }
232
+ else {
233
+ const a = this.alpha;
234
+ for (let it = 0; it < this.iterations; it++) {
235
+ this._primalSweep(a, inv_dt2);
236
+ this._dualPass(a);
237
+ }
238
+ if (beforePostStab)
239
+ beforePostStab();
240
+ }
241
+ }
242
+ /** Compute `‖C‖` for diagnostics. */
243
+ residualNorm() {
244
+ let s = 0;
245
+ for (const t of this._terms) {
246
+ if (t.disabled)
247
+ continue;
248
+ t.computeConstraint(0);
249
+ for (let r = 0; r < t.rows; r++)
250
+ s += t.C[r] * t.C[r];
251
+ }
252
+ return Math.sqrt(s);
253
+ }
254
+ /** Forward Gauss-Seidel sweep over cells. Hot path. */
255
+ _primalSweep(currentAlpha, inv_dt2) {
256
+ const lhs = this._lhs;
257
+ const rhs = this._rhs;
258
+ const positions = this.positions;
259
+ const anchors = this.anchors;
260
+ const masses = this.masses;
261
+ const dims = this.dims;
262
+ const offsets = this.offsets;
263
+ const cellTerms = this.cellTerms;
264
+ const cellTermIdx = this.cellTermIdx;
265
+ const N = this._cellCount;
266
+ for (let cellI = 0; cellI < N; cellI++) {
267
+ const dim = dims[cellI];
268
+ const off = offsets[cellI];
269
+ const m0 = masses[off];
270
+ // Pinned cells have slot-0 mass = 0; skip the primal step.
271
+ if (m0 <= 0)
272
+ continue;
273
+ // Initialise lhs = diag(masses[off + k]) / dt², rhs = same · (x − y).
274
+ // Hand-unrolled for dim=2 and dim=3 (rigid body); generic loop otherwise.
275
+ if (dim === 2) {
276
+ const m1 = masses[off + 1];
277
+ const m0Dt2 = m0 * inv_dt2;
278
+ const m1Dt2 = m1 * inv_dt2;
279
+ lhs[0] = m0Dt2;
280
+ lhs[1] = 0;
281
+ lhs[2] = 0;
282
+ lhs[3] = m1Dt2;
283
+ rhs[0] = m0Dt2 * (positions[off] - anchors[off]);
284
+ rhs[1] = m1Dt2 * (positions[off + 1] - anchors[off + 1]);
285
+ }
286
+ else if (dim === 3) {
287
+ const m1 = masses[off + 1];
288
+ const m2 = masses[off + 2];
289
+ const m0Dt2 = m0 * inv_dt2;
290
+ const m1Dt2 = m1 * inv_dt2;
291
+ const m2Dt2 = m2 * inv_dt2;
292
+ lhs[0] = m0Dt2;
293
+ lhs[1] = 0;
294
+ lhs[2] = 0;
295
+ lhs[3] = 0;
296
+ lhs[4] = m1Dt2;
297
+ lhs[5] = 0;
298
+ lhs[6] = 0;
299
+ lhs[7] = 0;
300
+ lhs[8] = m2Dt2;
301
+ rhs[0] = m0Dt2 * (positions[off] - anchors[off]);
302
+ rhs[1] = m1Dt2 * (positions[off + 1] - anchors[off + 1]);
303
+ rhs[2] = m2Dt2 * (positions[off + 2] - anchors[off + 2]);
304
+ }
305
+ else {
306
+ for (let i = 0; i < dim * dim; i++)
307
+ lhs[i] = 0;
308
+ for (let k = 0; k < dim; k++) {
309
+ const mk = masses[off + k];
310
+ const mkDt2 = mk * inv_dt2;
311
+ lhs[k * dim + k] = mkDt2;
312
+ rhs[k] = mkDt2 * (positions[off + k] - anchors[off + k]);
313
+ }
314
+ }
315
+ const termList = cellTerms[cellI];
316
+ const termCiList = cellTermIdx[cellI];
317
+ const flen = termList.length;
318
+ for (let fi = 0; fi < flen; fi++) {
319
+ const t = termList[fi];
320
+ if (t.disabled)
321
+ continue;
322
+ const ci = termCiList[fi];
323
+ t.computeConstraint(currentAlpha);
324
+ t.computeDerivatives(ci);
325
+ const Jblock = t.J[ci];
326
+ const Hcols = t.HCols[ci];
327
+ const fStiff = t.stiffness;
328
+ const fLambda = t.lambda;
329
+ const fPenalty = t.penalty;
330
+ const fC = t.C;
331
+ const fMin = t.lambdaMin;
332
+ const fMax = t.lambdaMax;
333
+ const rows = t.rows;
334
+ for (let r = 0; r < rows; r++) {
335
+ const lambda = fStiff[r] === Number.POSITIVE_INFINITY ? fLambda[r] : 0;
336
+ const kC = fPenalty[r] * fC[r] + lambda;
337
+ const lo = fMin[r];
338
+ const hi = fMax[r];
339
+ const fc = kC < lo ? lo : kC > hi ? hi : kC;
340
+ const baseJ = r * dim;
341
+ const penalty_r = fPenalty[r];
342
+ const absF = fc < 0 ? -fc : fc;
343
+ if (dim === 2) {
344
+ const j0 = Jblock[baseJ];
345
+ const j1 = Jblock[baseJ + 1];
346
+ rhs[0] += j0 * fc;
347
+ rhs[1] += j1 * fc;
348
+ lhs[0] += penalty_r * j0 * j0;
349
+ lhs[1] += penalty_r * j0 * j1;
350
+ lhs[2] += penalty_r * j1 * j0;
351
+ lhs[3] += penalty_r * j1 * j1;
352
+ if (absF > TINY) {
353
+ lhs[0] += Hcols[baseJ] * absF;
354
+ lhs[3] += Hcols[baseJ + 1] * absF;
355
+ }
356
+ }
357
+ else if (dim === 3) {
358
+ const j0 = Jblock[baseJ];
359
+ const j1 = Jblock[baseJ + 1];
360
+ const j2 = Jblock[baseJ + 2];
361
+ rhs[0] += j0 * fc;
362
+ rhs[1] += j1 * fc;
363
+ rhs[2] += j2 * fc;
364
+ const pj0 = penalty_r * j0;
365
+ const pj1 = penalty_r * j1;
366
+ const pj2 = penalty_r * j2;
367
+ lhs[0] += pj0 * j0;
368
+ lhs[1] += pj0 * j1;
369
+ lhs[2] += pj0 * j2;
370
+ lhs[3] += pj1 * j0;
371
+ lhs[4] += pj1 * j1;
372
+ lhs[5] += pj1 * j2;
373
+ lhs[6] += pj2 * j0;
374
+ lhs[7] += pj2 * j1;
375
+ lhs[8] += pj2 * j2;
376
+ if (absF > TINY) {
377
+ lhs[0] += Hcols[baseJ] * absF;
378
+ lhs[4] += Hcols[baseJ + 1] * absF;
379
+ lhs[8] += Hcols[baseJ + 2] * absF;
380
+ }
381
+ }
382
+ else {
383
+ for (let k = 0; k < dim; k++)
384
+ rhs[k] += Jblock[baseJ + k] * fc;
385
+ for (let i = 0; i < dim; i++) {
386
+ const ji = penalty_r * Jblock[baseJ + i];
387
+ for (let j = 0; j < dim; j++) {
388
+ lhs[i * dim + j] += ji * Jblock[baseJ + j];
389
+ }
390
+ }
391
+ if (absF > TINY) {
392
+ for (let k = 0; k < dim; k++) {
393
+ lhs[k * dim + k] += Hcols[baseJ + k] * absF;
394
+ }
395
+ }
396
+ }
397
+ }
398
+ }
399
+ // Solve `lhs · delta = -rhs`, write `position -= delta`. Skip on
400
+ // rank-deficiency or non-finite results (would poison positions
401
+ // with NaN).
402
+ if (!solveSPD(lhs, rhs, dim))
403
+ continue;
404
+ if (dim === 2) {
405
+ const r0 = rhs[0];
406
+ const r1 = rhs[1];
407
+ if (!Number.isFinite(r0) || !Number.isFinite(r1))
408
+ continue;
409
+ positions[off] -= r0;
410
+ positions[off + 1] -= r1;
411
+ }
412
+ else if (dim === 3) {
413
+ const r0 = rhs[0];
414
+ const r1 = rhs[1];
415
+ const r2 = rhs[2];
416
+ if (!Number.isFinite(r0) || !Number.isFinite(r1) || !Number.isFinite(r2))
417
+ continue;
418
+ positions[off] -= r0;
419
+ positions[off + 1] -= r1;
420
+ positions[off + 2] -= r2;
421
+ }
422
+ else {
423
+ let bad = false;
424
+ for (let k = 0; k < dim; k++) {
425
+ if (!Number.isFinite(rhs[k])) {
426
+ bad = true;
427
+ break;
428
+ }
429
+ }
430
+ if (bad)
431
+ continue;
432
+ for (let k = 0; k < dim; k++)
433
+ positions[off + k] -= rhs[k];
434
+ }
435
+ }
436
+ }
437
+ /** Dual update over all terms. */
438
+ _dualPass(currentAlpha) {
439
+ const beta = this.beta;
440
+ const allTerms = this._terms;
441
+ for (let fi = 0; fi < allTerms.length; fi++) {
442
+ const t = allTerms[fi];
443
+ if (t.disabled)
444
+ continue;
445
+ t.computeConstraint(currentAlpha);
446
+ const fLambda = t.lambda;
447
+ const fPenalty = t.penalty;
448
+ const fC = t.C;
449
+ const fMin = t.lambdaMin;
450
+ const fMax = t.lambdaMax;
451
+ const fStiff = t.stiffness;
452
+ const fFracture = t.fracture;
453
+ const rows = t.rows;
454
+ for (let r = 0; r < rows; r++) {
455
+ const lambda = fStiff[r] === Number.POSITIVE_INFINITY ? fLambda[r] : 0;
456
+ const kC = fPenalty[r] * fC[r] + lambda;
457
+ const lo = fMin[r];
458
+ const hi = fMax[r];
459
+ // Two clamps: user-supplied `[lambdaMin, lambdaMax]` (one-sided
460
+ // for inequalities), and the unconditional `±LAMBDA_MAX` to
461
+ // prevent runaway under infeasibility — see term.ts header.
462
+ let newLambda = kC < lo ? lo : kC > hi ? hi : kC;
463
+ if (newLambda > LAMBDA_MAX)
464
+ newLambda = LAMBDA_MAX;
465
+ else if (newLambda < -LAMBDA_MAX)
466
+ newLambda = -LAMBDA_MAX;
467
+ fLambda[r] = newLambda;
468
+ const absLambda = newLambda < 0 ? -newLambda : newLambda;
469
+ if (absLambda >= fFracture[r]) {
470
+ t.dispose();
471
+ break;
472
+ }
473
+ if (newLambda > lo && newLambda < hi) {
474
+ const stiff = fStiff[r];
475
+ const cap = stiff < PENALTY_MAX ? stiff : PENALTY_MAX;
476
+ const absC = fC[r] < 0 ? -fC[r] : fC[r];
477
+ const next = fPenalty[r] + beta * absC;
478
+ fPenalty[r] = next > cap ? cap : next;
479
+ }
480
+ }
481
+ }
482
+ }
483
+ _growCellArrays() {
484
+ const newLen = this.dims.length * 2;
485
+ const newDims = new Uint8Array(newLen);
486
+ const newOffsets = new Uint32Array(newLen);
487
+ newDims.set(this.dims);
488
+ newOffsets.set(this.offsets);
489
+ this.dims = newDims;
490
+ this.offsets = newOffsets;
491
+ }
492
+ _growScalarBuffers(needed) {
493
+ let cap = this._capacity || 1;
494
+ while (cap < this._totalDof + needed)
495
+ cap *= 2;
496
+ const newPositions = new Float64Array(cap);
497
+ const newInitials = new Float64Array(cap);
498
+ const newAnchors = new Float64Array(cap);
499
+ const newMasses = new Float64Array(cap);
500
+ newPositions.set(this.positions);
501
+ newInitials.set(this.initials);
502
+ newAnchors.set(this.anchors);
503
+ newMasses.set(this.masses);
504
+ this.positions = newPositions;
505
+ this.initials = newInitials;
506
+ this.anchors = newAnchors;
507
+ this.masses = newMasses;
508
+ this._capacity = cap;
509
+ }
510
+ }
@@ -0,0 +1,50 @@
1
+ import type { Solver } from "./solver.js";
2
+ export declare const PENALTY_MIN = 1;
3
+ export declare const PENALTY_MAX = 1000000000;
4
+ /** Hard symmetric cap on the per-row multiplier `λ`. Without it the
5
+ * dual update grows unboundedly while a constraint stays infeasible
6
+ * (e.g. a joint dragged outside its workspace), blowing positions to
7
+ * infinity. Capping at `1e9` saturates instead of exploding;
8
+ * symmetric so equalities stay reachable from either side. */
9
+ export declare const LAMBDA_MAX = 1000000000;
10
+ export declare abstract class Term {
11
+ readonly solver: Solver;
12
+ /** Bound cell ids in subclass order; `cells[ci]` is cell-index ci. */
13
+ readonly cells: readonly number[];
14
+ /** Per-cell-index offset into `solver.positions`; cached (offsets are
15
+ * append-only). */
16
+ readonly cellOffsets: readonly number[];
17
+ readonly cellDims: readonly number[];
18
+ readonly rows: number;
19
+ /** Current constraint values; filled by `computeConstraint`. */
20
+ readonly C: Float64Array;
21
+ /** Constraint values at start of step (hard-constraint stabilisation). */
22
+ readonly C0: Float64Array;
23
+ /** Per-row stiffness; `Infinity` = hard (augmented-Lagrangian path). */
24
+ readonly stiffness: Float64Array;
25
+ /** Per-row lower bound on λ (default `-Infinity`); one-sided
26
+ * constraints and friction-cone clamps. */
27
+ readonly lambdaMin: Float64Array;
28
+ readonly lambdaMax: Float64Array;
29
+ /** Fracture threshold: `|λ| > fracture` disables the term. */
30
+ readonly fracture: Float64Array;
31
+ /** Current penalty (warm-started, ramped via β). */
32
+ readonly penalty: Float64Array;
33
+ /** Lagrange multiplier for hard constraints (soft uses 0). */
34
+ readonly lambda: Float64Array;
35
+ /** Removal flag, honoured at the next `prepare()`. Set by
36
+ * `dispose()` or by fracture in `_dualPass`. */
37
+ disabled: boolean;
38
+ /** Per-cell-index Jacobian; `J[ci]` is `rows × dim` row-major. */
39
+ readonly J: Float64Array[];
40
+ /** Per-cell-index Hessian column norms; same shape as `J[ci]`. */
41
+ readonly HCols: Float64Array[];
42
+ constructor(solver: Solver, cells: readonly number[], rows: number);
43
+ abstract initialize(): boolean;
44
+ abstract computeConstraint(alpha: number): void;
45
+ abstract computeDerivatives(cellIdx: number): void;
46
+ /** Mark for removal; takes effect on the next solver pass. */
47
+ dispose(): void;
48
+ /** True iff row is hard (`stiffness === Infinity`). */
49
+ isHard(row: number): boolean;
50
+ }