lecodes-gl-lite 2.0.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 (46) hide show
  1. package/README.md +27 -0
  2. package/package.json +33 -0
  3. package/src/assets/decoders/README.md +13 -0
  4. package/src/assets/decoders/basis_transcoder.js +19 -0
  5. package/src/assets/decoders/basis_transcoder.wasm +0 -0
  6. package/src/assets/decoders/draco_decoder.wasm +0 -0
  7. package/src/assets/decoders/draco_wasm_wrapper.js +117 -0
  8. package/src/assets/neutral_ibl128.ktx +0 -0
  9. package/src/core/animBridge.ts +51 -0
  10. package/src/core/boneNames.ts +22 -0
  11. package/src/core/frame.ts +230 -0
  12. package/src/core/glbStructure.ts +100 -0
  13. package/src/core/headlessBridge.ts +92 -0
  14. package/src/core/mapView.ts +195 -0
  15. package/src/core/math3d.ts +275 -0
  16. package/src/core/particleBridge.ts +154 -0
  17. package/src/core/physicsBridge.ts +305 -0
  18. package/src/core/recording.ts +59 -0
  19. package/src/core/sceneCore.ts +833 -0
  20. package/src/core/serialize.ts +144 -0
  21. package/src/gen/unsupported.gen.ts +212 -0
  22. package/src/index.ts +5 -0
  23. package/src/vendor/creator-anim/dist/creatorAnim.d.ts +123 -0
  24. package/src/vendor/creator-anim/dist/creatorAnim.js +14 -0
  25. package/src/vendor/creator-anim/js/index.ts +656 -0
  26. package/src/vendor/creator-nav/dist/creatorNav.d.ts +56 -0
  27. package/src/vendor/creator-nav/dist/creatorNav.js +14 -0
  28. package/src/vendor/creator-nav/js/index.ts +400 -0
  29. package/src/vendor/creator-particles/dist/creatorParticles.d.ts +43 -0
  30. package/src/vendor/creator-particles/dist/creatorParticles.js +14 -0
  31. package/src/vendor/creator-particles/js/index.ts +137 -0
  32. package/src/vendor/creator-physics/dist/creatorPhysics.d.ts +107 -0
  33. package/src/vendor/creator-physics/dist/creatorPhysics.js +14 -0
  34. package/src/vendor/creator-physics/js/index.ts +515 -0
  35. package/src/vite-env.d.ts +1 -0
  36. package/src/webgl/batching.ts +453 -0
  37. package/src/webgl/createViewerLite.ts +1627 -0
  38. package/src/webgl/customMaterial.ts +228 -0
  39. package/src/webgl/decoders/basis.ts +123 -0
  40. package/src/webgl/decoders/draco.ts +110 -0
  41. package/src/webgl/decoders/index.ts +68 -0
  42. package/src/webgl/decoders/worker.ts +45 -0
  43. package/src/webgl/editor.ts +1297 -0
  44. package/src/webgl/glb.ts +593 -0
  45. package/src/webgl/ibl.ts +137 -0
  46. package/src/webgl/shaders.ts +779 -0
