@series-inc/rundot-syncplay 5.23.0 → 5.24.0-beta.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 (72) hide show
  1. package/README.md +20 -0
  2. package/dist/browser.d.ts +4 -1
  3. package/dist/browser.js +1 -0
  4. package/dist/checksum-basis.d.ts +4 -0
  5. package/dist/checksum-basis.js +17 -0
  6. package/dist/cjs/browser.js +4 -2
  7. package/dist/cjs/checksum-basis.js +22 -0
  8. package/dist/cjs/collider-cooking.js +834 -0
  9. package/dist/cjs/deterministic-session.js +2 -0
  10. package/dist/cjs/engine-parity-matrix.js +32 -9
  11. package/dist/cjs/engine-parity.js +1 -1
  12. package/dist/cjs/index.js +8 -2
  13. package/dist/cjs/instant-replay.js +4 -9
  14. package/dist/cjs/movement3d.js +17 -1
  15. package/dist/cjs/networked-client.js +11 -9
  16. package/dist/cjs/node.js +5 -1
  17. package/dist/cjs/offline-session.js +52 -24
  18. package/dist/cjs/physics-cert.js +7 -2
  19. package/dist/cjs/physics3d-hull.js +381 -0
  20. package/dist/cjs/physics3d-joints.js +163 -31
  21. package/dist/cjs/physics3d-recipes.js +197 -0
  22. package/dist/cjs/physics3d-shared.js +6 -0
  23. package/dist/cjs/physics3d-solver.js +746 -89
  24. package/dist/cjs/physics3d-stacking.js +154 -0
  25. package/dist/cjs/physics3d-toi.js +251 -0
  26. package/dist/cjs/physics3d-vehicle.js +35 -10
  27. package/dist/cjs/physics3d.js +401 -20
  28. package/dist/cjs/runtime-support.js +71 -0
  29. package/dist/cjs/session-promotion.js +5 -5
  30. package/dist/cjs/synctest.js +49 -0
  31. package/dist/cjs/testing.js +4 -1
  32. package/dist/cli.js +163 -41
  33. package/dist/collider-cooking.d.ts +141 -0
  34. package/dist/collider-cooking.js +829 -0
  35. package/dist/deterministic-session.js +2 -0
  36. package/dist/engine-parity-matrix.js +32 -9
  37. package/dist/engine-parity.js +1 -1
  38. package/dist/index.d.ts +3 -1
  39. package/dist/index.js +1 -0
  40. package/dist/instant-replay.d.ts +2 -2
  41. package/dist/instant-replay.js +4 -9
  42. package/dist/movement3d.js +18 -2
  43. package/dist/networked-client.js +11 -9
  44. package/dist/node.d.ts +2 -0
  45. package/dist/node.js +2 -0
  46. package/dist/offline-session.js +53 -25
  47. package/dist/physics-cert.js +7 -2
  48. package/dist/physics3d-hull.d.ts +53 -0
  49. package/dist/physics3d-hull.js +376 -0
  50. package/dist/physics3d-joints.d.ts +7 -0
  51. package/dist/physics3d-joints.js +163 -31
  52. package/dist/physics3d-recipes.d.ts +29 -0
  53. package/dist/physics3d-recipes.js +193 -0
  54. package/dist/physics3d-shared.js +6 -0
  55. package/dist/physics3d-solver.d.ts +65 -1
  56. package/dist/physics3d-solver.js +744 -89
  57. package/dist/physics3d-stacking.d.ts +26 -0
  58. package/dist/physics3d-stacking.js +147 -0
  59. package/dist/physics3d-toi.d.ts +70 -0
  60. package/dist/physics3d-toi.js +245 -0
  61. package/dist/physics3d-vehicle.js +35 -10
  62. package/dist/physics3d.d.ts +20 -2
  63. package/dist/physics3d.js +400 -21
  64. package/dist/runtime-support.d.ts +60 -0
  65. package/dist/runtime-support.js +66 -0
  66. package/dist/session-promotion.js +5 -5
  67. package/dist/synctest.d.ts +1 -1
  68. package/dist/synctest.js +49 -0
  69. package/dist/testing.d.ts +2 -0
  70. package/dist/testing.js +1 -0
  71. package/dist/types.d.ts +21 -2
  72. package/package.json +8 -3
