pelican-engine 0.0.0-stage → 0.3.1

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 (192) hide show
  1. package/AGENTS.md +147 -0
  2. package/LICENSE +22 -0
  3. package/README.md +129 -2
  4. package/distribution.json +197 -0
  5. package/docs/AI-WORKFLOW.md +517 -0
  6. package/docs/ARCHITECTURE.md +268 -0
  7. package/docs/CAPABILITIES.md +264 -0
  8. package/docs/DECISIONS.md +107 -0
  9. package/docs/M3-USAGE.md +192 -0
  10. package/docs/PELICAN-MIGRATION.md +38 -0
  11. package/docs/RENDER-BACKENDS.md +50 -0
  12. package/docs/ROADMAP.md +239 -0
  13. package/docs/THIRD-PARTY-NOTICES.md +151 -0
  14. package/docs/api/REFERENCE.md +1435 -0
  15. package/docs/api/index.json +1139 -0
  16. package/docs/npm/DISTRIBUTION.md +130 -0
  17. package/docs/npm/GAME-AUTHOR.md +147 -0
  18. package/engine.manifest.json +15 -0
  19. package/lib/assets/audio.js +67 -0
  20. package/lib/assets/errors.js +36 -0
  21. package/lib/assets/glb.js +277 -0
  22. package/lib/assets/index.js +53 -0
  23. package/lib/assets/jpeg.js +199 -0
  24. package/lib/assets/model.js +558 -0
  25. package/lib/assets/optimize.js +95 -0
  26. package/lib/assets/texture.js +187 -0
  27. package/lib/compiler/distribution.js +17 -0
  28. package/lib/compiler/extract.js +425 -0
  29. package/lib/compiler/graph.js +366 -0
  30. package/lib/compiler/index.js +574 -0
  31. package/lib/compiler/normalize.js +1194 -0
  32. package/lib/compiler/source-map.js +25 -0
  33. package/lib/compiler/types.js +2 -0
  34. package/lib/compiler/version.js +93 -0
  35. package/lib/experiments/simulation/index.js +2 -0
  36. package/lib/experiments/simulation/math.js +41 -0
  37. package/lib/experiments/simulation/probe.js +215 -0
  38. package/lib/experiments/simulation/scenes.js +192 -0
  39. package/lib/experiments/simulation/solver.js +346 -0
  40. package/lib/experiments/simulation/types.js +1 -0
  41. package/lib/experiments/world/adapters.js +30 -0
  42. package/lib/experiments/world/geometry.js +180 -0
  43. package/lib/experiments/world/index.js +2 -0
  44. package/lib/experiments/world/probe.js +288 -0
  45. package/lib/experiments/world/types.js +1 -0
  46. package/lib/experiments/world/world.js +599 -0
  47. package/lib/navigation/index.js +157 -0
  48. package/lib/physics/index.js +909 -0
  49. package/lib/protocol/index.js +92 -0
  50. package/lib/renderer/acceleration.js +609 -0
  51. package/lib/renderer/browser.js +2015 -0
  52. package/lib/renderer/bsdf.js +260 -0
  53. package/lib/renderer/checkpoint.js +88 -0
  54. package/lib/renderer/compute-executor.js +1034 -0
  55. package/lib/renderer/compute-texture-assets.js +49 -0
  56. package/lib/renderer/compute-view-contract.js +1 -0
  57. package/lib/renderer/denoiser.js +361 -0
  58. package/lib/renderer/dielectric.js +360 -0
  59. package/lib/renderer/embedded-image-pixels.js +368 -0
  60. package/lib/renderer/emissive-lights.js +67 -0
  61. package/lib/renderer/environment-importance.js +55 -0
  62. package/lib/renderer/environment.js +346 -0
  63. package/lib/renderer/gameplay.js +464 -0
  64. package/lib/renderer/geometry.js +103 -0
  65. package/lib/renderer/graph-expressions.js +485 -0
  66. package/lib/renderer/graph-tsl.js +141 -0
  67. package/lib/renderer/graph.js +214 -0
  68. package/lib/renderer/managed-texture-sampling.js +29 -0
  69. package/lib/renderer/material-graphs.js +297 -0
  70. package/lib/renderer/medium.js +348 -0
  71. package/lib/renderer/models.js +1053 -0
  72. package/lib/renderer/motion-shader.js +40 -0
  73. package/lib/renderer/path-shader.js +196 -0
  74. package/lib/renderer/pathtrace.js +324 -0
  75. package/lib/renderer/postprocess-contract.js +1 -0
  76. package/lib/renderer/postprocess.js +494 -0
  77. package/lib/renderer/render-buffers.js +132 -0
  78. package/lib/renderer/render-textures.js +155 -0
  79. package/lib/renderer/startup.js +50 -0
  80. package/lib/renderer/surface-capture.js +199 -0
  81. package/lib/renderer/surface-gpu.js +270 -0
  82. package/lib/renderer/surface-motion.js +84 -0
  83. package/lib/renderer/surface-ray-shader.js +91 -0
  84. package/lib/renderer/surface-shader.js +88 -0
  85. package/lib/renderer/surface-tracer.js +1720 -0
  86. package/lib/renderer/surface.js +1425 -0
  87. package/lib/renderer/temporal-filter.js +512 -0
  88. package/lib/renderer/texture-pixels.js +207 -0
  89. package/lib/renderer/traversal-nodes.js +54 -0
  90. package/lib/renderer/ui.js +446 -0
  91. package/lib/renderer/vertex-bounds.js +127 -0
  92. package/lib/renderer/vertex-contract.js +1 -0
  93. package/lib/renderer/vertex-graphs.js +197 -0
  94. package/lib/renderer/volume-shader.js +50 -0
  95. package/lib/runtime/compute-contract.js +1 -0
  96. package/lib/runtime/compute-service.js +674 -0
  97. package/lib/runtime/contracts.js +1 -0
  98. package/lib/runtime/error.js +37 -0
  99. package/lib/runtime/events.js +189 -0
  100. package/lib/runtime/game-services.js +772 -0
  101. package/lib/runtime/host.js +839 -0
  102. package/lib/runtime/material-parameters.js +145 -0
  103. package/lib/runtime/plugins.js +1 -0
  104. package/lib/runtime/postprocess-contract.js +1 -0
  105. package/lib/runtime/postprocess.js +189 -0
  106. package/lib/runtime/prefabs.js +13 -0
  107. package/lib/runtime/random.js +88 -0
  108. package/lib/runtime/render-buffer-contract.js +1 -0
  109. package/lib/runtime/render-buffers.js +243 -0
  110. package/lib/runtime/render-capabilities.js +17 -0
  111. package/lib/runtime/render-settings.js +249 -0
  112. package/lib/runtime/render-texture-contract.js +1 -0
  113. package/lib/runtime/render-textures.js +271 -0
  114. package/lib/runtime/resources.js +648 -0
  115. package/lib/runtime/scheduler.js +765 -0
  116. package/lib/runtime/storage.js +169 -0
  117. package/lib/runtime/world.js +594 -0
  118. package/lib/schema/index.js +24 -0
  119. package/lib/sdk/graph-values.js +16 -0
  120. package/lib/sdk/index.d.ts +800 -0
  121. package/lib/sdk/index.js +82 -0
  122. package/lib/tools/api-index.js +113 -0
  123. package/lib/tools/browser-session.js +89 -0
  124. package/lib/tools/cli.js +513 -0
  125. package/lib/tools/compress-dist.js +59 -0
  126. package/lib/tools/control-client.js +153 -0
  127. package/lib/tools/control-server.js +633 -0
  128. package/lib/tools/diagnostic-code.js +3 -0
  129. package/lib/tools/doctor.js +496 -0
  130. package/lib/tools/npm/browser-cli.js +6 -0
  131. package/lib/tools/npm/entry.js +40 -0
  132. package/lib/tools/npm/layout.js +195 -0
  133. package/lib/tools/setup.js +153 -0
  134. package/licenses/@dimforge/rapier2d-compat/LICENSE +201 -0
  135. package/licenses/@dimforge/rapier3d-compat/LICENSE +201 -0
  136. package/licenses/@gltf-transform/core/LICENSE.md +21 -0
  137. package/licenses/@gltf-transform/extensions/LICENSE.md +21 -0
  138. package/licenses/@gltf-transform/functions/LICENSE.md +21 -0
  139. package/licenses/@img/colour/LICENSE.md +82 -0
  140. package/licenses/@img/sharp-win32-x64/LICENSE +191 -0
  141. package/licenses/@oxc-project/types/LICENSE +22 -0
  142. package/licenses/@rolldown/pluginutils/LICENSE +21 -0
  143. package/licenses/@tweenjs/tween.js/LICENSE +23 -0
  144. package/licenses/@types/ndarray/LICENSE +21 -0
  145. package/licenses/@types/node/LICENSE +21 -0
  146. package/licenses/@types/stats.js/LICENSE +21 -0
  147. package/licenses/@types/three/@dimforge/rapier3d-compat/LICENSE +201 -0
  148. package/licenses/@types/three/LICENSE +21 -0
  149. package/licenses/@types/webxr/LICENSE +21 -0
  150. package/licenses/@webgpu/types/LICENSE +26 -0
  151. package/licenses/THIRD-PARTY-NOTICES.md +139 -0
  152. package/licenses/cwise-compiler/LICENSE +22 -0
  153. package/licenses/detect-libc/LICENSE +201 -0
  154. package/licenses/esbuild/LICENSE.md +21 -0
  155. package/licenses/fast-png/LICENSE +21 -0
  156. package/licenses/fdir/LICENSE +7 -0
  157. package/licenses/fflate/LICENSE +21 -0
  158. package/licenses/iobuffer/LICENSE +22 -0
  159. package/licenses/iota-array/LICENSE +22 -0
  160. package/licenses/is-buffer/LICENSE +21 -0
  161. package/licenses/ktx-parse/LICENSE +21 -0
  162. package/licenses/lightningcss/LICENSE +373 -0
  163. package/licenses/lightningcss-win32-x64-msvc/LICENSE +373 -0
  164. package/licenses/meshoptimizer/LICENSE.md +21 -0
  165. package/licenses/nanoid/LICENSE +20 -0
  166. package/licenses/ndarray/LICENSE +22 -0
  167. package/licenses/ndarray-lanczos/LICENSE +21 -0
  168. package/licenses/ndarray-ops/LICENSE +22 -0
  169. package/licenses/ndarray-pixels/LICENSE +21 -0
  170. package/licenses/ngraph.path/LICENSE +21 -0
  171. package/licenses/picocolors/LICENSE +15 -0
  172. package/licenses/picomatch/LICENSE +21 -0
  173. package/licenses/playwright/LICENSE +202 -0
  174. package/licenses/playwright/NOTICE +5 -0
  175. package/licenses/playwright-core/LICENSE +202 -0
  176. package/licenses/playwright-core/NOTICE +5 -0
  177. package/licenses/postcss/LICENSE +20 -0
  178. package/licenses/prettier/LICENSE +7 -0
  179. package/licenses/property-graph/LICENSE +21 -0
  180. package/licenses/rolldown/LICENSE +25 -0
  181. package/licenses/semver/LICENSE +15 -0
  182. package/licenses/sharp/LICENSE +191 -0
  183. package/licenses/source-map-js/LICENSE +28 -0
  184. package/licenses/three/LICENSE +21 -0
  185. package/licenses/tinyglobby/LICENSE +21 -0
  186. package/licenses/tsx/LICENSE +21 -0
  187. package/licenses/typescript/LICENSE.txt +55 -0
  188. package/licenses/undici-types/LICENSE +21 -0
  189. package/licenses/uniq/LICENSE +22 -0
  190. package/licenses/vite/LICENSE.md +2084 -0
  191. package/package.json +49 -4
  192. package/web/index.html +34 -0