@@ -0,0 +1,305 @@
1
+ // The web hosts' physics system: creator-physics' wasm (JoltPhysics) behind the same `_creator`
2
+ // physics / character / vehicle / ragdoll method shapes creator-gl exposes natively — ONE simulation
3
+ // for every host, no kinematic stand-in. Shared by viewer-lite's WebGL engine and lecodes-headless
4
+ // (headless); the host only supplies its scene graph (`PhysicsHost`).
5
+ //
6
+ // Lazy like the animation system: the module loads asynchronously (`loadCreatorPhysics()`, awaited by
7
+ // createViewerLite / runProject before the scene core exists), and the real bridge is created on the
8
+ // first physics call. `physicsHasSupport()` answers whether the module is resident — a host that never
9
+ // loaded it reports no physics and the SDK's aspects no-op, exactly like a physics-less native build.
10
+ //
11
+ // On top of the simulation this layer RECORDS what the SDK configured onto the entities (Ent.physics /
12
+ // Ent.ragdoll + the world config) for the headless JSON snapshots: the collider a node carries, its
13
+ // motion / mass / friction, a vehicle's tunables, a ragdoll's parts — so `lecodes render` shows the
14
+ // setup, and the simulation shows what it did.
15
+
16
+ import { createPhysicsBridge, loadCreatorPhysics, isCreatorPhysicsLoaded, type PhysicsHost, type PhysicsBridge } from "../vendor/creator-physics/js/index"
17
+ import type { Ent } from "./sceneCore"
18
+
19
+ export { loadCreatorPhysics, isCreatorPhysicsLoaded }
20
+ export type { PhysicsHost, PhysicsBridge }
21
+
22
+ type Methods = PhysicsBridge["methods"]
23
+ const METHOD_NAMES: (keyof Methods)[] = [
24
+ "physicsConfigure", "setInterpolation",
25
+ "physicsBuildBox", "physicsBuildSphere", "physicsBuildCylinder", "physicsBuildCapsule", "physicsBuildMesh",
26
+ "physicsBuildMeshFromEntity", "physicsBuildHeightField", "physicsUpdateHeightField", "glbTriangles",
27
+ "physicsSetShapeOrigin", "physicsSetBodyShape", "physicsDestroyShape",
28
+ "physicsCreateBody", "physicsSetPickable", "physicsSetFriction", "physicsGetFriction", "physicsRaycast",
29
+ "physicsCreateBox", "physicsCreateSphere", "physicsCreateCylinder",
30
+ "physicsSetLinearVelocity", "physicsGetLinearVelocity", "physicsSetAngularVelocity", "physicsGetAngularVelocity",
31
+ "physicsApplyImpulse", "physicsApplyImpulseAt", "physicsSetBodyPosition", "physicsSetBodyRotation", "physicsRemoveBody",
32
+ "characterCreate", "characterDestroy", "characterMove", "characterMoveFree", "characterSetVerticalVelocity",
33
+ "characterSetVelocity", "characterSetGravityScale", "characterSetMaxSlope", "characterSetShape",
34
+ "characterGetVelocity", "characterGetGroundState", "characterSetPosition",
35
+ "vehicleCreate", "vehicleDestroy", "vehicleSetInput", "vehicleSetTuning",
36
+ "vehicleGetState", "vehicleReset", "vehicleBodyId",
37
+ "ragdollCreate", "ragdollDestroy", "ragdollActivate", "ragdollDeactivate", "ragdollActive", "ragdollBodyId", "ragdollSetDrive",
38
+ "setOnPhysicsEvent",
39
+ ]
40
+
41
+ export type PhysicsSystemHost = PhysicsHost & { ent(id: number): Ent }
42
+
43
+ const FIXED_DT = 1 / 60
44
+ const MOTION = ["static", "kinematic", "dynamic"]
45
+
46
+ // Vehicle settings blob v15 — see bridges.d.ts for the field-by-field layout.
47
+ const VEHICLE_HEADER = 34
48
+ const VEHICLE_WHEEL = 15
49
+ const VEHICLE_WHEELS_AT = 32
50
+ const VEHICLE_CURVES_AT = 33
51
+ /** The half of the blob a live retune may change, decoded for the snapshot. */
52
+ const vehicleTunables = (s: Float32Array, curveCount: number): Record<string, unknown> => ({
53
+ engine: { torque: s[4], maxRpm: s[5], idleRpm: s[6], inertia: s[7], braking: s[8] },
54
+ clutchStrength: s[9],
55
+ clutchCapacity: s[31],
56
+ differential: s[10]! <= 0 ? "open" : s[10],
57
+ antiRoll: s[11],
58
+ maxTilt: s[12],
59
+ aero: { downforce: s[13], drag: s[14] },
60
+ steering: s[15] ? { feedback: { inertia: s[16], damping: s[17], parkingFriction: s[18], powerSteering: s[24], friction: s[29] }, caster: s[19], trail: s[20] } : { caster: s[19], trail: s[20] },
61
+ ...(curveCount > 0
62
+ ? { torqueCurve: Array.from(s.subarray(VEHICLE_HEADER, VEHICLE_HEADER + curveCount * 2)) }
63
+ : {}),
64
+ })
65
+
66
+ export const createPhysicsSystem = (host: PhysicsSystemHost) => {
67
+ let real: PhysicsBridge | null = null
68
+ const get = (): PhysicsBridge => {
69
+ if (!real) {
70
+ if (!isCreatorPhysicsLoaded()) throw new Error("creator-physics is not loaded: await loadCreatorPhysics() (viewer-lite/src/gl/core/physicsBridge) before the first physics call")
71
+ real = createPhysicsBridge(host)
72
+ }
73
+ return real
74
+ }
75
+ const sim = {} as Methods
76
+ for (const name of METHOD_NAMES) {
77
+ (sim as Record<string, unknown>)[name] = (...args: unknown[]) => (get().methods[name] as (...a: unknown[]) => unknown)(...args)
78
+ }
79
+
80
+ // ---- the JSON record (what the SDK configured; the simulation moves the entities) ----
81
+ const shapes = new Map<number, Record<string, unknown>>()
82
+ const bodyOwner = new Map<number, number>() // bodyId → entity
83
+ const charOwner = new Map<number, number>() // charId → entity
84
+ const vehicleOwner = new Map<number, number>() // vehicleId → entity
85
+ const ragdollOwner = new Map<number, number>() // ragdollId → model entity
86
+ let config: Record<string, unknown> | undefined
87
+ const shapeRec = (id: number, rec: Record<string, unknown>): number => { if (id) shapes.set(id, rec); return id }
88
+ const physicsOf = (owner: Map<number, number>, id: number): Record<string, unknown> | undefined => {
89
+ const e = owner.get(id)
90
+ return e === undefined ? undefined : host.ent(e).physics
91
+ }
92
+ const legacyBody = (id: number, entityId: number, motion: number, mass: number, shape: Record<string, unknown>): number => {
93
+ if (id) { bodyOwner.set(id, entityId); host.ent(entityId).physics = { motion: MOTION[motion] ?? motion, mass, shape } }
94
+ return id
95
+ }
96
+
97
+ const methods = {
98
+ ...sim,
99
+ physicsHasSupport: (): boolean => isCreatorPhysicsLoaded(),
100
+ physicsConfigure(gx: number, gy: number, gz: number, maxBodies: number): void {
101
+ config = { gravity: [gx, gy, gz], maxBodies }
102
+ sim.physicsConfigure(gx, gy, gz, maxBodies)
103
+ },
104
+ physicsBuildBox: (hx: number, hy: number, hz: number): number =>
105
+ shapeRec(sim.physicsBuildBox(hx, hy, hz), { type: "box", size: [hx * 2, hy * 2, hz * 2] }),
106
+ physicsBuildSphere: (radius: number): number =>
107
+ shapeRec(sim.physicsBuildSphere(radius), { type: "sphere", radius }),
108
+ physicsBuildCylinder: (halfHeight: number, radius: number): number =>
109
+ shapeRec(sim.physicsBuildCylinder(halfHeight, radius), { type: "cylinder", height: halfHeight * 2, radius }),
110
+ physicsBuildCapsule: (halfHeight: number, radius: number): number =>
111
+ shapeRec(sim.physicsBuildCapsule(halfHeight, radius), { type: "capsule", height: halfHeight * 2, radius }),
112
+ physicsBuildMesh: (vertices: Float32Array, indices: Uint32Array, convex: boolean, sx: number, sy: number, sz: number): number =>
113
+ shapeRec(sim.physicsBuildMesh(vertices, indices, convex, sx, sy, sz),
114
+ { type: convex ? "convexHull" : "mesh", vertices: vertices.length / 3, triangles: Math.floor(indices.length / 3), scale: [sx, sy, sz] }),
115
+ physicsBuildMeshFromEntity: (entityId: number, convex: boolean, sx: number, sy: number, sz: number): number =>
116
+ shapeRec(sim.physicsBuildMeshFromEntity(entityId, convex, sx, sy, sz),
117
+ { type: convex ? "convexHull" : "mesh", source: entityId, scale: [sx, sy, sz] }),
118
+ physicsBuildHeightField(heights: Float32Array, sizeX: number, sizeZ: number, cellSize: number, holes: Uint8Array | null, sx: number, sy: number, sz: number): number {
119
+ let holeCount = 0
120
+ if (holes) for (let i = 0; i < holes.length; i++) if (holes[i]) holeCount++
121
+ return shapeRec(sim.physicsBuildHeightField(heights, sizeX, sizeZ, cellSize, holes, sx, sy, sz),
122
+ { type: "heightfield", size: [sizeX, sizeZ], cellSize, samples: heights.length, holes: holeCount, scale: [sx, sy, sz] })
123
+ },
124
+ // Shape `origin`: on the shape record too (a JSON snapshot shows where the collider sits relative
125
+ // to the node). Zero clears it, matching the engine's "set, don't accumulate" rule.
126
+ physicsSetShapeOrigin(shapeId: number, x: number, y: number, z: number): void {
127
+ const shape = shapes.get(shapeId)
128
+ if (shape) { if (x === 0 && y === 0 && z === 0) delete shape.origin; else shape.origin = [x, y, z] }
129
+ sim.physicsSetShapeOrigin(shapeId, x, y, z)
130
+ },
131
+ physicsSetBodyShape(bodyId: number, shapeId: number, updateMass: boolean): void {
132
+ const p = physicsOf(bodyOwner, bodyId)
133
+ if (p) p.shape = shapes.get(shapeId)
134
+ sim.physicsSetBodyShape(bodyId, shapeId, updateMass)
135
+ },
136
+ physicsDestroyShape(shapeId: number): void { shapes.delete(shapeId); sim.physicsDestroyShape(shapeId) },
137
+ physicsCreateBody(entityId: number, shapeId: number, motion: number, mass: number, sensor: boolean, pickOnly: boolean): number {
138
+ const id = sim.physicsCreateBody(entityId, shapeId, motion, mass, sensor, pickOnly)
139
+ if (id) {
140
+ bodyOwner.set(id, entityId)
141
+ host.ent(entityId).physics = {
142
+ motion: MOTION[motion] ?? motion, mass,
143
+ ...(sensor && { sensor }), ...(pickOnly && { pickOnly }),
144
+ shape: shapes.get(shapeId),
145
+ }
146
+ }
147
+ return id
148
+ },
149
+ physicsSetFriction(bodyId: number, friction: number): void {
150
+ const p = physicsOf(bodyOwner, bodyId)
151
+ if (p) p.friction = friction
152
+ sim.physicsSetFriction(bodyId, friction)
153
+ },
154
+ physicsRemoveBody(bodyId: number): void { bodyOwner.delete(bodyId); sim.physicsRemoveBody(bodyId) },
155
+ physicsCreateBox: (entityId: number, hx: number, hy: number, hz: number, motionType: number, mass: number): number =>
156
+ legacyBody(sim.physicsCreateBox(entityId, hx, hy, hz, motionType, mass), entityId, motionType, mass, { type: "box", size: [hx * 2, hy * 2, hz * 2] }),
157
+ physicsCreateSphere: (entityId: number, radius: number, motionType: number, mass: number): number =>
158
+ legacyBody(sim.physicsCreateSphere(entityId, radius, motionType, mass), entityId, motionType, mass, { type: "sphere", radius }),
159
+ physicsCreateCylinder: (entityId: number, halfHeight: number, radius: number, motionType: number, mass: number): number =>
160
+ legacyBody(sim.physicsCreateCylinder(entityId, halfHeight, radius, motionType, mass), entityId, motionType, mass, { type: "cylinder", height: halfHeight * 2, radius }),
161
+
162
+ characterCreate(entityId: number, shapeId: number, maxSlopeDeg: number): number {
163
+ const id = sim.characterCreate(entityId, shapeId, maxSlopeDeg)
164
+ if (id) {
165
+ charOwner.set(id, entityId)
166
+ host.ent(entityId).physics = { character: true, maxSlopeDeg, shape: shapes.get(shapeId) }
167
+ }
168
+ return id
169
+ },
170
+ characterDestroy(charId: number): void { charOwner.delete(charId); sim.characterDestroy(charId) },
171
+ characterSetMaxSlope(charId: number, maxSlopeDeg: number): void {
172
+ const p = physicsOf(charOwner, charId)
173
+ if (p) p.maxSlopeDeg = maxSlopeDeg
174
+ sim.characterSetMaxSlope(charId, maxSlopeDeg)
175
+ },
176
+ characterSetShape(charId: number, shapeId: number): boolean {
177
+ const ok = sim.characterSetShape(charId, shapeId)
178
+ if (ok) { const p = physicsOf(charOwner, charId); if (p) p.shape = shapes.get(shapeId) }
179
+ return ok
180
+ },
181
+
182
+ // The blob layout is the one in bridges.d.ts (v5) — decoded back to readable keys so a JSON
183
+ // snapshot shows the car the SDK actually configured.
184
+ vehicleCreate(entityId: number, shapeId: number, settings: Float32Array): number {
185
+ const id = sim.vehicleCreate(entityId, shapeId, settings)
186
+ if (!id) return 0
187
+ const wheelCount = settings[VEHICLE_WHEELS_AT] ?? 0
188
+ const curveCount = settings[VEHICLE_CURVES_AT] ?? 0
189
+ let w = VEHICLE_HEADER + curveCount * 2
190
+ const wheels: Record<string, unknown>[] = []
191
+ for (let i = 0; i < wheelCount; i++) {
192
+ // Variable length: the fixed slots, then the two tire curves (their point counts are the last two).
193
+ const sideCount = settings[w + 13] ?? 0
194
+ const forwardCount = settings[w + 14] ?? 0
195
+ wheels.push({
196
+ position: [settings[w], settings[w + 1], settings[w + 2]],
197
+ radius: settings[w + 3], width: settings[w + 4],
198
+ ...(settings[w + 5] ? { steerDeg: settings[w + 5] } : {}),
199
+ ...(settings[w + 6] ? { driven: settings[w + 6] } : {}),
200
+ axle: settings[w + 7],
201
+ suspension: { travel: settings[w + 8], stiffness: settings[w + 9], damping: settings[w + 10] },
202
+ tire: { traction: settings[w + 11], circle: settings[w + 12], side: sideCount, forward: forwardCount },
203
+ })
204
+ w += VEHICLE_WHEEL + (sideCount + forwardCount) * 2
205
+ }
206
+ vehicleOwner.set(id, entityId)
207
+ host.ent(entityId).physics = {
208
+ motion: "dynamic", mass: settings[1], vehicle: true,
209
+ ...vehicleTunables(settings, curveCount),
210
+ wheels, shape: shapes.get(shapeId),
211
+ }
212
+ return id
213
+ },
214
+ vehicleDestroy(vehicleId: number): void { vehicleOwner.delete(vehicleId); sim.vehicleDestroy(vehicleId) },
215
+ vehicleSetTuning(vehicleId: number, settings: Float32Array): void {
216
+ const p = physicsOf(vehicleOwner, vehicleId)
217
+ if (p) Object.assign(p, vehicleTunables(settings, settings[VEHICLE_CURVES_AT] ?? 0))
218
+ sim.vehicleSetTuning(vehicleId, settings)
219
+ },
220
+ vehicleSetInput(vehicleId: number, input: Float32Array): void {
221
+ const p = physicsOf(vehicleOwner, vehicleId)
222
+ if (p) p.input = { throttle: input[0], steer: input[1], steerForce: input[2], ratio: input[3], clutch: input[4], brakes: Array.from(input.subarray(5)) }
223
+ sim.vehicleSetInput(vehicleId, input)
224
+ },
225
+
226
+ // The parts are decoded from the blob (layout in bridges.d.ts) onto the model entity so a JSON
227
+ // snapshot shows the skeleton the SDK handed to physics.
228
+ ragdollCreate(rootEntityId: number, bones: Uint32Array, settings: Float32Array): number {
229
+ const id = sim.ragdollCreate(rootEntityId, bones, settings)
230
+ if (!id) return 0
231
+ const header = 16 // blob v3 (bridges.d.ts)
232
+ const count = settings[1] ?? 0, stride = settings[2] ?? 12
233
+ const parts: Record<string, unknown>[] = []
234
+ for (let i = 0; i < count; i++) {
235
+ const o = header + i * stride
236
+ parts.push({
237
+ bone: bones[i * 2], ...(bones[i * 2 + 1] ? { to: bones[i * 2 + 1] } : {}),
238
+ parent: settings[o], radius: settings[o + 1], ...(settings[o + 2] ? { length: settings[o + 2] } : {}),
239
+ mass: settings[o + 3],
240
+ ...(settings[o + 6]
241
+ ? { hinge: { axis: [settings[o + 7], settings[o + 8], settings[o + 9]], min: settings[o + 10], max: settings[o + 11] } }
242
+ : { swing: settings[o + 4], twist: settings[o + 5] }),
243
+ })
244
+ }
245
+ ragdollOwner.set(id, rootEntityId)
246
+ host.ent(rootEntityId).ragdoll = {
247
+ friction: settings[3],
248
+ collide: settings[6] ? "static" : "all", freeze: !!settings[7], freezeAfter: settings[8] ?? 0,
249
+ drive: { frequency: settings[9], damping: settings[10], torque: settings[11] }, jointFriction: settings[12],
250
+ limits: settings[13] ? "clips" : "table", limitsMargin: settings[14],
251
+ parts,
252
+ }
253
+ return id
254
+ },
255
+ ragdollDestroy(ragdollId: number): void { ragdollOwner.delete(ragdollId); sim.ragdollDestroy(ragdollId) },
256
+ ragdollActivate(ragdollId: number, vx: number, vy: number, vz: number): boolean {
257
+ const ok = sim.ragdollActivate(ragdollId, vx, vy, vz)
258
+ const e = ragdollOwner.get(ragdollId)
259
+ const rag = e === undefined ? undefined : host.ent(e).ragdoll
260
+ if (ok && rag) { rag.active = true; rag.launch = [vx, vy, vz] }
261
+ return ok
262
+ },
263
+ ragdollDeactivate(ragdollId: number, blend: number): void {
264
+ const e = ragdollOwner.get(ragdollId)
265
+ const rag = e === undefined ? undefined : host.ent(e).ragdoll
266
+ if (rag) { rag.active = false; delete rag.launch; if (blend > 0) rag.blendOut = blend }
267
+ sim.ragdollDeactivate(ragdollId, blend)
268
+ },
269
+ ragdollSetDrive(ragdollId: number, strength: number, anchor: boolean): void {
270
+ const e = ragdollOwner.get(ragdollId)
271
+ const rag = e === undefined ? undefined : host.ent(e).ragdoll
272
+ if (rag) { rag.strength = strength; rag.anchored = anchor }
273
+ sim.ragdollSetDrive(ragdollId, strength, anchor)
274
+ },
275
+ }
276
+
277
+ let acc = 0
278
+ return {
279
+ methods,
280
+ /** The simulation exists (something was created). */
281
+ loaded: (): boolean => real !== null,
282
+ /** One frame of GAME time: the fixed steps with the SDK's fixed phase between them, the render
283
+ * poses onto the entities, the contact events. Without a simulation (nothing created yet, or the
284
+ * module never loaded) the fixed phase still runs on its own 1/60 accumulator — creator-gl's
285
+ * physics-less build does the same (init.cpp). */
286
+ frame(dt: number, fixedUpdate: ((dt: number) => void) | null): void {
287
+ if (real) { real.frame(dt, fixedUpdate); return }
288
+ if (!fixedUpdate) { acc = 0; return }
289
+ acc += dt
290
+ let steps = 0
291
+ while (acc >= FIXED_DT && steps < 4) { fixedUpdate(FIXED_DT); acc -= FIXED_DT; steps++ }
292
+ if (steps === 4) acc = 0
293
+ },
294
+ /** After the animators: the animated poses are cached (velocities / the motors' target), then the
295
+ * active ragdolls' bodies overwrite their bones. */
296
+ syncRagdolls(dt: number): void { if (real) real.syncRagdolls(dt) },
297
+ /** Root motion → a CharacterController on the node or above it. False = nobody took it. */
298
+ applyRootMotion: (entityId: number, wx: number, wz: number, dt: number, yaw: number): boolean =>
299
+ real ? real.applyRootMotion(entityId, wx, wz, dt, yaw) : false,
300
+ /** The world config the SDK set (serialized at scene level), undefined if never configured. */
301
+ config: (): Record<string, unknown> | undefined => config,
302
+ }
303
+ }
304
+
305
+ export type PhysicsSystem = ReturnType<typeof createPhysicsSystem>
@@ -0,0 +1,59 @@
1
+ // Headless recording method groups for the lite 3D scene core — the parts of the `_creator` bridge
2
+ // whose behavior legitimately differs per consumer (gl-lite-core-plan.md "Capabilities"): AR/VR &
3
+ // anchors, colliders, scene-material and capture. Headless records them (so SDK code paths execute
4
+ // and get captured); the WebGL backend supplies its own honest versions (AR throws). All of these
5
+ // mutate the shared core state (entity.collider/anchor, scene.arMode), so a recorded scene serializes
6
+ // with the parameters the SDK actually set.
7
+ // Physics, particles and noise are NOT recorded here any more: the core simulates them for real
8
+ // (physicsBridge.ts / particleBridge.ts — creator-physics' and creator-particles' wasm) and records
9
+ // the configuration alongside.
10
+
11
+ import type { SceneCore } from "./sceneCore"
12
+
13
+ export type RecordingHandle = {
14
+ methods: Record<string, (...args: any[]) => any>
15
+ }
16
+
17
+ export function createRecording(core: SceneCore): RecordingHandle {
18
+ const { ent, newEnt, scenes, sourceName, materials } = core
19
+
20
+ const methods = {
21
+ // --- AR / VR lifecycle (permissive: records + completes) ---
22
+ launchAR(_sceneId: number, onComplete: () => void): void { onComplete() },
23
+ stopAR(): void {},
24
+ launchVR(_sceneId: number, onComplete: () => void): void { onComplete() },
25
+ stopVR(): void {},
26
+ warmRender(_sceneId: number, onComplete: () => void): void { onComplete() },
27
+
28
+ // --- colliders (recorded on the entity for serialization; the CPU pick path has no raycast headless) ---
29
+ setColliderFromMesh(entityId: number, _meshEntityId: number, form: number): void { ent(entityId).collider = { type: "mesh", form } },
30
+ setColliderBox(entityId: number, cx: number, cy: number, cz: number, sx: number, sy: number, sz: number): void {
31
+ ent(entityId).collider = { type: "box", center: [cx, cy, cz], size: [sx, sy, sz] }
32
+ },
33
+ setColliderSphere(entityId: number, cx: number, cy: number, cz: number, radius: number): void {
34
+ ent(entityId).collider = { type: "sphere", center: [cx, cy, cz], radius }
35
+ },
36
+
37
+ // --- AR controllers + anchors ---
38
+ createARController(sceneId: number, _cameraId: number, mode: string): void {
39
+ const s = scenes.get(sceneId)
40
+ if (s) s.arMode = mode
41
+ },
42
+ createRootAnchor(_sceneId: number): number {
43
+ const e = newEnt()
44
+ e.anchor = "root"
45
+ return e.id
46
+ },
47
+ createAnchor(_sceneId: number, physicalWidth: number, systemId: number): number {
48
+ const e = newEnt()
49
+ e.anchor = sourceName(systemId) ?? "image"
50
+ void physicalWidth
51
+ return e.id
52
+ },
53
+
54
+ // --- capture (can't rasterize headless) ---
55
+ captureImage(_sceneId: number, _onComplete: unknown, onReject: () => void): void { onReject() },
56
+ }
57
+
58
+ return { methods }
59
+ }