@@ -0,0 +1,381 @@
1
+ "use strict";
2
+ /**
3
+ * Deterministic convex-hull preprocessing for the rigid-body solver.
4
+ *
5
+ * Given ≤ 16 local-space vertices, this builds the outward face planes (with
6
+ * CCW-ordered polygons) and the unique edge directions the SAT narrow phase
7
+ * needs. It uses exhaustive supporting-plane enumeration over vertex triples
8
+ * (C(16,3) = 560 candidates — trivial), never a quickhull, so the result is a
9
+ * pure deterministic function of the input with lexicographic tie-breaking.
10
+ *
11
+ * Numeric regime: add/mul/cmp plus `solverSqrt` for the final unit normals only.
12
+ * No transcendentals (banned in src/ by the static determinism checker).
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.buildSolverHull = buildSolverHull;
16
+ exports.rayHullIntervalLocal = rayHullIntervalLocal;
17
+ exports.closestPointOnHullLocal = closestPointOnHullLocal;
18
+ const physics3d_shared_js_1 = require("./physics3d-shared.js");
19
+ const PLANE_TOL = 1e-9;
20
+ const COPLANAR_TOL = 1e-6;
21
+ const DEDUP_TOL2 = 1e-12;
22
+ const EDGE_TOL = 1e-6;
23
+ function lexCompare(a, b) {
24
+ if (a.x !== b.x) {
25
+ return a.x < b.x ? -1 : 1;
26
+ }
27
+ if (a.y !== b.y) {
28
+ return a.y < b.y ? -1 : 1;
29
+ }
30
+ if (a.z !== b.z) {
31
+ return a.z < b.z ? -1 : 1;
32
+ }
33
+ return 0;
34
+ }
35
+ function dedupSorted(input) {
36
+ const sorted = [...input].sort(lexCompare);
37
+ const out = [];
38
+ for (const v of sorted) {
39
+ const last = out[out.length - 1];
40
+ if (last !== undefined) {
41
+ const dx = v.x - last.x;
42
+ const dy = v.y - last.y;
43
+ const dz = v.z - last.z;
44
+ if (dx * dx + dy * dy + dz * dz <= DEDUP_TOL2) {
45
+ continue;
46
+ }
47
+ }
48
+ out.push({ x: v.x, y: v.y, z: v.z });
49
+ }
50
+ return out;
51
+ }
52
+ function isFinitePoint(v) {
53
+ return Number.isFinite(v.x) && Number.isFinite(v.y) && Number.isFinite(v.z);
54
+ }
55
+ /**
56
+ * Order the on-plane vertex indices CCW around `normal` (right-hand rule) using
57
+ * a signed-angle comparator built from cross/dot sign tests only — no atan2.
58
+ * The polygon is convex, so all points share a consistent winding relative to
59
+ * the centroid; we sort by half-plane then by cross-product sign.
60
+ */
61
+ function orderFaceVerts(indices, verts, normal) {
62
+ /* istanbul ignore next -- a supporting-plane face always carries at least three vertices. */
63
+ if (indices.length <= 2) {
64
+ return [...indices];
65
+ }
66
+ let cx = 0;
67
+ let cy = 0;
68
+ let cz = 0;
69
+ for (const i of indices) {
70
+ cx += verts[i].x;
71
+ cy += verts[i].y;
72
+ cz += verts[i].z;
73
+ }
74
+ const inv = 1 / indices.length;
75
+ const center = { x: cx * inv, y: cy * inv, z: cz * inv };
76
+ // Build an orthonormal basis (u, w) spanning the face plane.
77
+ const ref = (0, physics3d_shared_js_1.subVec3)(verts[indices[0]], center);
78
+ const refLen = (0, physics3d_shared_js_1.solverSqrt)((0, physics3d_shared_js_1.dotVec3)(ref, ref));
79
+ /* istanbul ignore next -- a face vertex never coincides with its own face centroid, so refLen is always positive. */
80
+ const u = refLen > EDGE_TOL ? (0, physics3d_shared_js_1.scaleVec3)(ref, 1 / refLen) : orthonormalAny(normal);
81
+ const w = (0, physics3d_shared_js_1.crossVec3)(normal, u);
82
+ const key = (i) => {
83
+ const d = (0, physics3d_shared_js_1.subVec3)(verts[i], center);
84
+ const s = (0, physics3d_shared_js_1.dotVec3)(d, w);
85
+ const c = (0, physics3d_shared_js_1.dotVec3)(d, u);
86
+ // half 0 for upper (s>=0), 1 for lower — upper sweeps first for CCW order.
87
+ return { half: s >= 0 ? 0 : 1, s, c };
88
+ };
89
+ return [...indices].sort((ia, ib) => {
90
+ const a = key(ia);
91
+ const b = key(ib);
92
+ if (a.half !== b.half) {
93
+ return a.half - b.half;
94
+ }
95
+ // Within a half-plane, angle increases as (c descending) via cross sign:
96
+ // cross of the two direction vectors' sign gives relative order.
97
+ const da = (0, physics3d_shared_js_1.subVec3)(verts[ia], center);
98
+ const db = (0, physics3d_shared_js_1.subVec3)(verts[ib], center);
99
+ const cr = (0, physics3d_shared_js_1.dotVec3)((0, physics3d_shared_js_1.crossVec3)(da, db), normal);
100
+ if (cr > EDGE_TOL) {
101
+ return -1;
102
+ }
103
+ /* istanbul ignore else -- an exact cross-product tie needs two same-half face vertices antipodal through the centroid; no convex face produces one (searched over cube + 4/6/8-gon prisms at four phases). The index tie-break stays as the determinism guarantee. */
104
+ if (cr < -EDGE_TOL) {
105
+ return 1;
106
+ }
107
+ /* istanbul ignore next -- see above: the exact tie is unreachable, the tie-break stays for determinism. */
108
+ return ia - ib;
109
+ });
110
+ }
111
+ /* istanbul ignore next -- only reachable from the unreachable refLen guard in orderFaceVerts. */
112
+ function orthonormalAny(n) {
113
+ const ax = Math.abs(n.x);
114
+ const ay = Math.abs(n.y);
115
+ const az = Math.abs(n.z);
116
+ const pick = ax <= ay && ax <= az ? { x: 1, y: 0, z: 0 } : ay <= az ? { x: 0, y: 1, z: 0 } : { x: 0, y: 0, z: 1 };
117
+ const proj = (0, physics3d_shared_js_1.dotVec3)(pick, n);
118
+ const t = (0, physics3d_shared_js_1.subVec3)(pick, (0, physics3d_shared_js_1.scaleVec3)(n, proj));
119
+ const len = (0, physics3d_shared_js_1.solverSqrt)((0, physics3d_shared_js_1.dotVec3)(t, t));
120
+ return len > EDGE_TOL ? (0, physics3d_shared_js_1.scaleVec3)(t, 1 / len) : { x: 1, y: 0, z: 0 };
121
+ }
122
+ function buildHullUncached(vertices) {
123
+ if (vertices.length < 4 || vertices.length > 16) {
124
+ throw new Error(`hull requires 4-16 finite vertices with non-zero volume, received ${vertices.length}`);
125
+ }
126
+ for (const v of vertices) {
127
+ if (!isFinitePoint(v)) {
128
+ throw new Error('hull requires 4-16 finite vertices with non-zero volume, received non-finite vertex');
129
+ }
130
+ }
131
+ const verts = dedupSorted(vertices);
132
+ if (verts.length < 4) {
133
+ throw new Error('hull requires 4-16 finite vertices with non-zero volume, received degenerate vertex set');
134
+ }
135
+ const n = verts.length;
136
+ let cx = 0;
137
+ let cy = 0;
138
+ let cz = 0;
139
+ for (const v of verts) {
140
+ cx += v.x;
141
+ cy += v.y;
142
+ cz += v.z;
143
+ }
144
+ const centroid = { x: cx / n, y: cy / n, z: cz / n };
145
+ // Enumerate every triple as a candidate supporting plane.
146
+ const candidates = [];
147
+ for (let i = 0; i < n; i += 1) {
148
+ for (let j = i + 1; j < n; j += 1) {
149
+ for (let k = j + 1; k < n; k += 1) {
150
+ const e1 = (0, physics3d_shared_js_1.subVec3)(verts[j], verts[i]);
151
+ const e2 = (0, physics3d_shared_js_1.subVec3)(verts[k], verts[i]);
152
+ const cr = (0, physics3d_shared_js_1.crossVec3)(e1, e2);
153
+ const len2 = (0, physics3d_shared_js_1.dotVec3)(cr, cr);
154
+ if (len2 < PLANE_TOL * PLANE_TOL) {
155
+ continue; // collinear triple
156
+ }
157
+ const invLen = 1 / (0, physics3d_shared_js_1.solverSqrt)(len2);
158
+ let nx = cr.x * invLen;
159
+ let ny = cr.y * invLen;
160
+ let nz = cr.z * invLen;
161
+ let offset = nx * verts[i].x + ny * verts[i].y + nz * verts[i].z;
162
+ // Orient outward: centroid must lie behind the plane (dot < offset).
163
+ const centroidDot = nx * centroid.x + ny * centroid.y + nz * centroid.z;
164
+ if (centroidDot > offset) {
165
+ nx = -nx;
166
+ ny = -ny;
167
+ nz = -nz;
168
+ offset = -offset;
169
+ }
170
+ // Keep iff all vertices lie on or behind the plane.
171
+ let supporting = true;
172
+ const onPlane = [];
173
+ for (let m = 0; m < n; m += 1) {
174
+ const d = nx * verts[m].x + ny * verts[m].y + nz * verts[m].z - offset;
175
+ if (d > PLANE_TOL) {
176
+ supporting = false;
177
+ break;
178
+ }
179
+ if (d >= -PLANE_TOL) {
180
+ onPlane.push(m);
181
+ }
182
+ }
183
+ if (supporting && onPlane.length >= 3) {
184
+ candidates.push({ nx, ny, nz, offset, onPlane });
185
+ }
186
+ }
187
+ }
188
+ }
189
+ // Merge coplanar candidates (same oriented plane within tolerance).
190
+ const faces = [];
191
+ const usedVerts = new Array(n).fill(false);
192
+ for (const cand of candidates) {
193
+ const existing = faces.find((f) => Math.abs(f.normal.x - cand.nx) <= COPLANAR_TOL
194
+ && Math.abs(f.normal.y - cand.ny) <= COPLANAR_TOL
195
+ && Math.abs(f.normal.z - cand.nz) <= COPLANAR_TOL
196
+ && Math.abs(f.offset - cand.offset) <= COPLANAR_TOL);
197
+ if (existing !== undefined) {
198
+ continue;
199
+ }
200
+ const merged = new Set(cand.onPlane);
201
+ for (let m = 0; m < n; m += 1) {
202
+ const d = cand.nx * verts[m].x + cand.ny * verts[m].y + cand.nz * verts[m].z - cand.offset;
203
+ if (d >= -PLANE_TOL && d <= PLANE_TOL) {
204
+ merged.add(m);
205
+ }
206
+ }
207
+ const normal = { x: cand.nx, y: cand.ny, z: cand.nz };
208
+ const ordered = orderFaceVerts([...merged], verts, normal);
209
+ for (const idx of ordered) {
210
+ usedVerts[idx] = true;
211
+ }
212
+ faces.push({ normal, offset: cand.offset, verts: ordered });
213
+ }
214
+ if (faces.length < 4) {
215
+ throw new Error('hull requires 4-16 finite vertices with non-zero volume, received coplanar vertex set');
216
+ }
217
+ for (let m = 0; m < n; m += 1) {
218
+ if (!usedVerts[m]) {
219
+ throw new Error('hull requires all vertices on the convex boundary, received interior vertex');
220
+ }
221
+ }
222
+ // Sort faces deterministically by outward normal then offset.
223
+ /* istanbul ignore next -- two faces of a convex hull never share a normal, so the offset tie-break never runs; it stays as the determinism guarantee. */
224
+ faces.sort((a, b) => lexCompare(a.normal, b.normal) || (a.offset < b.offset ? -1 : a.offset > b.offset ? 1 : 0));
225
+ // Unique undirected edge directions from face polygon edges.
226
+ const edgeDirs = [];
227
+ for (const face of faces) {
228
+ const fv = face.verts;
229
+ for (let e = 0; e < fv.length; e += 1) {
230
+ const a = verts[fv[e]];
231
+ const b = verts[fv[(e + 1) % fv.length]];
232
+ let dir = (0, physics3d_shared_js_1.subVec3)(b, a);
233
+ const len = (0, physics3d_shared_js_1.solverSqrt)((0, physics3d_shared_js_1.dotVec3)(dir, dir));
234
+ /* istanbul ignore next -- dedupSorted removes coincident vertices, so no face edge is degenerate. */
235
+ if (len <= EDGE_TOL) {
236
+ continue;
237
+ }
238
+ dir = (0, physics3d_shared_js_1.scaleVec3)(dir, 1 / len);
239
+ // Canonical sign: first non-zero component positive.
240
+ if (dir.x < -EDGE_TOL || (Math.abs(dir.x) <= EDGE_TOL && (dir.y < -EDGE_TOL || (Math.abs(dir.y) <= EDGE_TOL && dir.z < 0)))) {
241
+ dir = (0, physics3d_shared_js_1.scaleVec3)(dir, -1);
242
+ }
243
+ const dup = edgeDirs.some((d) => Math.abs(d.x - dir.x) <= EDGE_TOL && Math.abs(d.y - dir.y) <= EDGE_TOL && Math.abs(d.z - dir.z) <= EDGE_TOL);
244
+ if (!dup) {
245
+ edgeDirs.push(dir);
246
+ }
247
+ }
248
+ }
249
+ edgeDirs.sort(lexCompare);
250
+ return { vertices: verts, faces, edgeDirs };
251
+ }
252
+ // Memoize by vertex-array identity: hull topology is a pure derived function of
253
+ // the immutable input vertex list (same rationale as quatMat3Cache) — never
254
+ // canonical rollback state, so it is safe to keep outside the frame.
255
+ // rdm-ignore-next-line determinism/no-module-mutable-state: pure derived hull data keyed by immutable input identity
256
+ const hullCache = new WeakMap();
257
+ function buildSolverHull(vertices) {
258
+ const cached = hullCache.get(vertices);
259
+ if (cached !== undefined) {
260
+ return cached;
261
+ }
262
+ const built = buildHullUncached(vertices);
263
+ hullCache.set(vertices, built);
264
+ return built;
265
+ }
266
+ /**
267
+ * Ray-vs-hull interval clip in hull-local space. Returns the [tEnter, tExit]
268
+ * parameter interval where the ray lies inside all half-spaces, or undefined
269
+ * when the ray misses. When the origin is inside the hull, tEnter < 0; callers
270
+ * clamp the reported hit distance to 0 (matching sphere/AABB inside-ray semantics).
271
+ */
272
+ function rayHullIntervalLocal(origin, dir, hull) {
273
+ let tEnter = -Infinity;
274
+ let tExit = Infinity;
275
+ let enterNormal = { x: 0, y: 1, z: 0 };
276
+ for (const face of hull.faces) {
277
+ const nd = face.normal.x * dir.x + face.normal.y * dir.y + face.normal.z * dir.z;
278
+ const dist = face.offset - (face.normal.x * origin.x + face.normal.y * origin.y + face.normal.z * origin.z);
279
+ // Half-space: dot(n, origin + t·dir) <= offset ⇔ t·nd <= dist
280
+ if (Math.abs(nd) < 1e-12) {
281
+ if (dist < 0) {
282
+ return undefined; // parallel and outside this face
283
+ }
284
+ continue;
285
+ }
286
+ const t = dist / nd;
287
+ if (nd < 0) {
288
+ // entering this half-space at t
289
+ if (t > tEnter) {
290
+ tEnter = t;
291
+ enterNormal = face.normal;
292
+ }
293
+ }
294
+ else if (t < tExit) {
295
+ tExit = t;
296
+ }
297
+ if (tEnter > tExit) {
298
+ return undefined;
299
+ }
300
+ }
301
+ if (tExit < 0) {
302
+ return undefined;
303
+ }
304
+ return { tEnter, tExit, enterNormal };
305
+ }
306
+ /**
307
+ * Closest point on (or inside) the hull to `point`, in hull-local space, plus
308
+ * the deepest-violated face normal and its signed distance (positive outside).
309
+ * When the point is inside every face, `inside` is true and the closest point
310
+ * is `point` itself.
311
+ */
312
+ function closestPointOnHullLocal(point, hull) {
313
+ let maxDist = -Infinity;
314
+ let maxFace = hull.faces[0];
315
+ for (const face of hull.faces) {
316
+ const d = face.normal.x * point.x + face.normal.y * point.y + face.normal.z * point.z - face.offset;
317
+ if (d > maxDist) {
318
+ maxDist = d;
319
+ maxFace = face;
320
+ }
321
+ }
322
+ if (maxDist <= 0) {
323
+ return { point: { x: point.x, y: point.y, z: point.z }, inside: true, normal: maxFace.normal, distance: maxDist };
324
+ }
325
+ // Outside: project onto the deepest-violated face polygon (clamped to it),
326
+ // which is the exact closest point for points beyond a single face and a good
327
+ // deterministic approximation near edges/corners (refined by clamping).
328
+ const projected = projectOntoFacePolygon(point, hull, maxFace);
329
+ return { point: projected, inside: false, normal: maxFace.normal, distance: maxDist };
330
+ }
331
+ function projectOntoFacePolygon(point, hull, face) {
332
+ const d = face.normal.x * point.x + face.normal.y * point.y + face.normal.z * point.z - face.offset;
333
+ const onPlane = {
334
+ x: point.x - face.normal.x * d,
335
+ y: point.y - face.normal.y * d,
336
+ z: point.z - face.normal.z * d,
337
+ };
338
+ // If the projection is inside the face polygon, that is the closest point.
339
+ const fv = face.verts;
340
+ let insidePolygon = true;
341
+ for (let e = 0; e < fv.length; e += 1) {
342
+ const a = hull.vertices[fv[e]];
343
+ const b = hull.vertices[fv[(e + 1) % fv.length]];
344
+ const edge = (0, physics3d_shared_js_1.subVec3)(b, a);
345
+ const toPoint = (0, physics3d_shared_js_1.subVec3)(onPlane, a);
346
+ const c = (0, physics3d_shared_js_1.crossVec3)(edge, toPoint);
347
+ if ((0, physics3d_shared_js_1.dotVec3)(c, face.normal) < -EDGE_TOL) {
348
+ insidePolygon = false;
349
+ break;
350
+ }
351
+ }
352
+ if (insidePolygon) {
353
+ return onPlane;
354
+ }
355
+ // Otherwise clamp to the nearest polygon edge segment.
356
+ let best = hull.vertices[fv[0]];
357
+ let bestDist2 = Infinity;
358
+ for (let e = 0; e < fv.length; e += 1) {
359
+ const a = hull.vertices[fv[e]];
360
+ const b = hull.vertices[fv[(e + 1) % fv.length]];
361
+ const cand = closestPointOnSegmentLocal(point, a, b);
362
+ const dd = (0, physics3d_shared_js_1.subVec3)(cand, point);
363
+ const dist2 = (0, physics3d_shared_js_1.dotVec3)(dd, dd);
364
+ if (dist2 < bestDist2) {
365
+ bestDist2 = dist2;
366
+ best = cand;
367
+ }
368
+ }
369
+ return best;
370
+ }
371
+ function closestPointOnSegmentLocal(point, a, b) {
372
+ const ab = (0, physics3d_shared_js_1.subVec3)(b, a);
373
+ const denom = (0, physics3d_shared_js_1.dotVec3)(ab, ab);
374
+ /* istanbul ignore next -- hull edges are never degenerate (dedupSorted). */
375
+ if (denom === 0) {
376
+ return { x: a.x, y: a.y, z: a.z };
377
+ }
378
+ let t = (0, physics3d_shared_js_1.dotVec3)((0, physics3d_shared_js_1.subVec3)(point, a), ab) / denom;
379
+ t = t < 0 ? 0 : t > 1 ? 1 : t;
380
+ return (0, physics3d_shared_js_1.addVec3)(a, (0, physics3d_shared_js_1.scaleVec3)(ab, t));
381
+ }