@@ -0,0 +1,346 @@
1
+ import { accumulate, add, copy, cross, dot, fail, isVec3, length, mul, sub, volume, } from "./math.js";
2
+ import { allPoints, makeScene, resolveOptions } from "./scenes.js";
3
+ const REST_DENSITY = 1;
4
+ const SMOOTHING_LENGTH = 0.34;
5
+ const PRESSURE_STIFFNESS = 12;
6
+ const VISCOSITY = 0.025;
7
+ const LOWER = [-1.35, -1.05, -1.1];
8
+ const UPPER = [1.35, 1.65, 1.1];
9
+ /** Pure synchronous CPU reference solver; intentionally small O(n²) fluid neighbor search. */
10
+ export function createSimulation(kind, options = {}) {
11
+ let state = makeScene(kind, resolveOptions(kind, options));
12
+ let disposed = false;
13
+ let solverWallMs = 0, measuredSteps = 0, maxStepMs = 0;
14
+ const alive = () => {
15
+ if (disposed)
16
+ fail("SIM_DISPOSED", "Create a new simulation before stepping, restoring, or reading geometry.");
17
+ };
18
+ function fluidData() {
19
+ const h2 = SMOOTHING_LENGTH ** 2;
20
+ const poly6 = 315 / (64 * Math.PI * SMOOTHING_LENGTH ** 9);
21
+ const density = state.particles.map((p) => {
22
+ let rho = 0;
23
+ for (const q of state.particles) {
24
+ const delta = sub(p.position, q.position), r2 = dot(delta, delta);
25
+ if (r2 < h2)
26
+ rho += (1 / q.inverseMass) * poly6 * (h2 - r2) ** 3;
27
+ }
28
+ return rho;
29
+ });
30
+ // Weakly compressible SPH: compressive pressure only, free-surface tension omitted.
31
+ const pressure = density.map((rho) => PRESSURE_STIFFNESS * Math.max(0, rho - REST_DENSITY));
32
+ return { density, pressure };
33
+ }
34
+ function sphAccelerations() {
35
+ const points = state.particles;
36
+ const { density, pressure } = fluidData();
37
+ const forces = points.map(() => [0, 0, 0]);
38
+ const kernel = 45 / (Math.PI * SMOOTHING_LENGTH ** 6);
39
+ for (let i = 0; i < points.length; i++)
40
+ for (let j = i + 1; j < points.length; j++) {
41
+ const delta = sub(points[i].position, points[j].position), r = length(delta);
42
+ if (r >= SMOOTHING_LENGTH || r < 1e-10)
43
+ continue;
44
+ const mi = 1 / points[i].inverseMass, mj = 1 / points[j].inverseMass;
45
+ const d = SMOOTHING_LENGTH - r;
46
+ const pressureForce = mul(delta, (mi *
47
+ mj *
48
+ (pressure[i] / density[i] ** 2 + pressure[j] / density[j] ** 2) *
49
+ kernel *
50
+ d ** 2) /
51
+ r);
52
+ const viscousForce = mul(sub(points[j].velocity, points[i].velocity), ((VISCOSITY * mi * mj) / (density[i] * density[j])) * kernel * d);
53
+ const force = add(pressureForce, viscousForce);
54
+ // Equal-and-opposite pair forces; total internal momentum is conserved before damping/walls.
55
+ accumulate(forces[i], force);
56
+ accumulate(forces[j], force, -1);
57
+ }
58
+ return forces.map((f, i) => mul(f, points[i].inverseMass));
59
+ }
60
+ function solveDistances(h, lambdas) {
61
+ for (let i = 0; i < state.distances.length; i++) {
62
+ const c = state.distances[i], a = state.particles[c.a], b = state.particles[c.b];
63
+ const delta = sub(a.position, b.position), r = length(delta);
64
+ if (r < 1e-12)
65
+ continue;
66
+ const alpha = c.compliance / (h * h), denom = a.inverseMass + b.inverseMass + alpha;
67
+ if (denom === 0)
68
+ continue;
69
+ const dl = (-(r - c.restLength) - alpha * lambdas[i]) / denom;
70
+ lambdas[i] += dl;
71
+ accumulate(a.position, delta, (a.inverseMass * dl) / r);
72
+ accumulate(b.position, delta, (-b.inverseMass * dl) / r);
73
+ }
74
+ }
75
+ function solveVolumes(h, lambdas) {
76
+ for (let i = 0; i < state.volumes.length; i++) {
77
+ const c = state.volumes[i], p = c.indices.map((index) => state.particles[index]);
78
+ const [a, b, d, e] = p.map((point) => point.position);
79
+ const g1 = mul(cross(sub(d, a), sub(e, a)), 1 / 6);
80
+ const g2 = mul(cross(sub(e, a), sub(b, a)), 1 / 6);
81
+ const g3 = mul(cross(sub(b, a), sub(d, a)), 1 / 6);
82
+ const gradients = [mul(add(add(g1, g2), g3), -1), g1, g2, g3];
83
+ const alpha = c.compliance / (h * h);
84
+ const denom = p.reduce((sum, point, k) => sum + point.inverseMass * dot(gradients[k], gradients[k]), alpha);
85
+ if (denom < 1e-20)
86
+ continue;
87
+ const dl = (-(volume(a, b, d, e) - c.restVolume) - alpha * lambdas[i]) / denom;
88
+ lambdas[i] += dl;
89
+ for (let k = 0; k < 4; k++)
90
+ accumulate(p[k].position, gradients[k], p[k].inverseMass * dl);
91
+ }
92
+ }
93
+ function solveContacts(h) {
94
+ if (!state.options.contacts)
95
+ return;
96
+ for (const body of state.bodies)
97
+ for (const p of state.particles) {
98
+ if (p.inverseMass === 0)
99
+ continue;
100
+ const delta = sub(p.position, body.position), r = length(delta), penetration = body.radius + p.radius - r;
101
+ if (penetration <= 0)
102
+ continue;
103
+ const normal = r > 1e-12 ? mul(delta, 1 / r) : [1, 0, 0];
104
+ const bodyWeight = state.options.twoWayCoupling ? body.inverseMass : 0;
105
+ const weight = p.inverseMass + bodyWeight;
106
+ accumulate(p.position, normal, (penetration * p.inverseMass) / weight);
107
+ accumulate(body.position, normal, (-penetration * bodyWeight) / weight);
108
+ state.counters.contactCount++;
109
+ state.counters.maxContactCorrection = Math.max(state.counters.maxContactCorrection, penetration);
110
+ if (bodyWeight > 0) {
111
+ const impulse = mul(normal, -penetration / (weight * h));
112
+ accumulate(state.counters.reactionImpulse, impulse);
113
+ state.counters.absoluteReactionImpulse += length(impulse);
114
+ }
115
+ }
116
+ }
117
+ function constrainWalls(p) {
118
+ if (p.inverseMass === 0)
119
+ return;
120
+ for (let k = 0; k < 3; k++)
121
+ p.position[k] = Math.max(LOWER[k] + p.radius, Math.min(UPPER[k] - p.radius, p.position[k]));
122
+ }
123
+ function substep(h) {
124
+ const points = allPoints(state), oldPositions = points.map((p) => [...p.position]);
125
+ const fluid = kind === "fluid" ? sphAccelerations() : null;
126
+ for (let i = 0; i < points.length; i++) {
127
+ const p = points[i];
128
+ if (p.inverseMass === 0)
129
+ continue;
130
+ accumulate(p.velocity, state.options.gravity, h);
131
+ if (fluid && i >= state.bodies.length)
132
+ accumulate(p.velocity, fluid[i - state.bodies.length], h);
133
+ // Explicitly dissipative reference model, not an energy-conserving integrator.
134
+ p.velocity = mul(p.velocity, Math.exp(-(i < state.bodies.length ? 0.12 : 0.25) * h));
135
+ accumulate(p.position, p.velocity, h);
136
+ }
137
+ const distanceLambdas = new Float64Array(state.distances.length);
138
+ const volumeLambdas = new Float64Array(state.volumes.length);
139
+ for (let iteration = 0; iteration < state.options.iterations; iteration++) {
140
+ solveDistances(h, distanceLambdas);
141
+ solveVolumes(h, volumeLambdas);
142
+ solveContacts(h);
143
+ for (const p of points)
144
+ constrainWalls(p);
145
+ }
146
+ for (let i = 0; i < points.length; i++) {
147
+ const p = points[i];
148
+ if (p.inverseMass === 0)
149
+ continue;
150
+ p.velocity = mul(sub(p.position, oldPositions[i]), 1 / h);
151
+ state.counters.maxObservedSpeed = Math.max(state.counters.maxObservedSpeed, length(p.velocity));
152
+ }
153
+ if (points.some((p) => !isVec3(p.position) || !isVec3(p.velocity)))
154
+ fail("SIM_NONFINITE", `Non-finite state at tick ${state.tick}; capture the last valid snapshot and reduce the timestep or impulse.`);
155
+ }
156
+ function inspect() {
157
+ const points = allPoints(state);
158
+ let maxDistanceRelativeError = 0, maxVolumeRelativeError = 0, minSignedVolumeRatio = null;
159
+ for (const c of state.distances)
160
+ maxDistanceRelativeError = Math.max(maxDistanceRelativeError, Math.abs(length(sub(state.particles[c.a].position, state.particles[c.b].position)) - c.restLength) / c.restLength);
161
+ for (const c of state.volumes) {
162
+ const v = volume(...c.indices.map((i) => state.particles[i].position));
163
+ const ratio = v / c.restVolume;
164
+ maxVolumeRelativeError = Math.max(maxVolumeRelativeError, Math.abs(ratio - 1));
165
+ minSignedVolumeRatio = Math.min(minSignedVolumeRatio ?? Infinity, ratio);
166
+ }
167
+ const bounds = state.particles.length
168
+ ? {
169
+ min: [0, 1, 2].map((k) => Math.min(...state.particles.map((p) => p.position[k]))),
170
+ max: [0, 1, 2].map((k) => Math.max(...state.particles.map((p) => p.position[k]))),
171
+ }
172
+ : null;
173
+ let kineticEnergy = 0, maxSpeed = 0;
174
+ const momentum = [0, 0, 0];
175
+ for (const p of points) {
176
+ maxSpeed = Math.max(maxSpeed, length(p.velocity));
177
+ if (p.inverseMass > 0) {
178
+ kineticEnergy += dot(p.velocity, p.velocity) / (2 * p.inverseMass);
179
+ accumulate(momentum, p.velocity, 1 / p.inverseMass);
180
+ }
181
+ }
182
+ const f = kind === "fluid" && !disposed ? fluidData() : null;
183
+ return {
184
+ kind,
185
+ tick: state.tick,
186
+ simulatedSeconds: state.tick * state.options.fixedDt,
187
+ disposed,
188
+ finite: points.every((p) => isVec3(p.position) && isVec3(p.velocity)),
189
+ resources: {
190
+ bodies: state.bodies.length,
191
+ particles: state.particles.length,
192
+ constraints: state.distances.length + state.volumes.length,
193
+ triangles: state.triangles.length,
194
+ },
195
+ bodies: copy(state.bodies),
196
+ particleBounds: bounds,
197
+ maxDistanceRelativeError,
198
+ maxVolumeRelativeError,
199
+ minSignedVolumeRatio,
200
+ maxSpeed,
201
+ kineticEnergy,
202
+ momentum,
203
+ contacts: copy(state.counters),
204
+ fluid: f
205
+ ? {
206
+ method: "WCSPH",
207
+ restDensity: REST_DENSITY,
208
+ minDensity: Math.min(...f.density),
209
+ maxDensity: Math.max(...f.density),
210
+ maxPressure: Math.max(...f.pressure),
211
+ }
212
+ : null,
213
+ timing: {
214
+ solverWallMs,
215
+ measuredSteps,
216
+ meanStepMs: measuredSteps ? solverWallMs / measuredSteps : 0,
217
+ maxStepMs,
218
+ },
219
+ };
220
+ }
221
+ return {
222
+ kind,
223
+ step(steps = 1) {
224
+ alive();
225
+ if (!Number.isInteger(steps) || steps < 0 || steps > 10000)
226
+ fail("SIM_STEPS", "steps must be an integer in [0,10000].");
227
+ for (let frame = 0; frame < steps; frame++) {
228
+ const start = performance.now();
229
+ for (let j = 0; j < state.options.substeps; j++)
230
+ substep(state.options.fixedDt / state.options.substeps);
231
+ state.tick++;
232
+ const elapsed = performance.now() - start;
233
+ solverWallMs += elapsed;
234
+ measuredSteps++;
235
+ maxStepMs = Math.max(maxStepMs, elapsed);
236
+ }
237
+ return inspect();
238
+ },
239
+ snapshot() {
240
+ alive();
241
+ return copy(state);
242
+ },
243
+ restore(snapshot) {
244
+ alive();
245
+ state = validateSnapshot(snapshot, kind);
246
+ solverWallMs = 0;
247
+ measuredSteps = 0;
248
+ maxStepMs = 0;
249
+ },
250
+ inspect,
251
+ getRenderState() {
252
+ alive();
253
+ const renderPoint = (p) => ({
254
+ id: p.id,
255
+ position: [...p.position],
256
+ radius: p.radius,
257
+ });
258
+ return {
259
+ kind,
260
+ tick: state.tick,
261
+ bodies: state.bodies.map(renderPoint),
262
+ particles: state.particles.map(renderPoint),
263
+ links: state.distances.map((c) => [c.a, c.b]),
264
+ triangles: state.triangles.map((t) => [...t]),
265
+ };
266
+ },
267
+ applyImpulse(id, impulse) {
268
+ alive();
269
+ if (!isVec3(impulse) || impulse.some((x) => Math.abs(x) > 100))
270
+ fail("SIM_IMPULSE", "impulse must contain three finite components in [-100,100].");
271
+ const p = allPoints(state).find((point) => point.id === id);
272
+ if (!p)
273
+ fail("SIM_ENTITY", `Unknown body or particle id: ${id}`);
274
+ if (p.inverseMass === 0)
275
+ fail("SIM_PINNED", `Cannot apply an impulse to fixed anchor ${id}.`);
276
+ accumulate(p.velocity, impulse, p.inverseMass);
277
+ },
278
+ dispose() {
279
+ if (disposed)
280
+ return;
281
+ state.bodies.length = 0;
282
+ state.particles.length = 0;
283
+ state.distances.length = 0;
284
+ state.volumes.length = 0;
285
+ state.triangles.length = 0;
286
+ disposed = true;
287
+ },
288
+ };
289
+ }
290
+ /** Restore accepts only this prototype's fixed topology; validates before replacing live state. */
291
+ function validateSnapshot(input, kind) {
292
+ try {
293
+ if (!input ||
294
+ input.format !== "pelican-m0-simulation" ||
295
+ input.version !== 1 ||
296
+ input.kind !== kind)
297
+ throw new Error("format, version, or kind mismatch");
298
+ if (!input.options ||
299
+ !isVec3(input.options.gravity) ||
300
+ !isVec3(input.options.bodyPosition) ||
301
+ !isVec3(input.options.bodyVelocity))
302
+ throw new Error("invalid options");
303
+ const expected = makeScene(kind, resolveOptions(kind, input.options));
304
+ if (!Number.isSafeInteger(input.tick) || input.tick < 0)
305
+ throw new Error("invalid tick");
306
+ for (const key of ["bodies", "particles"]) {
307
+ if (!Array.isArray(input[key]) ||
308
+ input[key].length !== expected[key].length)
309
+ throw new Error(`invalid ${key} count`);
310
+ input[key].forEach((p, i) => {
311
+ const reference = expected[key][i];
312
+ if (p.id !== reference.id ||
313
+ p.inverseMass !== reference.inverseMass ||
314
+ p.radius !== reference.radius ||
315
+ !isVec3(p.position) ||
316
+ !isVec3(p.velocity))
317
+ throw new Error(`invalid ${key}[${i}]`);
318
+ if (p.inverseMass === 0 &&
319
+ (JSON.stringify(p.position) !== JSON.stringify(reference.position) ||
320
+ length(p.velocity) !== 0))
321
+ throw new Error("anchor changed");
322
+ });
323
+ }
324
+ for (const key of ["distances", "volumes", "triangles"])
325
+ if (JSON.stringify(input[key]) !== JSON.stringify(expected[key]))
326
+ throw new Error(`invalid ${key} topology`);
327
+ const c = input.counters;
328
+ if (!c ||
329
+ !Number.isSafeInteger(c.contactCount) ||
330
+ c.contactCount < 0 ||
331
+ !isVec3(c.reactionImpulse))
332
+ throw new Error("invalid contact counters");
333
+ for (const n of [
334
+ c.absoluteReactionImpulse,
335
+ c.maxContactCorrection,
336
+ c.maxObservedSpeed,
337
+ ])
338
+ if (!Number.isFinite(n) || n < 0)
339
+ throw new Error("invalid counters");
340
+ // Validate the original before JSON copying, so NaN / Infinity cannot silently become null.
341
+ return copy({ ...input, options: expected.options });
342
+ }
343
+ catch (error) {
344
+ fail("SIM_SNAPSHOT", `Snapshot rejected without changing live state: ${error instanceof Error ? error.message : String(error)}`);
345
+ }
346
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,30 @@
1
+ import { clone } from "./geometry.js";
2
+ /** Reference consumers prepare real derived data, then swap one reference synchronously. */
3
+ export function createConsumer(name, initial, build) {
4
+ let current = initial;
5
+ return {
6
+ name,
7
+ read: () => current,
8
+ prepare(t) {
9
+ let prepared = build(t);
10
+ const previous = current;
11
+ return {
12
+ commit() {
13
+ if (prepared === undefined)
14
+ throw new Error("WORLD_PREPARED: preparation was discarded");
15
+ current = prepared;
16
+ },
17
+ rollback() {
18
+ current = previous;
19
+ prepared = undefined;
20
+ },
21
+ discard() {
22
+ prepared = undefined;
23
+ },
24
+ };
25
+ },
26
+ dispose() {
27
+ current = clone(initial);
28
+ },
29
+ };
30
+ }
@@ -0,0 +1,180 @@
1
+ export const clone = (v) => JSON.parse(JSON.stringify(v));
2
+ export const key = (c) => `${c[0]}:${c[1]}`;
3
+ export const chunkId = (c) => `chunk:${key(c)}`;
4
+ export function fail(code, message) {
5
+ throw new Error(`${code}: ${message}`);
6
+ }
7
+ export const vector = (v, length) => Array.isArray(v) &&
8
+ v.length === length &&
9
+ v.every((n) => typeof n === "number" && Number.isFinite(n));
10
+ export const coordValid = (v) => vector(v, 2) && v.every((n) => Number.isSafeInteger(n) && Math.abs(n) <= 1e9);
11
+ export function inside(c, config) {
12
+ return c.every((n, i) => n >= config.bounds.min[i] && n <= config.bounds.max[i]);
13
+ }
14
+ export function assertCoord(c, config) {
15
+ if (!coordValid(c) || !inside(c, config))
16
+ fail("WORLD_COORD", "Expected an integer chunk coordinate inside configured bounds.");
17
+ }
18
+ export function assertOrigin(o, config) {
19
+ if (!o ||
20
+ !coordValid(o.chunk) ||
21
+ !vector(o.offset, 3) ||
22
+ o.offset.some((n) => Math.abs(n) > config.chunkSize))
23
+ fail("WORLD_ORIGIN", "Expected a chunk coordinate and finite local offset within one chunk size.");
24
+ }
25
+ export function vertexKey(c, x, z, n) {
26
+ return key([c[0] * n + x, c[1] * n + z]);
27
+ }
28
+ export function baseHeight(gx, gz, seed) {
29
+ let h = Math.imul(gx | 0, 73856093) ^ Math.imul(gz | 0, 19349663) ^ seed;
30
+ h = Math.imul(h ^ (h >>> 16), 0x45d9f3b);
31
+ h = Math.imul(h ^ (h >>> 16), 0x45d9f3b);
32
+ h ^= h >>> 16;
33
+ return ((h >>> 0) / 4294967295 - 0.5) * 0.12;
34
+ }
35
+ export function buildChunk(c, version, config, edits) {
36
+ const n = config.cellsPerChunk, heights = [];
37
+ for (let z = 0; z <= n; z++)
38
+ for (let x = 0; x <= n; x++)
39
+ heights.push(edits.get(vertexKey(c, x, z, n)) ??
40
+ baseHeight(c[0] * n + x, c[1] * n + z, config.seed));
41
+ return { id: chunkId(c), coord: [...c], version, heights };
42
+ }
43
+ export function affectedChunks(vertex, config) {
44
+ const n = config.cellsPerChunk;
45
+ const candidates = (v) => v % n === 0 ? [v / n - 1, v / n] : [Math.floor(v / n)];
46
+ const result = [];
47
+ for (const x of candidates(vertex[0]))
48
+ for (const z of candidates(vertex[1]))
49
+ if (inside([x, z], config))
50
+ result.push([x, z]);
51
+ return result;
52
+ }
53
+ export function localPoint(point, origin, config) {
54
+ return [
55
+ (point.chunk[0] - origin.chunk[0]) * config.chunkSize +
56
+ point.local[0] -
57
+ origin.offset[0],
58
+ point.local[1] - origin.offset[1],
59
+ (point.chunk[1] - origin.chunk[1]) * config.chunkSize +
60
+ point.local[2] -
61
+ origin.offset[2],
62
+ ];
63
+ }
64
+ export function normalizePoint(point, config) {
65
+ const dx = Math.floor(point.local[0] / config.chunkSize), dz = Math.floor(point.local[2] / config.chunkSize);
66
+ return {
67
+ chunk: [point.chunk[0] + dx, point.chunk[1] + dz],
68
+ local: [
69
+ point.local[0] - dx * config.chunkSize,
70
+ point.local[1],
71
+ point.local[2] - dz * config.chunkSize,
72
+ ],
73
+ };
74
+ }
75
+ export function heightAt(heights, n, x, z) {
76
+ const ix = Math.min(n - 1, Math.max(0, Math.floor(x))), iz = Math.min(n - 1, Math.max(0, Math.floor(z)));
77
+ const fx = x - ix, fz = z - iz, a = iz * (n + 1) + ix;
78
+ const ha = heights[a], hb = heights[a + 1], hc = heights[a + n + 1], hd = heights[a + n + 2];
79
+ return fx + fz <= 1
80
+ ? ha + (hb - ha) * fx + (hc - ha) * fz
81
+ : hd + (hc - hd) * (1 - fx) + (hb - hd) * (1 - fz);
82
+ }
83
+ export function renderBuild(t) {
84
+ const n = t.config.cellsPerChunk, step = t.config.chunkSize / n;
85
+ return {
86
+ epoch: t.epoch,
87
+ origin: clone(t.origin),
88
+ chunks: t.chunks.map((c) => {
89
+ const positions = [], indices = [];
90
+ for (let z = 0; z <= n; z++)
91
+ for (let x = 0; x <= n; x++)
92
+ positions.push(...localPoint({
93
+ chunk: c.coord,
94
+ local: [x * step, c.heights[z * (n + 1) + x], z * step],
95
+ }, t.origin, t.config));
96
+ for (let z = 0; z < n; z++)
97
+ for (let x = 0; x < n; x++) {
98
+ const a = z * (n + 1) + x;
99
+ indices.push(a, a + n + 1, a + 1, a + 1, a + n + 1, a + n + 2);
100
+ }
101
+ return {
102
+ id: c.id,
103
+ coord: [...c.coord],
104
+ version: c.version,
105
+ positions,
106
+ indices,
107
+ };
108
+ }),
109
+ };
110
+ }
111
+ export function collisionBuild(t) {
112
+ return {
113
+ epoch: t.epoch,
114
+ origin: clone(t.origin),
115
+ chunks: t.chunks.map((c) => ({
116
+ id: c.id,
117
+ coord: [...c.coord],
118
+ version: c.version,
119
+ heights: [...c.heights],
120
+ cells: t.config.cellsPerChunk,
121
+ cellSize: t.config.chunkSize / t.config.cellsPerChunk,
122
+ offset: localPoint({ chunk: c.coord, local: [0, 0, 0] }, t.origin, t.config),
123
+ })),
124
+ };
125
+ }
126
+ export const navId = (a) => `${chunkId(a.chunk)}/cell:${a.cell[0]}:${a.cell[1]}`;
127
+ export function navBuild(t) {
128
+ const n = t.config.cellsPerChunk, step = t.config.chunkSize / n;
129
+ const result = {
130
+ epoch: t.epoch,
131
+ origin: clone(t.origin),
132
+ versions: t.chunks.map((c) => ({ id: c.id, version: c.version })),
133
+ nodes: [],
134
+ blockedCells: 0,
135
+ };
136
+ const grid = new Map();
137
+ for (const c of t.chunks)
138
+ for (let z = 0; z < n; z++)
139
+ for (let x = 0; x < n; x++) {
140
+ const a = z * (n + 1) + x, hs = [
141
+ c.heights[a],
142
+ c.heights[a + 1],
143
+ c.heights[a + n + 1],
144
+ c.heights[a + n + 2],
145
+ ];
146
+ if (Math.max(...hs) - Math.min(...hs) > t.config.navMaxStep) {
147
+ result.blockedCells++;
148
+ continue;
149
+ }
150
+ const height = heightAt(c.heights, n, x + 0.5, z + 0.5);
151
+ const node = {
152
+ id: navId({ chunk: c.coord, cell: [x, z] }),
153
+ coord: [...c.coord],
154
+ cell: [x, z],
155
+ height,
156
+ position: localPoint({
157
+ chunk: c.coord,
158
+ local: [(x + 0.5) * step, height, (z + 0.5) * step],
159
+ }, t.origin, t.config),
160
+ neighbors: [],
161
+ };
162
+ result.nodes.push(node);
163
+ grid.set(key([c.coord[0] * n + x, c.coord[1] * n + z]), node);
164
+ }
165
+ for (const node of result.nodes)
166
+ for (const [dx, dz] of [
167
+ [1, 0],
168
+ [-1, 0],
169
+ [0, 1],
170
+ [0, -1],
171
+ ]) {
172
+ const other = grid.get(key([
173
+ node.coord[0] * n + node.cell[0] + dx,
174
+ node.coord[1] * n + node.cell[1] + dz,
175
+ ]));
176
+ if (other && Math.abs(other.height - node.height) <= t.config.navMaxStep)
177
+ node.neighbors.push(other.id);
178
+ }
179
+ return result;
180
+ }
@@ -0,0 +1,2 @@
1
+ export { createWorldProbe } from "./world.js";
2
+ export { runWorldProbe } from "./probe.js";