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,835 @@
1
+ // rigid.ts — 2D rigid-body relations + box-box collision.
2
+ //
3
+ // A rigid body is a 3-DOF cell `(x, y, θ)` with diagonal mass
4
+ // `(m, m, I)`. Geometry and broadphase bookkeeping live on a `Body`
5
+ // wrapper. Box-box collisions generate a `BoxContact` term with
6
+ // normal + tangential rows and feature-pair tracking for static
7
+ // friction.
8
+ //
9
+ // `Body`, `Joint`, and `BodyAnchor` are `Relation`s — add via
10
+ // `c.add(...)`, typically inside a `world()` factory.
11
+ //
12
+ // SAT collision is a port of Box2D-Lite's box-box collide (MIT,
13
+ // Erin Catto), as used by the AVBD 2D reference. See:
14
+ // https://github.com/savant117/avbd-demo2d/blob/main/source/collide.cpp
15
+ import { isCell, Num, num as numSig, pose as poseSig, Vec, vec, } from "../core/index.js";
16
+ import { Term } from "./term.js";
17
+ const COLLISION_MARGIN = 0.0005;
18
+ const STICK_THRESH = 0.01;
19
+ /** A 2D rigid body (a `Relation`; add via `world.add(body)`).
20
+ *
21
+ * Pose is one `Pose` signal; `position` (Vec) and `angle` (Num) are
22
+ * lenses on it for consumers wanting a narrower view. `cellId` is the
23
+ * solver cell after `bind` (-1 before). */
24
+ export class Body {
25
+ w;
26
+ h;
27
+ mass;
28
+ moment;
29
+ friction;
30
+ /** Bounding radius for the broadphase. */
31
+ radius;
32
+ /** Reactive pose — single source of truth, updated each tick. */
33
+ pose;
34
+ /** Vec lens on `pose` (xy); writes propagate back through `pose`. */
35
+ position;
36
+ /** Num lens on `pose` (theta). */
37
+ angle;
38
+ /** Solver cell id; -1 until `bind` runs. */
39
+ cellId = -1;
40
+ /** @internal — set on bind so force code can read solver state. */
41
+ _solver;
42
+ constructor(opts, init) {
43
+ this.w = opts.size.w;
44
+ this.h = opts.size.h;
45
+ const density = opts.density ?? 1;
46
+ const area = this.w * this.h;
47
+ this.mass = density === 0 ? 0 : area * density;
48
+ this.moment = density === 0 ? 0 : (this.mass * (this.w * this.w + this.h * this.h)) / 12;
49
+ this.friction = opts.friction ?? 0.5;
50
+ this.radius = Math.hypot(this.w * 0.5, this.h * 0.5);
51
+ this.pose = poseSig({ x: init.x, y: init.y, theta: init.theta ?? 0 });
52
+ // Position lens — Vec view of (pose.x, pose.y); writes preserve theta.
53
+ this.position = Vec.lens(this.pose, p => ({ x: p.x, y: p.y }), (v, p) => ({ x: v.x, y: v.y, theta: p.theta }));
54
+ // Angle lens — Num view of pose.theta; writes preserve xy.
55
+ this.angle = Num.lens(this.pose, p => p.theta, (t, p) => ({ x: p.x, y: p.y, theta: t }));
56
+ }
57
+ bind(c) {
58
+ if (this.cellId >= 0)
59
+ throw new Error("Body: already bound");
60
+ this.cellId = c._bind(this.pose);
61
+ this._solver = c.solver;
62
+ if (this.mass === 0)
63
+ c.solver.setMass(this.cellId, 0);
64
+ else
65
+ c.solver.setMassDiag(this.cellId, [this.mass, this.mass, this.moment]);
66
+ return () => {
67
+ // Solver cells are append-only; can't free. Mark kinematic and
68
+ // forget cellId so the body could be re-bound elsewhere.
69
+ c.solver.setMass(this.cellId, 0);
70
+ this.cellId = -1;
71
+ this._solver = undefined;
72
+ };
73
+ }
74
+ /** Relation that pins this body (mass → 0, kinematic) while
75
+ * attached; restores `(m, m, I)` on detach. Composes with
76
+ * `addWhile` for drag. The pose still updates from
77
+ * `body.position` / `body.angle` writes. */
78
+ pin() {
79
+ const body = this;
80
+ return {
81
+ bind(c) {
82
+ if (body.cellId < 0)
83
+ throw new Error("body.pin: add the body first");
84
+ const wasStatic = body.mass === 0;
85
+ c.solver.setMass(body.cellId, 0);
86
+ return () => {
87
+ if (!wasStatic) {
88
+ c.solver.setMassDiag(body.cellId, [body.mass, body.mass, body.moment]);
89
+ }
90
+ };
91
+ },
92
+ };
93
+ }
94
+ }
95
+ /** Create a rigid body (add via `world.add`). `density: 0` is static
96
+ * (mass 0, kinematic cell). */
97
+ export function body(opts, init) {
98
+ return new Body(opts, init);
99
+ }
100
+ /** @internal — Read live solver position into `out`. The `pose`
101
+ * signal holds the start-of-tick value; the live value during
102
+ * iteration is in `solver.positions`. For inner-loop use. */
103
+ function readPose(solver, cellId, out) {
104
+ const off = solver.offsets[cellId];
105
+ const p = solver.positions;
106
+ out.x = p[off];
107
+ out.y = p[off + 1];
108
+ out.theta = p[off + 2];
109
+ }
110
+ /** Edge identifiers for feature-pair tracking. */
111
+ var Edge;
112
+ (function (Edge) {
113
+ Edge[Edge["None"] = 0] = "None";
114
+ Edge[Edge["E1"] = 1] = "E1";
115
+ Edge[Edge["E2"] = 2] = "E2";
116
+ Edge[Edge["E3"] = 3] = "E3";
117
+ Edge[Edge["E4"] = 4] = "E4";
118
+ })(Edge || (Edge = {}));
119
+ var Axis;
120
+ (function (Axis) {
121
+ Axis[Axis["FaceAX"] = 0] = "FaceAX";
122
+ Axis[Axis["FaceAY"] = 1] = "FaceAY";
123
+ Axis[Axis["FaceBX"] = 2] = "FaceBX";
124
+ Axis[Axis["FaceBY"] = 3] = "FaceBY";
125
+ })(Axis || (Axis = {}));
126
+ function makeFP(inE1, outE1, inE2, outE2) {
127
+ return (inE1 & 0xff) | ((outE1 & 0xff) << 8) | ((inE2 & 0xff) << 16) | ((outE2 & 0xff) << 24);
128
+ }
129
+ function flipFP(fp) {
130
+ // Swap (inE1, outE1) with (inE2, outE2).
131
+ const inE1 = fp & 0xff;
132
+ const outE1 = (fp >> 8) & 0xff;
133
+ const inE2 = (fp >> 16) & 0xff;
134
+ const outE2 = (fp >> 24) & 0xff;
135
+ return makeFP(inE2, outE2, inE1, outE1);
136
+ }
137
+ /** Clip segment `vIn` to the half-plane `n · v ≤ offset`. A bisected
138
+ * segment's new vertex inherits the clipped endpoint's feature pair,
139
+ * overwriting edge-1 with `clipEdge` per Box2D-Lite. Stable feature
140
+ * pairs are what let penalty / λ warm-start through sliding contacts. */
141
+ function clipSegmentToLine(vOut, vIn, nx, ny, offset, clipEdge) {
142
+ let n = 0;
143
+ const d0 = nx * vIn[0].x + ny * vIn[0].y - offset;
144
+ const d1 = nx * vIn[1].x + ny * vIn[1].y - offset;
145
+ if (d0 <= 0)
146
+ vOut[n++] = vIn[0];
147
+ if (d1 <= 0)
148
+ vOut[n++] = vIn[1];
149
+ if (d0 * d1 < 0) {
150
+ const t = d0 / (d0 - d1);
151
+ const x = vIn[0].x + (vIn[1].x - vIn[0].x) * t;
152
+ const y = vIn[0].y + (vIn[1].y - vIn[0].y) * t;
153
+ let fp;
154
+ if (d0 > 0) {
155
+ // vIn[0] was on the wrong side. Inherit fp from vIn[0] but
156
+ // overwrite inEdge1 with clipEdge and clear inEdge2.
157
+ const src = vIn[0].fp;
158
+ const outE1 = (src >> 8) & 0xff;
159
+ const outE2 = (src >> 24) & 0xff;
160
+ fp = makeFP(clipEdge, outE1, Edge.None, outE2);
161
+ }
162
+ else {
163
+ // vIn[1] was on the wrong side. Inherit fp from vIn[1] but
164
+ // overwrite outEdge1 with clipEdge and clear outEdge2.
165
+ const src = vIn[1].fp;
166
+ const inE1 = src & 0xff;
167
+ const inE2 = (src >> 16) & 0xff;
168
+ fp = makeFP(inE1, clipEdge, inE2, Edge.None);
169
+ }
170
+ vOut[n++] = { x, y, fp };
171
+ }
172
+ return n;
173
+ }
174
+ /** Compute the incident edge of box B that opposes the chosen face
175
+ * of box A. Output two clip vertices in world space. */
176
+ function computeIncidentEdge(c, hx, hy, posX, posY, cosT, sinT, nx, ny) {
177
+ // Convert normal to incident-frame and flip.
178
+ const ln = -(cosT * nx + sinT * ny);
179
+ const lny = -(-sinT * nx + cosT * ny);
180
+ const absX = Math.abs(ln);
181
+ const absY = Math.abs(lny);
182
+ let v0x, v0y, v1x, v1y;
183
+ let fp0, fp1;
184
+ if (absX > absY) {
185
+ if (ln > 0) {
186
+ v0x = hx;
187
+ v0y = -hy;
188
+ v1x = hx;
189
+ v1y = hy;
190
+ fp0 = makeFP(0, 0, Edge.E3, Edge.E4);
191
+ fp1 = makeFP(0, 0, Edge.E4, Edge.E1);
192
+ }
193
+ else {
194
+ v0x = -hx;
195
+ v0y = hy;
196
+ v1x = -hx;
197
+ v1y = -hy;
198
+ fp0 = makeFP(0, 0, Edge.E1, Edge.E2);
199
+ fp1 = makeFP(0, 0, Edge.E2, Edge.E3);
200
+ }
201
+ }
202
+ else {
203
+ if (lny > 0) {
204
+ v0x = hx;
205
+ v0y = hy;
206
+ v1x = -hx;
207
+ v1y = hy;
208
+ fp0 = makeFP(0, 0, Edge.E4, Edge.E1);
209
+ fp1 = makeFP(0, 0, Edge.E1, Edge.E2);
210
+ }
211
+ else {
212
+ v0x = -hx;
213
+ v0y = -hy;
214
+ v1x = hx;
215
+ v1y = -hy;
216
+ fp0 = makeFP(0, 0, Edge.E2, Edge.E3);
217
+ fp1 = makeFP(0, 0, Edge.E3, Edge.E4);
218
+ }
219
+ }
220
+ c[0].x = posX + cosT * v0x - sinT * v0y;
221
+ c[0].y = posY + sinT * v0x + cosT * v0y;
222
+ c[0].fp = fp0;
223
+ c[1].x = posX + cosT * v1x - sinT * v1y;
224
+ c[1].y = posY + sinT * v1x + cosT * v1y;
225
+ c[1].fp = fp1;
226
+ }
227
+ function makeContact() {
228
+ return { rAx: 0, rAy: 0, rBx: 0, rBy: 0, nx: 0, ny: 0, fp: 0, stick: false };
229
+ }
230
+ /** Collide two oriented boxes. Returns the number of contacts (0–2)
231
+ * written into `out`. Direct port of Box2D-Lite (Catto, MIT). */
232
+ function collideBoxes(out, posA, hA, posB, hB) {
233
+ const hAx = hA.w * 0.5;
234
+ const hAy = hA.h * 0.5;
235
+ const hBx = hB.w * 0.5;
236
+ const hBy = hB.h * 0.5;
237
+ const cA = Math.cos(posA.theta);
238
+ const sA = Math.sin(posA.theta);
239
+ const cB = Math.cos(posB.theta);
240
+ const sB = Math.sin(posB.theta);
241
+ // dA = RotAᵀ · (posB - posA), dB = RotBᵀ · (posB - posA)
242
+ const dpx = posB.x - posA.x;
243
+ const dpy = posB.y - posA.y;
244
+ const dAx = cA * dpx + sA * dpy;
245
+ const dAy = -sA * dpx + cA * dpy;
246
+ const dBx = cB * dpx + sB * dpy;
247
+ const dBy = -sB * dpx + cB * dpy;
248
+ // C = RotAᵀ · RotB (relative rotation).
249
+ const c00 = cA * cB + sA * sB;
250
+ const c01 = -cA * sB + sA * cB;
251
+ const c10 = -sA * cB + cA * sB;
252
+ const c11 = sA * sB + cA * cB;
253
+ const ac00 = Math.abs(c00);
254
+ const ac01 = Math.abs(c01);
255
+ const ac10 = Math.abs(c10);
256
+ const ac11 = Math.abs(c11);
257
+ // Box A faces: |dA| − hA − |C| · hB
258
+ const faceAX = Math.abs(dAx) - hAx - (ac00 * hBx + ac01 * hBy);
259
+ const faceAY = Math.abs(dAy) - hAy - (ac10 * hBx + ac11 * hBy);
260
+ if (faceAX > 0 || faceAY > 0)
261
+ return 0;
262
+ // Box B faces: |dB| − |Cᵀ| · hA − hB
263
+ const faceBX = Math.abs(dBx) - (ac00 * hAx + ac10 * hAy) - hBx;
264
+ const faceBY = Math.abs(dBy) - (ac01 * hAx + ac11 * hAy) - hBy;
265
+ if (faceBX > 0 || faceBY > 0)
266
+ return 0;
267
+ // Pick the best axis with bias toward A (relative + absolute tol).
268
+ let axis = Axis.FaceAX;
269
+ let separation = faceAX;
270
+ let nx = dAx > 0 ? cA : -cA;
271
+ let ny = dAx > 0 ? sA : -sA;
272
+ const RT = 0.95;
273
+ const AT = 0.01;
274
+ if (faceAY > RT * separation + AT * hAy) {
275
+ axis = Axis.FaceAY;
276
+ separation = faceAY;
277
+ nx = dAy > 0 ? -sA : sA;
278
+ ny = dAy > 0 ? cA : -cA;
279
+ }
280
+ if (faceBX > RT * separation + AT * hBx) {
281
+ axis = Axis.FaceBX;
282
+ separation = faceBX;
283
+ nx = dBx > 0 ? cB : -cB;
284
+ ny = dBx > 0 ? sB : -sB;
285
+ }
286
+ if (faceBY > RT * separation + AT * hBy) {
287
+ axis = Axis.FaceBY;
288
+ separation = faceBY;
289
+ nx = dBy > 0 ? -sB : sB;
290
+ ny = dBy > 0 ? cB : -cB;
291
+ }
292
+ let frontNx, frontNy;
293
+ let sideNx, sideNy;
294
+ let front;
295
+ let negSide, posSide;
296
+ let negEdge, posEdge;
297
+ const incidentEdge = [
298
+ { x: 0, y: 0, fp: 0 },
299
+ { x: 0, y: 0, fp: 0 },
300
+ ];
301
+ if (axis === Axis.FaceAX) {
302
+ frontNx = nx;
303
+ frontNy = ny;
304
+ front = posA.x * frontNx + posA.y * frontNy + hAx;
305
+ sideNx = -sA;
306
+ sideNy = cA;
307
+ const side = posA.x * sideNx + posA.y * sideNy;
308
+ negSide = -side + hAy;
309
+ posSide = side + hAy;
310
+ negEdge = Edge.E3;
311
+ posEdge = Edge.E1;
312
+ computeIncidentEdge(incidentEdge, hBx, hBy, posB.x, posB.y, cB, sB, frontNx, frontNy);
313
+ }
314
+ else if (axis === Axis.FaceAY) {
315
+ frontNx = nx;
316
+ frontNy = ny;
317
+ front = posA.x * frontNx + posA.y * frontNy + hAy;
318
+ sideNx = cA;
319
+ sideNy = sA;
320
+ const side = posA.x * sideNx + posA.y * sideNy;
321
+ negSide = -side + hAx;
322
+ posSide = side + hAx;
323
+ negEdge = Edge.E2;
324
+ posEdge = Edge.E4;
325
+ computeIncidentEdge(incidentEdge, hBx, hBy, posB.x, posB.y, cB, sB, frontNx, frontNy);
326
+ }
327
+ else if (axis === Axis.FaceBX) {
328
+ frontNx = -nx;
329
+ frontNy = -ny;
330
+ front = posB.x * frontNx + posB.y * frontNy + hBx;
331
+ sideNx = -sB;
332
+ sideNy = cB;
333
+ const side = posB.x * sideNx + posB.y * sideNy;
334
+ negSide = -side + hBy;
335
+ posSide = side + hBy;
336
+ negEdge = Edge.E3;
337
+ posEdge = Edge.E1;
338
+ computeIncidentEdge(incidentEdge, hAx, hAy, posA.x, posA.y, cA, sA, frontNx, frontNy);
339
+ }
340
+ else {
341
+ frontNx = -nx;
342
+ frontNy = -ny;
343
+ front = posB.x * frontNx + posB.y * frontNy + hBy;
344
+ sideNx = cB;
345
+ sideNy = sB;
346
+ const side = posB.x * sideNx + posB.y * sideNy;
347
+ negSide = -side + hBx;
348
+ posSide = side + hBx;
349
+ negEdge = Edge.E2;
350
+ posEdge = Edge.E4;
351
+ computeIncidentEdge(incidentEdge, hAx, hAy, posA.x, posA.y, cA, sA, frontNx, frontNy);
352
+ }
353
+ // Clip the incident edge to the side planes.
354
+ const clip1 = [
355
+ { x: 0, y: 0, fp: 0 },
356
+ { x: 0, y: 0, fp: 0 },
357
+ ];
358
+ const clip2 = [
359
+ { x: 0, y: 0, fp: 0 },
360
+ { x: 0, y: 0, fp: 0 },
361
+ ];
362
+ let np = clipSegmentToLine(clip1, incidentEdge, -sideNx, -sideNy, negSide, negEdge);
363
+ if (np < 2)
364
+ return 0;
365
+ np = clipSegmentToLine(clip2, clip1, sideNx, sideNy, posSide, posEdge);
366
+ if (np < 2)
367
+ return 0;
368
+ // Convert clipped points to contacts.
369
+ let count = 0;
370
+ for (let i = 0; i < 2; i++) {
371
+ const cp = clip2[i];
372
+ const sep = frontNx * cp.x + frontNy * cp.y - front;
373
+ if (sep <= 0) {
374
+ const c = out[count];
375
+ c.nx = -nx;
376
+ c.ny = -ny;
377
+ // rA = RotAᵀ · (cp − frontN·sep − posA), rB = RotBᵀ · (cp − posB)
378
+ // For B-faces, swap rA and rB.
379
+ const useBFace = axis === Axis.FaceBX || axis === Axis.FaceBY;
380
+ const cpAdjX = cp.x - (useBFace ? 0 : frontNx * sep);
381
+ const cpAdjY = cp.y - (useBFace ? 0 : frontNy * sep);
382
+ const cpAdjBX = cp.x - (useBFace ? frontNx * sep : 0);
383
+ const cpAdjBY = cp.y - (useBFace ? frontNy * sep : 0);
384
+ const dAxRel = cpAdjX - posA.x;
385
+ const dAyRel = cpAdjY - posA.y;
386
+ c.rAx = cA * dAxRel + sA * dAyRel;
387
+ c.rAy = -sA * dAxRel + cA * dAyRel;
388
+ const dBxRel = cpAdjBX - posB.x;
389
+ const dByRel = cpAdjBY - posB.y;
390
+ c.rBx = cB * dBxRel + sB * dByRel;
391
+ c.rBy = -sB * dBxRel + cB * dByRel;
392
+ c.fp = useBFace ? flipFP(cp.fp) : cp.fp;
393
+ count++;
394
+ }
395
+ }
396
+ return count;
397
+ }
398
+ const SCRATCH_CONTACTS = [makeContact(), makeContact()];
399
+ /** Up to 2 contacts × (normal + tangent) = 4 rows. Coulomb friction
400
+ * via clamping tangential `λ` to the cone `±μ·|λ_normal|` (updated
401
+ * per iteration). Truncated Taylor at `x⁻` (paper §4), so `J` is
402
+ * precomputed in `initialize` and copied out by `computeDerivatives`. */
403
+ export class BoxContact extends Term {
404
+ bodyA;
405
+ bodyB;
406
+ numContacts = 0;
407
+ contacts = [makeContact(), makeContact()];
408
+ // Precomputed Jacobians for the 4 possible rows.
409
+ // Each is a 3-vector (∂C/∂x, ∂C/∂y, ∂C/∂θ) per body.
410
+ JAn; // normal Jacobians on A (3 per contact)
411
+ JBn; // normal Jacobians on B
412
+ JAt; // tangential Jacobians on A
413
+ JBt; // tangential Jacobians on B
414
+ C0n; // normal C0 per contact
415
+ C0t; // tangential C0 per contact
416
+ friction = 0;
417
+ // Scratch for live-buffer pose reads (alloc-free hot path).
418
+ _poseA = { x: 0, y: 0, theta: 0 };
419
+ _poseB = { x: 0, y: 0, theta: 0 };
420
+ constructor(solver, bodyA, bodyB) {
421
+ super(solver, [bodyA.cellId, bodyB.cellId], 4);
422
+ this.bodyA = bodyA;
423
+ this.bodyB = bodyB;
424
+ this.JAn = new Float64Array(6); // 2 contacts × 3 components
425
+ this.JBn = new Float64Array(6);
426
+ this.JAt = new Float64Array(6);
427
+ this.JBt = new Float64Array(6);
428
+ this.C0n = new Float64Array(2);
429
+ this.C0t = new Float64Array(2);
430
+ // Normal rows: lambda ≤ 0 (push apart). fmin = -∞, fmax = 0.
431
+ this.lambdaMax[0] = 0;
432
+ this.lambdaMax[2] = 0;
433
+ // Tangential rows: friction cone is set per-iteration in computeConstraint.
434
+ }
435
+ initialize() {
436
+ this.friction = Math.sqrt(this.bodyA.friction * this.bodyB.friction);
437
+ // Stash old contact state for warm-starting.
438
+ const oldContacts = [{ ...this.contacts[0] }, { ...this.contacts[1] }];
439
+ const oldNumContacts = this.numContacts;
440
+ const oldPenalty = [this.penalty[0], this.penalty[1], this.penalty[2], this.penalty[3]];
441
+ const oldLambda = [this.lambda[0], this.lambda[1], this.lambda[2], this.lambda[3]];
442
+ const oldStick = [oldContacts[0].stick, oldContacts[1].stick];
443
+ // Re-collide. Returning `true` with zero contacts keeps the
444
+ // manifold registered so warm-start survives a brief separation;
445
+ // the rows are zeroed in `computeConstraint`, so it contributes
446
+ // nothing while inactive.
447
+ readPose(this.solver, this.bodyA.cellId, this._poseA);
448
+ readPose(this.solver, this.bodyB.cellId, this._poseB);
449
+ const numNew = collideBoxes(SCRATCH_CONTACTS, this._poseA, this.bodyA, this._poseB, this.bodyB);
450
+ this.numContacts = numNew;
451
+ if (numNew === 0) {
452
+ for (let r = 0; r < 4; r++) {
453
+ this.lambda[r] = 0;
454
+ this.penalty[r] = 0;
455
+ }
456
+ return true;
457
+ }
458
+ // Copy fresh contacts and merge old penalty/lambda by feature ID.
459
+ for (let i = 0; i < numNew; i++) {
460
+ const src = SCRATCH_CONTACTS[i];
461
+ const dst = this.contacts[i];
462
+ dst.rAx = src.rAx;
463
+ dst.rAy = src.rAy;
464
+ dst.rBx = src.rBx;
465
+ dst.rBy = src.rBy;
466
+ dst.nx = src.nx;
467
+ dst.ny = src.ny;
468
+ dst.fp = src.fp;
469
+ dst.stick = false;
470
+ this.penalty[i * 2 + 0] = 0;
471
+ this.penalty[i * 2 + 1] = 0;
472
+ this.lambda[i * 2 + 0] = 0;
473
+ this.lambda[i * 2 + 1] = 0;
474
+ for (let j = 0; j < oldNumContacts; j++) {
475
+ if (oldContacts[j].fp === src.fp) {
476
+ this.penalty[i * 2 + 0] = oldPenalty[j * 2 + 0];
477
+ this.penalty[i * 2 + 1] = oldPenalty[j * 2 + 1];
478
+ this.lambda[i * 2 + 0] = oldLambda[j * 2 + 0];
479
+ this.lambda[i * 2 + 1] = oldLambda[j * 2 + 1];
480
+ dst.stick = oldStick[j];
481
+ if (oldStick[j]) {
482
+ // Static friction: keep the old anchor point.
483
+ dst.rAx = oldContacts[j].rAx;
484
+ dst.rAy = oldContacts[j].rAy;
485
+ dst.rBx = oldContacts[j].rBx;
486
+ dst.rBy = oldContacts[j].rBy;
487
+ }
488
+ break;
489
+ }
490
+ }
491
+ }
492
+ // Precompute Jacobians and C0 for each contact row. Reuse the
493
+ // scratch poses we read just above.
494
+ const poseA = this._poseA;
495
+ const poseB = this._poseB;
496
+ const cA = Math.cos(poseA.theta);
497
+ const sA = Math.sin(poseA.theta);
498
+ const cB = Math.cos(poseB.theta);
499
+ const sB = Math.sin(poseB.theta);
500
+ for (let i = 0; i < numNew; i++) {
501
+ const con = this.contacts[i];
502
+ const tx = con.ny;
503
+ const ty = -con.nx;
504
+ // World-frame anchor offsets.
505
+ const rAWx = cA * con.rAx - sA * con.rAy;
506
+ const rAWy = sA * con.rAx + cA * con.rAy;
507
+ const rBWx = cB * con.rBx - sB * con.rBy;
508
+ const rBWy = sB * con.rBx + cB * con.rBy;
509
+ // Normal row: penetration constraint.
510
+ this.JAn[i * 3 + 0] = con.nx;
511
+ this.JAn[i * 3 + 1] = con.ny;
512
+ this.JAn[i * 3 + 2] = rAWx * con.ny - rAWy * con.nx; // cross(rAW, n)
513
+ this.JBn[i * 3 + 0] = -con.nx;
514
+ this.JBn[i * 3 + 1] = -con.ny;
515
+ this.JBn[i * 3 + 2] = -(rBWx * con.ny - rBWy * con.nx);
516
+ // Tangent row: friction.
517
+ this.JAt[i * 3 + 0] = tx;
518
+ this.JAt[i * 3 + 1] = ty;
519
+ this.JAt[i * 3 + 2] = rAWx * ty - rAWy * tx;
520
+ this.JBt[i * 3 + 0] = -tx;
521
+ this.JBt[i * 3 + 1] = -ty;
522
+ this.JBt[i * 3 + 2] = -(rBWx * ty - rBWy * tx);
523
+ // C0 = (basis · ((posA + rAW) − (posB + rBW))) + margin (normal only).
524
+ const dpx = poseA.x + rAWx - poseB.x - rBWx;
525
+ const dpy = poseA.y + rAWy - poseB.y - rBWy;
526
+ this.C0n[i] = con.nx * dpx + con.ny * dpy + COLLISION_MARGIN;
527
+ this.C0t[i] = tx * dpx + ty * dpy;
528
+ }
529
+ return true;
530
+ }
531
+ computeConstraint(alpha) {
532
+ // Truncated Taylor: C(x) ≈ C0(1 − α) + J · Δp.
533
+ const offA = this.solver.offsets[this.bodyA.cellId];
534
+ const offB = this.solver.offsets[this.bodyB.cellId];
535
+ const initials = this.solver.initials;
536
+ const positions = this.solver.positions;
537
+ const dApx = positions[offA] - initials[offA];
538
+ const dApy = positions[offA + 1] - initials[offA + 1];
539
+ const dApt = positions[offA + 2] - initials[offA + 2];
540
+ const dBpx = positions[offB] - initials[offB];
541
+ const dBpy = positions[offB + 1] - initials[offB + 1];
542
+ const dBpt = positions[offB + 2] - initials[offB + 2];
543
+ for (let i = 0; i < this.numContacts; i++) {
544
+ const dn = this.JAn[i * 3 + 0] * dApx +
545
+ this.JAn[i * 3 + 1] * dApy +
546
+ this.JAn[i * 3 + 2] * dApt +
547
+ this.JBn[i * 3 + 0] * dBpx +
548
+ this.JBn[i * 3 + 1] * dBpy +
549
+ this.JBn[i * 3 + 2] * dBpt;
550
+ const dt = this.JAt[i * 3 + 0] * dApx +
551
+ this.JAt[i * 3 + 1] * dApy +
552
+ this.JAt[i * 3 + 2] * dApt +
553
+ this.JBt[i * 3 + 0] * dBpx +
554
+ this.JBt[i * 3 + 1] * dBpy +
555
+ this.JBt[i * 3 + 2] * dBpt;
556
+ this.C[i * 2 + 0] = this.C0n[i] * (1 - alpha) + dn;
557
+ this.C[i * 2 + 1] = this.C0t[i] * (1 - alpha) + dt;
558
+ // Update friction cone from current normal lambda.
559
+ const bound = Math.abs(this.lambda[i * 2 + 0]) * this.friction;
560
+ this.lambdaMax[i * 2 + 1] = bound;
561
+ this.lambdaMin[i * 2 + 1] = -bound;
562
+ // Sticking detection for static friction next frame.
563
+ const con = this.contacts[i];
564
+ con.stick =
565
+ Math.abs(this.lambda[i * 2 + 1]) < bound && Math.abs(this.C0t[i]) < STICK_THRESH;
566
+ }
567
+ // Zero out unused rows so they contribute nothing to the dual update.
568
+ for (let i = this.numContacts; i < 2; i++) {
569
+ this.C[i * 2 + 0] = 0;
570
+ this.C[i * 2 + 1] = 0;
571
+ }
572
+ }
573
+ computeDerivatives(cellIdx) {
574
+ // Just copy the precomputed Jacobians (already in 3-component form).
575
+ const J = this.J[cellIdx];
576
+ if (cellIdx === 0) {
577
+ for (let i = 0; i < this.numContacts; i++) {
578
+ J[i * 2 * 3 + 0] = this.JAn[i * 3 + 0];
579
+ J[i * 2 * 3 + 1] = this.JAn[i * 3 + 1];
580
+ J[i * 2 * 3 + 2] = this.JAn[i * 3 + 2];
581
+ J[i * 2 * 3 + 3] = this.JAt[i * 3 + 0];
582
+ J[i * 2 * 3 + 4] = this.JAt[i * 3 + 1];
583
+ J[i * 2 * 3 + 5] = this.JAt[i * 3 + 2];
584
+ }
585
+ }
586
+ else {
587
+ for (let i = 0; i < this.numContacts; i++) {
588
+ J[i * 2 * 3 + 0] = this.JBn[i * 3 + 0];
589
+ J[i * 2 * 3 + 1] = this.JBn[i * 3 + 1];
590
+ J[i * 2 * 3 + 2] = this.JBn[i * 3 + 2];
591
+ J[i * 2 * 3 + 3] = this.JBt[i * 3 + 0];
592
+ J[i * 2 * 3 + 4] = this.JBt[i * 3 + 1];
593
+ J[i * 2 * 3 + 5] = this.JBt[i * 3 + 2];
594
+ }
595
+ }
596
+ // Clear unused rows.
597
+ for (let i = this.numContacts; i < 2; i++) {
598
+ for (let k = 0; k < 6; k++)
599
+ J[i * 2 * 3 + k] = 0;
600
+ }
601
+ }
602
+ }
603
+ /** Internal term for a revolute joint (anchors `rA` on `bodyA`, `rB`
604
+ * on `bodyB`). Default: hard position rows, free angle (hinge);
605
+ * override via `JointStiffness`. Port of the AVBD 2D `Joint`. User
606
+ * code uses the `Joint` Relation via `world.add(joint(...))`. */
607
+ export class JointTerm extends Term {
608
+ bodyA;
609
+ bodyB;
610
+ rAx;
611
+ rAy;
612
+ rBx;
613
+ rBy;
614
+ torqueArm;
615
+ restAngle;
616
+ // Cached anchor-rotation values per body (refreshed each iter).
617
+ _Cn = new Float64Array(3);
618
+ _C0Cache = new Float64Array(3);
619
+ _poseA = { x: 0, y: 0, theta: 0 };
620
+ _poseB = { x: 0, y: 0, theta: 0 };
621
+ constructor(solver, bodyA, bodyB, rA, rB, opts = {}) {
622
+ super(solver, [bodyA.cellId, bodyB.cellId], 3);
623
+ this.bodyA = bodyA;
624
+ this.bodyB = bodyB;
625
+ this.rAx = rA.x;
626
+ this.rAy = rA.y;
627
+ this.rBx = rB.x;
628
+ this.rBy = rB.y;
629
+ this.stiffness[0] = opts.x ?? Number.POSITIVE_INFINITY;
630
+ this.stiffness[1] = opts.y ?? Number.POSITIVE_INFINITY;
631
+ this.stiffness[2] = opts.angle ?? 0;
632
+ this.restAngle = bodyA.pose.peek().theta - bodyB.pose.peek().theta;
633
+ const sumW = bodyA.w + bodyB.w;
634
+ const sumH = bodyA.h + bodyB.h;
635
+ // AVBD's torqueArm scales the angular row so its units are
636
+ // commensurate with the positional rows.
637
+ this.torqueArm = sumW * sumW + sumH * sumH;
638
+ }
639
+ initialize() {
640
+ readPose(this.solver, this.bodyA.cellId, this._poseA);
641
+ readPose(this.solver, this.bodyB.cellId, this._poseB);
642
+ const poseA = this._poseA;
643
+ const poseB = this._poseB;
644
+ const cA = Math.cos(poseA.theta);
645
+ const sA = Math.sin(poseA.theta);
646
+ const cB = Math.cos(poseB.theta);
647
+ const sB = Math.sin(poseB.theta);
648
+ const aWx = poseA.x + cA * this.rAx - sA * this.rAy;
649
+ const aWy = poseA.y + sA * this.rAx + cA * this.rAy;
650
+ const bWx = poseB.x + cB * this.rBx - sB * this.rBy;
651
+ const bWy = poseB.y + sB * this.rBx + cB * this.rBy;
652
+ this._C0Cache[0] = aWx - bWx;
653
+ this._C0Cache[1] = aWy - bWy;
654
+ this._C0Cache[2] = (poseA.theta - poseB.theta - this.restAngle) * this.torqueArm;
655
+ return this.stiffness[0] !== 0 || this.stiffness[1] !== 0 || this.stiffness[2] !== 0;
656
+ }
657
+ computeConstraint(alpha) {
658
+ readPose(this.solver, this.bodyA.cellId, this._poseA);
659
+ readPose(this.solver, this.bodyB.cellId, this._poseB);
660
+ const poseA = this._poseA;
661
+ const poseB = this._poseB;
662
+ const cA = Math.cos(poseA.theta);
663
+ const sA = Math.sin(poseA.theta);
664
+ const cB = Math.cos(poseB.theta);
665
+ const sB = Math.sin(poseB.theta);
666
+ const aWx = poseA.x + cA * this.rAx - sA * this.rAy;
667
+ const aWy = poseA.y + sA * this.rAx + cA * this.rAy;
668
+ const bWx = poseB.x + cB * this.rBx - sB * this.rBy;
669
+ const bWy = poseB.y + sB * this.rBx + cB * this.rBy;
670
+ this._Cn[0] = aWx - bWx;
671
+ this._Cn[1] = aWy - bWy;
672
+ this._Cn[2] = (poseA.theta - poseB.theta - this.restAngle) * this.torqueArm;
673
+ for (let i = 0; i < 3; i++) {
674
+ if (this.stiffness[i] === Number.POSITIVE_INFINITY) {
675
+ this.C[i] = this._Cn[i] - this.C0[i] * alpha;
676
+ }
677
+ else {
678
+ this.C[i] = this._Cn[i];
679
+ }
680
+ }
681
+ }
682
+ computeDerivatives(cellIdx) {
683
+ const J = this.J[cellIdx];
684
+ const Hcols = this.HCols[cellIdx];
685
+ const pose = cellIdx === 0 ? this._poseA : this._poseB;
686
+ const c = Math.cos(pose.theta);
687
+ const s = Math.sin(pose.theta);
688
+ const rLocalX = cellIdx === 0 ? this.rAx : this.rBx;
689
+ const rLocalY = cellIdx === 0 ? this.rAy : this.rBy;
690
+ const rWx = c * rLocalX - s * rLocalY;
691
+ const rWy = s * rLocalX + c * rLocalY;
692
+ const sign = cellIdx === 0 ? 1 : -1;
693
+ // Row 0: ∂C[0]/∂(x, y, θ)
694
+ J[0] = sign;
695
+ J[1] = 0;
696
+ J[2] = -sign * rWy;
697
+ // Row 1: ∂C[1]/∂(x, y, θ)
698
+ J[3] = 0;
699
+ J[4] = sign;
700
+ J[5] = sign * rWx;
701
+ // Row 2: angle row
702
+ J[6] = 0;
703
+ J[7] = 0;
704
+ J[8] = sign * this.torqueArm;
705
+ // Hessian column norms — only non-zero entries are at H[r][2,2].
706
+ Hcols[0] = 0;
707
+ Hcols[1] = 0;
708
+ Hcols[2] = Math.abs(rWx);
709
+ Hcols[3] = 0;
710
+ Hcols[4] = 0;
711
+ Hcols[5] = Math.abs(rWy);
712
+ Hcols[6] = 0;
713
+ Hcols[7] = 0;
714
+ Hcols[8] = 0;
715
+ }
716
+ }
717
+ /** @internal — soft 2-row term pulling a body's translation toward
718
+ * `target` with finite `stiffness` (angle DOF left free). Backs the
719
+ * `BodyAnchor` relation; see `bodyAnchor`. */
720
+ export class BodyAnchorTerm extends Term {
721
+ body;
722
+ /** World-space target signal (mutable). */
723
+ target;
724
+ /** Mutable stiffness signal — refreshed each `initialize()`. */
725
+ stiffnessSig;
726
+ constructor(solver, body, target, stiffness) {
727
+ super(solver, [body.cellId], 2);
728
+ this.body = body;
729
+ this.target = target;
730
+ this.stiffnessSig = stiffness;
731
+ }
732
+ initialize() {
733
+ const k = this.stiffnessSig.value;
734
+ this.stiffness[0] = k;
735
+ this.stiffness[1] = k;
736
+ return k > 0;
737
+ }
738
+ computeConstraint(_alpha) {
739
+ const off = this.cellOffsets[0];
740
+ const p = this.solver.positions;
741
+ const t = this.target.value;
742
+ this.C[0] = p[off] - t.x;
743
+ this.C[1] = p[off + 1] - t.y;
744
+ }
745
+ computeDerivatives(_cellIdx) {
746
+ const J = this.J[0];
747
+ const Hcols = this.HCols[0];
748
+ // ∂C[0]/∂(x, y, θ) = (1, 0, 0)
749
+ J[0] = 1;
750
+ J[1] = 0;
751
+ J[2] = 0;
752
+ // ∂C[1]/∂(x, y, θ) = (0, 1, 0)
753
+ J[3] = 0;
754
+ J[4] = 1;
755
+ J[5] = 0;
756
+ // No geometric stiffness (linear constraint).
757
+ Hcols[0] = 0;
758
+ Hcols[1] = 0;
759
+ Hcols[2] = 0;
760
+ Hcols[3] = 0;
761
+ Hcols[4] = 0;
762
+ Hcols[5] = 0;
763
+ }
764
+ }
765
+ /** Revolute or weld joint between two bodies (add via `world.add`).
766
+ * Default: hard position rows, free angle (hinge). `{ angle: Infinity }`
767
+ * welds; finite `{ x, y }` softens. `world` skips broadphase contacts
768
+ * between jointed pairs. */
769
+ export class Joint {
770
+ bodyA;
771
+ bodyB;
772
+ rA;
773
+ rB;
774
+ opts;
775
+ constructor(bodyA, bodyB, rA, rB, opts) {
776
+ this.bodyA = bodyA;
777
+ this.bodyB = bodyB;
778
+ this.rA = rA;
779
+ this.rB = rB;
780
+ this.opts = opts;
781
+ }
782
+ bind(c) {
783
+ if (this.bodyA.cellId < 0 || this.bodyB.cellId < 0) {
784
+ throw new Error("joint: add both bodies before the joint");
785
+ }
786
+ const f = new JointTerm(c.solver, this.bodyA, this.bodyB, this.rA, this.rB, this.opts);
787
+ c.solver.addTerm(f);
788
+ return () => c.solver.removeTerm(f);
789
+ }
790
+ }
791
+ export function joint(a, b, rA, rB, opts) {
792
+ return new Joint(a, b, rA, rB, opts);
793
+ }
794
+ /** Rigid weld — a joint with all rows hard; fuses two bodies while
795
+ * keeping their independent inertias. */
796
+ export function weld(a, b, rA, rB) {
797
+ return new Joint(a, b, rA, rB, {
798
+ x: Number.POSITIVE_INFINITY,
799
+ y: Number.POSITIVE_INFINITY,
800
+ angle: Number.POSITIVE_INFINITY,
801
+ });
802
+ }
803
+ /** Soft "drag" handle: pulls a body's translation toward `target`
804
+ * with finite stiffness. The body keeps its mass and reacts to
805
+ * contacts, so it lags behind the cursor when blocked rather than
806
+ * punching through. `target` and `stiffness` are mutable signals.
807
+ *
808
+ * const a = bodyAnchor(body, body.position.value, 5e4);
809
+ * world.addWhile(dragging, a);
810
+ * onPointerMove(p => a.target.value = p);
811
+ */
812
+ export class BodyAnchor {
813
+ body;
814
+ target;
815
+ stiffness;
816
+ constructor(body, target, stiffness) {
817
+ this.body = body;
818
+ this.target = isCell(target)
819
+ ? target
820
+ : vec(target.x, target.y);
821
+ this.stiffness = isCell(stiffness)
822
+ ? stiffness
823
+ : numSig(stiffness);
824
+ }
825
+ bind(c) {
826
+ if (this.body.cellId < 0)
827
+ throw new Error("bodyAnchor: add the body first");
828
+ const f = new BodyAnchorTerm(c.solver, this.body, this.target, this.stiffness);
829
+ c.solver.addTerm(f);
830
+ return () => c.solver.removeTerm(f);
831
+ }
832
+ }
833
+ export function bodyAnchor(body, target, stiffness = 1e5) {
834
+ return new BodyAnchor(body, target, stiffness);
835
+ }