lecodes-viewer 0.19.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 (103) hide show
  1. package/README.md +5 -0
  2. package/dist-embed/VERSION.json +5 -0
  3. package/dist-embed/assets/Roboto-Variable-DHm9jpd-.woff2 +0 -0
  4. package/dist-embed/assets/__vite-browser-external-BIHI7g3E.js +1 -0
  5. package/dist-embed/assets/basis-C64VHDVD.js +1 -0
  6. package/dist-embed/assets/basis_transcoder-VXdx5NbI.wasm +0 -0
  7. package/dist-embed/assets/createViewer2d-D12Wy3VF.js +1 -0
  8. package/dist-embed/assets/createViewerLite-CP-WHGLg.js +707 -0
  9. package/dist-embed/assets/creator-gl-BF2CQzeI.wasm +0 -0
  10. package/dist-embed/assets/creator-gl.js-B_0GrQRe.symbols +5379 -0
  11. package/dist-embed/assets/creator-ui-BX5Fvnbt.js +1 -0
  12. package/dist-embed/assets/creator-ui-CzKHLH2Q.wasm +0 -0
  13. package/dist-embed/assets/creator2d-BtQ-1_B0.wasm +0 -0
  14. package/dist-embed/assets/draco-BiISTFcR.js +118 -0
  15. package/dist-embed/assets/draco_decoder-DsQ12WqX.wasm +0 -0
  16. package/dist-embed/assets/host-CWKB1WDd.js +162 -0
  17. package/dist-embed/assets/index-CIK8PnUA.js +2 -0
  18. package/dist-embed/assets/lit-DUE9rhY9.filamat +0 -0
  19. package/dist-embed/assets/loadCreatorUiNode-UAdhjRTd.js +1 -0
  20. package/dist-embed/assets/neutral_ibl128-D-lzdkLN.ktx +0 -0
  21. package/dist-embed/assets/particles-DxYDmI1O.filamat +0 -0
  22. package/dist-embed/assets/particles-quad-DwYo9yCK.filamat +0 -0
  23. package/dist-embed/assets/shadow-C9xJpqlY.filamat +0 -0
  24. package/dist-embed/assets/uberarchive-uXjRYa9p.bin +0 -0
  25. package/dist-embed/assets/unlit-pe4tFY5b.filamat +0 -0
  26. package/dist-embed/assets/unlit-transparent-D9OyaOBR.filamat +0 -0
  27. package/dist-embed/assets/video-eedvhMvS.filamat +0 -0
  28. package/dist-embed/assets/worker-Caf-yYEI.js +2 -0
  29. package/dist-embed/embed.html +48 -0
  30. package/dist-embed/embed.js +187 -0
  31. package/embed.html +48 -0
  32. package/package.json +35 -0
  33. package/src/assets/banner.png +0 -0
  34. package/src/assets/image2.png +0 -0
  35. package/src/assets/landing.mp4 +0 -0
  36. package/src/assets/neutral_ibl128.ktx +0 -0
  37. package/src/assets/neutral_ibl256.ktx +0 -0
  38. package/src/assets/output.mp4 +0 -0
  39. package/src/assets/output2.mp4 +0 -0
  40. package/src/assets/texture.png +0 -0
  41. package/src/assets/vue.svg +1 -0
  42. package/src/canvas/createCanvasHost.ts +2 -0
  43. package/src/createViewer.ts +103 -0
  44. package/src/embed.ts +515 -0
  45. package/src/host.ts +428 -0
  46. package/src/lite-test.ts +160 -0
  47. package/src/main.ts +77 -0
  48. package/src/sourcemapError.ts +2 -0
  49. package/src/ui/CuiProps.gen.ts +103 -0
  50. package/src/ui/UIScreen.ts +486 -0
  51. package/src/ui/createBareUI.ts +32 -0
  52. package/src/ui/createMeasureSystem.ts +220 -0
  53. package/src/ui/createNodeElement.ts +77 -0
  54. package/src/ui/createPagerSystem.ts +345 -0
  55. package/src/ui/createTouchSystem.ts +2 -0
  56. package/src/ui/createUI.ts +806 -0
  57. package/src/ui/creator-ui.js +14 -0
  58. package/src/ui/creator-ui.wasm +0 -0
  59. package/src/ui/defaultStyle.css +176 -0
  60. package/src/ui/layers.ts +9 -0
  61. package/src/ui/mountCanvasUI.ts +213 -0
  62. package/src/ui/nativeViewSystem.ts +91 -0
  63. package/src/ui/presentPlayer.ts +156 -0
  64. package/src/ui/props-apply.ts +403 -0
  65. package/src/ui/router.ts +2 -0
  66. package/src/ui/safeAreaSpec.ts +3 -0
  67. package/src/ui/shells.ts +37 -0
  68. package/src/ui/toasts.ts +2 -0
  69. package/src/ui/transitions.ts +250 -0
  70. package/src/ui/ui.d.ts +78 -0
  71. package/src/ui/vListSystem.ts +212 -0
  72. package/src/ui/viewScale.ts +2 -0
  73. package/src/utils/animate.ts +2 -0
  74. package/src/utils/createUtils.ts +2 -0
  75. package/src/utils/fetch.ts +3 -0
  76. package/src/utils/fonts.ts +2 -0
  77. package/src/utils/globals.ts +2 -0
  78. package/src/utils/imageNode.ts +260 -0
  79. package/src/utils/openFilePicker.ts +2 -0
  80. package/src/utils/svg.ts +2 -0
  81. package/src/utils/videoNode.ts +2 -0
  82. package/src/utils/ws.ts +2 -0
  83. package/src/viewer/createViewer.ts +993 -0
  84. package/src/viewer/creator-gl.js +14 -0
  85. package/src/viewer/creator-gl.js.symbols +5379 -0
  86. package/src/viewer/creator-gl.wasm +0 -0
  87. package/src/viewer/lit.filamat +0 -0
  88. package/src/viewer/mediaplayer.ts +3 -0
  89. package/src/viewer/particles-quad.filamat +0 -0
  90. package/src/viewer/particles.filamat +0 -0
  91. package/src/viewer/physics.ts +140 -0
  92. package/src/viewer/shadow.filamat +0 -0
  93. package/src/viewer/uberarchive.bin +0 -0
  94. package/src/viewer/unlit-transparent.filamat +0 -0
  95. package/src/viewer/unlit.filamat +0 -0
  96. package/src/viewer/validateGlb.ts +262 -0
  97. package/src/viewer/video.filamat +0 -0
  98. package/src/viewer/wasmCrashSymbolizer.ts +133 -0
  99. package/src/viewer2d/createViewer2d.ts +438 -0
  100. package/src/viewer2d/creator2d.js +14 -0
  101. package/src/viewer2d/creator2d.wasm +0 -0
  102. package/src/viewer2d/sprite2d.ts +80 -0
  103. package/src/vite-env.d.ts +1 -0
@@ -0,0 +1,993 @@
1
+ // @ts-ignore
2
+ import createModule from './creator-gl.js'
3
+ import { _fetch, _fetchLocal, getBlob, getBuffer, putBuffer, putLocalBuffer } from '../utils/fetch'
4
+
5
+ import wasmLib from './creator-gl.wasm?url'
6
+ import litMaterialSrc from './lit.filamat?url'
7
+ import unlitMaterialSrc from './unlit.filamat?url'
8
+ import unlitTransparentMaterialSrc from './unlit-transparent.filamat?url'
9
+ import videoMaterialSrc from './video.filamat?url'
10
+ import shadowMaterialSrc from './shadow.filamat?url'
11
+ import particlesMaterialSrc from './particles.filamat?url'
12
+ import particlesQuadMaterialSrc from './particles-quad.filamat?url'
13
+ import iblTexture from '../assets/neutral_ibl128.ktx?url'
14
+ import uberarchieve from './uberarchive.bin?url'
15
+
16
+ // const wasmLib = `https://cdn.jt3.ru/le/creator-gl.wasm`
17
+ // const litMaterialSrc = `https://cdn.jt3.ru/le/lit.filamat`
18
+ // const unlitMaterialSrc = `https://cdn.jt3.ru/le/unlit.filamat`
19
+ // const iblTexture = `https://cdn.jt3.ru/le/neutral_ibl.ktx`
20
+ // const uberarchieve = `https://cdn.jt3.ru/le/uberarchive.bin`
21
+
22
+ import { getPlayer, removePlayers } from './mediaplayer'
23
+ import { installWasmCrashSymbolizer, symbolizeWasmError } from './wasmCrashSymbolizer'
24
+ import type { CanvasHost } from '../canvas/createCanvasHost'
25
+
26
+ export const baseProps = {
27
+ backend: "opengl",
28
+ }
29
+
30
+ export const initModule = async (canvas: HTMLCanvasElement, viewerOptions: ViewerOptions) => {
31
+ installWasmCrashSymbolizer()
32
+ const wasmBuffer = await fetch(wasmLib).then(resp => resp.arrayBuffer())
33
+
34
+ const options: WebGLContextAttributes = {
35
+ // majorVersion: 2,
36
+ // minorVersion: 0,
37
+ antialias: false,
38
+ depth: true,
39
+ alpha: false,
40
+ stencil: false,
41
+ desynchronized: false,
42
+ failIfMajorPerformanceCaveat: false,
43
+ powerPreference: "high-performance",
44
+ premultipliedAlpha: true,
45
+ preserveDrawingBuffer: viewerOptions.preloads?.has('capture') ?? false
46
+ }
47
+ Object.assign(options, {
48
+ majorVersion: 2,
49
+ minorVersion: 0
50
+ })
51
+
52
+ const ctx = canvas.getContext("webgl2", options)!
53
+ // @ts-ignore
54
+ window.filament_glContext = ctx
55
+ // @ts-ignore
56
+ window.filament_glOptions = options;
57
+
58
+ const module = await createModule({
59
+ wasmBinary: wasmBuffer,
60
+ canvas
61
+ })
62
+
63
+ module._init()
64
+ module._createSwapChainHeadless()
65
+
66
+ if (viewerOptions.preloads?.has('uberarchive')) {
67
+ const uberarchiveBuffer = await fetch(uberarchieve).then(resp => resp.arrayBuffer())
68
+ const ptr = module._malloc(uberarchiveBuffer.byteLength)
69
+ module.HEAPU8.set(new Uint8Array(uberarchiveBuffer), ptr)
70
+ module._initGlbSupport(ptr, uberarchiveBuffer.byteLength)
71
+ }
72
+
73
+ return module
74
+ }
75
+
76
+ const decoder = new TextDecoder('utf-8')
77
+
78
+ export type ViewerOptions = {
79
+ preloads?: Set<string>
80
+ /** The core 2D-canvas host (`_creatorCanvas`); lets createTextureFromCanvas read baked surfaces. */
81
+ canvasHost?: CanvasHost
82
+ }
83
+
84
+ export const createViewer = async (canvas: HTMLCanvasElement, options: ViewerOptions = {}) => {
85
+
86
+ const module = await initModule(canvas, options)
87
+
88
+ if (options.preloads?.has('lit.filamat')) {
89
+ const litMaterial = await (await fetch(litMaterialSrc)).arrayBuffer()
90
+ putLocalBuffer("lit.filamat", litMaterial)
91
+ }
92
+ if (options.preloads?.has('unlit.filamat')) {
93
+ const unlitMaterial = await (await fetch(unlitMaterialSrc)).arrayBuffer()
94
+ putLocalBuffer("unlit.filamat", unlitMaterial)
95
+ }
96
+ // The blended twin of unlit (Material.unlit({ transparent: true })) — its own package because
97
+ // matc bakes the blending mode in.
98
+ if (options.preloads?.has('unlit-transparent.filamat')) {
99
+ const unlitTransparentMaterial = await (await fetch(unlitTransparentMaterialSrc)).arrayBuffer()
100
+ putLocalBuffer("unlit-transparent.filamat", unlitTransparentMaterial)
101
+ }
102
+ if (options.preloads?.has('video.filamat')) {
103
+ const videoMaterial = await (await fetch(videoMaterialSrc)).arrayBuffer()
104
+ putLocalBuffer("video.filamat", videoMaterial)
105
+ }
106
+ if (options.preloads?.has('shadow.filamat')) {
107
+ const shadowMaterial = await (await fetch(shadowMaterialSrc)).arrayBuffer()
108
+ putLocalBuffer("shadow.filamat", shadowMaterial)
109
+ }
110
+ if (options.preloads?.has('particles.filamat')) {
111
+ const particlesMaterial = await (await fetch(particlesMaterialSrc)).arrayBuffer()
112
+ putLocalBuffer("particles.filamat", particlesMaterial)
113
+ }
114
+ if (options.preloads?.has('particles-quad.filamat')) {
115
+ const particlesQuadMaterial = await (await fetch(particlesQuadMaterialSrc)).arrayBuffer()
116
+ putLocalBuffer("particles-quad.filamat", particlesQuadMaterial)
117
+ }
118
+ const iblBuffer = await (await fetch(iblTexture)).arrayBuffer()
119
+
120
+ // const HEAPU8 = module.HEAPU8 as Uint8Array
121
+ // const HEAPU16 = module.HEAPU16 as Uint16Array
122
+ // const HEAPU32 = module.HEAPU32 as Uint32Array
123
+ // const HEAPF32 = module.HEAPF32 as Float32Array
124
+
125
+ const activePlayers: { player: HTMLVideoElement, glTexture: WebGLTexture, lastTime: number, bindWidth: number, bindHeight: number }[] = []
126
+ let arController: any = null
127
+
128
+ const updateVideoPlayers = () => {
129
+ for (let player of activePlayers) {
130
+ const video = player.player
131
+ if (video.videoWidth === 0 || video.currentTime === player.lastTime) continue
132
+
133
+ gl.bindTexture(gl.TEXTURE_2D, player.glTexture);
134
+
135
+ // if (video.videoWidth !== player.bindWidth || video.videoHeight !== player.bindHeight) {
136
+ // gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGBA8, video.videoWidth, video.videoHeight)
137
+ // player.bindWidth = video.videoWidth
138
+ // player.bindHeight = video.videoHeight
139
+ // console.info("Bind video to ", player.bindWidth, player.bindHeight)
140
+ // }
141
+ // gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, video.videoWidth, video.videoHeight, gl.RGBA, gl.UNSIGNED_BYTE, video);
142
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, video.videoWidth, video.videoHeight, 0, gl.RGBA, gl.UNSIGNED_BYTE, video)
143
+ player.lastTime = video.currentTime
144
+ }
145
+ }
146
+
147
+ const gl = (window as any).filament_glContext as WebGL2RenderingContext
148
+
149
+ const materialsMap = new Map<number, number>()
150
+
151
+ // Compile a filamat buffer once per systemId; both createMaterial and createMaterialS share this
152
+ // cache, so Material.lit() and createMaterialS("lit") produce instances of ONE Filament material.
153
+ const compileMaterial = (systemId: number): number => {
154
+ if (!materialsMap.has(systemId)) {
155
+ const arrayBuffer = getBuffer(systemId)
156
+ const ptr = module._malloc(arrayBuffer.byteLength)
157
+ module.HEAPU8.set(new Uint8Array(arrayBuffer), ptr)
158
+ const materialId = module._createMaterial(ptr, arrayBuffer.byteLength)
159
+ module._free(ptr)
160
+ materialsMap.set(systemId, materialId)
161
+ }
162
+ return materialsMap.get(systemId)!
163
+ }
164
+
165
+ const call = (method: string, arg1: number, arg2: string, arg3: any, arg4?: any, arg5?: any) => {
166
+ const maxLen = arg2.length + 1;
167
+ const uniformPtr = module._malloc(maxLen);
168
+ module.stringToUTF8(arg2, uniformPtr, maxLen);
169
+ if (arg5 !== undefined) {
170
+ module[method](arg1, uniformPtr, arg3, arg4, arg5)
171
+ } else if (arg4 !== undefined) {
172
+ module[method](arg1, uniformPtr, arg3, arg4)
173
+ } else {
174
+ module[method](arg1, uniformPtr, arg3)
175
+ }
176
+ module._free(uniformPtr)
177
+ }
178
+
179
+ let sceneOpened = false
180
+
181
+ // Resolution factor (devicePixelRatio × CSS zoom) the backing store is sized at, and the logical
182
+ // (CSS-px) viewport size. The bridge's screen-space contract is LOGICAL px — the touch system,
183
+ // getBoundingClientRect and user code all speak logical — so raycast / view-direction inputs are
184
+ // scaled up to backing px by the factor, and getDisplaySize reports the logical size. Set by
185
+ // _onResize (docs/simulator-plan.md phase 3).
186
+ let resFactor = 1
187
+ let logicalW = 0, logicalH = 0
188
+
189
+ // One frame, no scheduling. Split out of the loop so a host can force a draw right before reading
190
+ // the canvas back: the WebGL context has no preserveDrawingBuffer unless the app asked for it
191
+ // (`// preload: capture`), so the pixels are only valid inside the task that drew them.
192
+ const renderOnce = (): boolean => {
193
+ if (!sceneOpened) return false
194
+ if (module._beginFrame()) {
195
+ if (arController !== null) {
196
+ arController.track(true)
197
+ }
198
+ updateVideoPlayers()
199
+ module._render(BigInt(Date.now()))
200
+ return true
201
+ }
202
+ if (arController !== null) {
203
+ arController.track(false)
204
+ }
205
+ return false // Filament declined this frame (its own pacing) — see warmRender's retry loop
206
+ }
207
+
208
+ const render = () => {
209
+ if (!sceneOpened) return
210
+ renderOnce()
211
+ requestAnimationFrame(render)
212
+ }
213
+
214
+ const createMediaPlayerTexture = (id: number) => {
215
+ const player = getPlayer(id) as HTMLVideoElement
216
+ const glTextureId = module.GL.getNewId(module.GL.textures)
217
+ const glTexture = gl.createTexture()
218
+ module.GL.textures[glTextureId] = glTexture
219
+
220
+ gl.bindTexture(gl.TEXTURE_2D, glTexture)
221
+ gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
222
+ gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
223
+ gl.pixelStorei( gl.UNPACK_COLORSPACE_CONVERSION_WEBGL, gl.NONE );
224
+
225
+ activePlayers.push({ player, glTexture, lastTime: 0, bindWidth: 0, bindHeight: 0 })
226
+
227
+ const textureId = module._importTexture(glTextureId, 0, 0)
228
+ return textureId
229
+ }
230
+
231
+ // One persistent wasm scratch for native float read-backs, sized to the largest reader (a 4x4
232
+ // matrix = 16). Its address is stable across heap growth, so we malloc once instead of per call.
233
+ // Each read-back writes into a caller-owned `out`, so there's no shared-buffer aliasing — we only
234
+ // drop the per-call malloc/free + view allocation. Read module.HEAPF32 FRESH each call (emscripten
235
+ // swaps that view on heap growth; the scratch address survives).
236
+ const READ_SCRATCH = 64 // largest reader: vehicleGetState = 7 + 3 per wheel
237
+ const readScratchPtr = module._malloc(READ_SCRATCH * 4)
238
+ const readInto = (out: Float32Array, n: number, fn: (ptr: number) => void): void => {
239
+ fn(readScratchPtr)
240
+ const base = readScratchPtr >> 2
241
+ const heap = module.HEAPF32
242
+ for (let i = 0; i < n; i++) out[i] = heap[base + i]
243
+ }
244
+
245
+ // 12-byte KTX2 magic («KTX 20» CR LF 0x1A LF) — createTexture routes these to the wasm transcoder.
246
+ const KTX2_MAGIC = [0xab, 0x4b, 0x54, 0x58, 0x20, 0x32, 0x30, 0xbb, 0x0d, 0x0a, 0x1a, 0x0a]
247
+ const isKtx2Bytes = (b: Uint8Array) => b.length >= 12 && KTX2_MAGIC.every((v, i) => b[i] === v)
248
+ // PNG/JPG/WebP… via the browser decoder → sRGB8_A8 GL texture imported into Filament.
249
+ const createBitmapTexture = (blob: Blob, onComplete: (textureId: number, width: number, height: number) => void, onReject: any) => {
250
+ createImageBitmap(blob)
251
+ .then((bitmap) => {
252
+ const glTextureId = module.GL.getNewId(module.GL.textures)
253
+ const texture = gl.createTexture()
254
+ module.GL.textures[glTextureId] = texture
255
+ gl.bindTexture(gl.TEXTURE_2D, texture);
256
+
257
+ gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
258
+ gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
259
+ gl.pixelStorei( gl.UNPACK_COLORSPACE_CONVERSION_WEBGL, gl.NONE );
260
+
261
+ gl.texStorage2D(gl.TEXTURE_2D, 1, gl.SRGB8_ALPHA8, bitmap.width, bitmap.height)
262
+ gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, bitmap.width, bitmap.height, gl.RGBA, gl.UNSIGNED_BYTE, bitmap);
263
+ const textureId = module._importTexture(glTextureId, bitmap.width, bitmap.height)
264
+ onComplete(textureId, bitmap.width, bitmap.height)
265
+ })
266
+ .catch(onReject)
267
+ }
268
+ return {
269
+ ...baseProps,
270
+ createScene: module._createScene,
271
+ createEntity: module._createEntity,
272
+ destroyEntity: module._destroyEntity as (entityId: number) => void,
273
+ addEntityToScene: module._addEntityToScene as (sceneId: number, entityId: number) => void,
274
+ removeEntityFromScene: module._removeEntityFromScene as (sceneId: number, entityId: number) => void,
275
+ createOverlayScene: module._createOverlayScene,
276
+ setMaterial: module._setMaterial,
277
+ createSunLight: module._createSunLight,
278
+ setSkybox: module._setSkybox,
279
+ setVisible: module._setVisible,
280
+ isVisible: module._isVisible,
281
+ setCastShadowsGlobal: module._setCastShadowsGlobal,
282
+ setReceiveShadowsGlobal: module._setReceiveShadowsGlobal,
283
+ setReceiveShadows: module._setReceiveShadows,
284
+ setCastShadows: module._setCastShadows,
285
+ setSceneMultiSampleAntiAliasing: module._setMultiSampleAntiAliasing as (sceneId: number, enabled: boolean, samples: number) => void,
286
+ // Only present once the wasm is rebuilt with the export (creator-gl scene.cpp setSceneRenderOptions);
287
+ // the SDK feature-detects it. The web viewer draws to its own canvas, so only the dynamic-resolution
288
+ // half applies (renderScale is for hosts that own a scene texture).
289
+ setSceneRenderOptions: module._setSceneRenderOptions as ((sceneId: number, renderScale: number, dynamicResolution: boolean, minScale: number) => void) | undefined,
290
+
291
+ setPosition: module._setPosition,
292
+ setQuaternion: module._setQuaternion,
293
+ setEulerAngles: module._setEulerAngles,
294
+ setScale: module._setScale,
295
+
296
+ setColliderFromMesh: module._setColliderFromMesh,
297
+ setColliderBox: module._setColliderBox,
298
+ setColliderSphere: module._setColliderSphere,
299
+
300
+ // JoltPhysics (see physics.ts). All args are plain numbers, so these are direct
301
+ // passthroughs. The step + dynamic-body transform sync run entirely in C++ inside
302
+ // module._render — JS never touches per-frame transforms.
303
+ physicsHasSupport: () => !!module._creatorHasPhysics?.(),
304
+ physicsConfigure: (gx: number, gy: number, gz: number, maxBodies: number) =>
305
+ module._physicsConfigure(gx, gy, gz, maxBodies),
306
+ setInterpolation: (enabled: boolean) => module._setPhysicsInterpolation?.(enabled),
307
+ physicsCreateBox: (entityId: number, hx: number, hy: number, hz: number, motionType: number, mass: number): number =>
308
+ module._physicsCreateBox(entityId, hx, hy, hz, motionType, mass),
309
+ physicsCreateSphere: (entityId: number, radius: number, motionType: number, mass: number): number =>
310
+ module._physicsCreateSphere(entityId, radius, motionType, mass),
311
+ physicsCreateCylinder: (entityId: number, halfHeight: number, radius: number, motionType: number, mass: number): number =>
312
+ module._physicsCreateCylinder(entityId, halfHeight, radius, motionType, mass),
313
+ physicsSetLinearVelocity: (bodyId: number, x: number, y: number, z: number) =>
314
+ module._physicsSetLinearVelocity(bodyId, x, y, z),
315
+ physicsGetLinearVelocity: (bodyId: number, out: Float32Array) =>
316
+ readInto(out, 3, ptr => module._physicsGetLinearVelocity(bodyId, ptr)),
317
+ physicsSetAngularVelocity: (bodyId: number, x: number, y: number, z: number) =>
318
+ module._physicsSetAngularVelocity(bodyId, x, y, z),
319
+ physicsGetAngularVelocity: (bodyId: number, out: Float32Array) =>
320
+ readInto(out, 3, ptr => module._physicsGetAngularVelocity(bodyId, ptr)),
321
+ physicsApplyImpulse: (bodyId: number, x: number, y: number, z: number) =>
322
+ module._physicsApplyImpulse(bodyId, x, y, z),
323
+ physicsSetBodyPosition: (bodyId: number, x: number, y: number, z: number) =>
324
+ module._physicsSetBodyPosition(bodyId, x, y, z),
325
+ physicsSetBodyRotation: (bodyId: number, qx: number, qy: number, qz: number, qw: number) =>
326
+ module._physicsSetBodyRotation(bodyId, qx, qy, qz, qw),
327
+ physicsRemoveBody: (bodyId: number) => module._physicsRemoveBody(bodyId),
328
+ // Shape registry + body creation (Shape/Physics/Trigger aspects).
329
+ physicsBuildBox: (hx: number, hy: number, hz: number): number => module._physicsBuildBox(hx, hy, hz),
330
+ physicsBuildSphere: (radius: number): number => module._physicsBuildSphere(radius),
331
+ physicsBuildCylinder: (halfHeight: number, radius: number): number => module._physicsBuildCylinder(halfHeight, radius),
332
+ physicsBuildCapsule: (halfHeight: number, radius: number): number => module._physicsBuildCapsule(halfHeight, radius),
333
+ physicsBuildMesh: (vertices: Float32Array, indices: Uint32Array, convex: boolean, sx: number, sy: number, sz: number): number => {
334
+ // Jolt copies the data into the shape during the call — the staging buffers are freed right after.
335
+ const ptrV = module._malloc(vertices.byteLength)
336
+ const ptrI = module._malloc(indices.byteLength)
337
+ module.HEAPF32.set(vertices, ptrV / 4)
338
+ module.HEAPU32.set(indices, ptrI / 4)
339
+ const id = module._physicsBuildMesh(ptrV, Math.floor(vertices.length / 3), ptrI, indices.length, convex, sx, sy, sz)
340
+ module._free(ptrV)
341
+ module._free(ptrI)
342
+ return id
343
+ },
344
+ physicsBuildMeshFromEntity: (entityId: number, convex: boolean, sx: number, sy: number, sz: number): number =>
345
+ module._physicsBuildMeshFromEntity(entityId, convex, sx, sy, sz),
346
+ // Optional ABI members: left UNDEFINED when the loaded wasm predates them, so the SDK's
347
+ // feature-detect (`if (!_creator.physicsSetShapeOrigin)`) degrades with a warning instead of
348
+ // calling through to a missing export. Drop the guards after the next creator-gl wasm rebuild.
349
+ physicsSetShapeOrigin: module._physicsSetShapeOrigin
350
+ ? (shapeId: number, x: number, y: number, z: number) => module._physicsSetShapeOrigin(shapeId, x, y, z)
351
+ : undefined,
352
+ physicsSetBodyShape: module._physicsSetBodyShape
353
+ ? (bodyId: number, shapeId: number, updateMass: boolean) => module._physicsSetBodyShape(bodyId, shapeId, updateMass)
354
+ : undefined,
355
+ physicsDestroyShape: (shapeId: number) => module._physicsDestroyShape(shapeId),
356
+ physicsCreateBody: (entityId: number, shapeId: number, motion: number, mass: number, sensor: boolean, pickOnly: boolean): number =>
357
+ module._physicsCreateBody(entityId, shapeId, motion, mass, sensor, pickOnly),
358
+ physicsSetPickable: (bodyId: number, pickable: boolean) => module._physicsSetPickable(bodyId, pickable),
359
+ physicsSetFriction: (bodyId: number, friction: number) => module._physicsSetFriction(bodyId, friction),
360
+ physicsGetFriction: (bodyId: number): number => module._physicsGetFriction(bodyId),
361
+ physicsRaycast: (ox: number, oy: number, oz: number, dx: number, dy: number, dz: number, maxDist: number, out?: Float32Array): number => {
362
+ if (!out) return module._physicsRaycast(ox, oy, oz, dx, dy, dz, maxDist, 0)
363
+ let id = 0
364
+ readInto(out, 7, ptr => { id = module._physicsRaycast(ox, oy, oz, dx, dy, dz, maxDist, ptr) })
365
+ return id
366
+ },
367
+ // CharacterController (Jolt CharacterVirtual).
368
+ characterCreate: (entityId: number, shapeId: number, maxSlopeDeg: number): number =>
369
+ module._characterCreate(entityId, shapeId, maxSlopeDeg),
370
+ characterDestroy: (charId: number) => module._characterDestroy(charId),
371
+ characterMove: (charId: number, x: number, z: number) => module._characterMove(charId, x, z),
372
+ characterMoveFree: (charId: number, x: number, y: number, z: number) =>
373
+ module._characterMoveFree(charId, x, y, z),
374
+ characterSetVerticalVelocity: (charId: number, vy: number) =>
375
+ module._characterSetVerticalVelocity(charId, vy),
376
+ characterSetVelocity: (charId: number, x: number, y: number, z: number) =>
377
+ module._characterSetVelocity(charId, x, y, z),
378
+ characterSetGravityScale: (charId: number, scale: number) =>
379
+ module._characterSetGravityScale(charId, scale),
380
+ characterSetMaxSlope: (charId: number, maxSlopeDeg: number) =>
381
+ module._characterSetMaxSlope(charId, maxSlopeDeg),
382
+ characterSetShape: (charId: number, shapeId: number): boolean =>
383
+ !!module._characterSetShape(charId, shapeId),
384
+ characterGetVelocity: (charId: number, out: Float32Array) =>
385
+ readInto(out, 3, ptr => module._characterGetVelocity(charId, ptr)),
386
+ characterGetGroundState: (charId: number): number => module._characterGetGroundState(charId),
387
+ characterSetPosition: (charId: number, x: number, y: number, z: number) =>
388
+ module._characterSetPosition(charId, x, y, z),
389
+ // Vehicle (Jolt VehicleConstraint). The settings blob + wheel-entity ids are staged into the wasm
390
+ // heap for the call; Jolt copies everything it keeps, so both buffers are freed right after.
391
+ vehicleCreate: (entityId: number, shapeId: number, settings: Float32Array): number => {
392
+ const ptrS = module._malloc(settings.byteLength)
393
+ module.HEAPF32.set(settings, ptrS / 4)
394
+ const id = module._vehicleCreate(entityId, shapeId, ptrS, settings.length)
395
+ module._free(ptrS)
396
+ return id
397
+ },
398
+ vehicleDestroy: (vehicleId: number) => module._vehicleDestroy(vehicleId),
399
+ vehicleSetTransmission: (vehicleId: number, ratio: number, clutch: number) =>
400
+ module._vehicleSetTransmission(vehicleId, ratio, clutch),
401
+ vehicleSetTuning: (vehicleId: number, settings: Float32Array) => {
402
+ const ptr = module._malloc(settings.byteLength)
403
+ module.HEAPF32.set(settings, ptr / 4)
404
+ module._vehicleSetTuning(vehicleId, ptr, settings.length)
405
+ module._free(ptr)
406
+ },
407
+ vehicleSetInput: (vehicleId: number, forward: number, right: number, brake: number, handBrake: number) =>
408
+ module._vehicleSetInput(vehicleId, forward, right, brake, handBrake),
409
+ vehicleGetState: (vehicleId: number, out: Float32Array) =>
410
+ readInto(out, Math.min(out.length, READ_SCRATCH), (ptr) => module._vehicleGetState(vehicleId, ptr, Math.min(out.length, READ_SCRATCH))),
411
+ vehicleReset: (vehicleId: number, x: number, y: number, z: number, qx: number, qy: number, qz: number, qw: number) =>
412
+ module._vehicleReset(vehicleId, x, y, z, qx, qy, qz, qw),
413
+ vehicleBodyId: (vehicleId: number): number => module._vehicleBodyId(vehicleId),
414
+ // Ragdoll (Jolt Ragdoll over a model's bones). Bone list + settings blob staged into the wasm heap
415
+ // for the call; Jolt copies everything it keeps, so both buffers are freed right after.
416
+ ragdollCreate: (rootEntityId: number, bones: Uint32Array, settings: Float32Array): number => {
417
+ const ptrB = module._malloc(bones.byteLength)
418
+ module.HEAPU32.set(bones, ptrB / 4)
419
+ const ptrS = module._malloc(settings.byteLength)
420
+ module.HEAPF32.set(settings, ptrS / 4)
421
+ const id = module._ragdollCreate(rootEntityId, ptrB, bones.length, ptrS, settings.length)
422
+ module._free(ptrS)
423
+ module._free(ptrB)
424
+ return id
425
+ },
426
+ ragdollDestroy: (ragdollId: number) => module._ragdollDestroy(ragdollId),
427
+ ragdollActivate: (ragdollId: number, vx: number, vy: number, vz: number): boolean =>
428
+ !!module._ragdollActivate(ragdollId, vx, vy, vz),
429
+ ragdollDeactivate: (ragdollId: number) => module._ragdollDeactivate(ragdollId),
430
+ ragdollActive: (ragdollId: number): boolean => !!module._ragdollActive(ragdollId),
431
+ ragdollBodyId: (ragdollId: number, index: number): number => module._ragdollBodyId(ragdollId, index),
432
+ computeBoundingBox(entityId: number) {
433
+ // Returns a fresh 6-float [minX,minY,minZ, maxX,maxY,maxZ]. Uses the shared scratch (the old
434
+ // code malloc'd per call AND never freed it — a leak) and copies out, so the result is a
435
+ // standalone, retainable array (no live view over the wasm heap that detaches on growth).
436
+ const out = new Float32Array(6)
437
+ readInto(out, 6, ptr => module._computeBoundingBox(entityId, ptr))
438
+ return out
439
+ },
440
+ // Screen inputs are logical px; scale to backing px for the native (viewport-space) pick.
441
+ raycastView: (offsetX: number, offsetY: number) => module._raycastView(offsetX * resFactor, offsetY * resFactor),
442
+ setCulling: module._setCulling,
443
+ setGlbCulling: module._setGlbCulling,
444
+ // Culling default for animated GLBs (Model.load): only once the wasm carries the export — an older
445
+ // binary leaves the SDK on the always-draw default (see bridges.d.ts).
446
+ skinnedCullingSupported: module._skinnedCullingSupported ? () => !!module._skinnedCullingSupported() : undefined,
447
+ // Model.lod / Animator.lod (docs/lod-plan.md) — only once the wasm carries the LOD pass.
448
+ setLod: module._setLod ? (entityId: number, mesh: number, anim: number) => module._setLod(entityId, mesh, anim) : undefined,
449
+ // Shadow flags across a whole GLB instance (glb.cpp setGlbShadows). Only present once the wasm
450
+ // carries the export — the SDK feature-detects it, so an older binary just leaves models casting.
451
+ setGlbShadows: module._setGlbShadows,
452
+ hasMesh: module._hasMesh,
453
+ cloneEntity: module._cloneEntity,
454
+ setMaterialGlobalParameter: module._setMaterialGlobalParameter,
455
+
456
+ setParent: module._setParent as (entityId: number, parentEntityId: number, worldPositionStays: boolean) => void,
457
+ setParentNull: module._setParentNull,
458
+ // Batch setParent; children keep their LOCAL transform (worldPositionStays = false) — the same
459
+ // semantics as the lite scene core and the creator-pkg binding.
460
+ addChildren: (parentEntityId: number, entityIds: number[]) => {
461
+ for (const id of entityIds) module._setParent(id, parentEntityId, false)
462
+ },
463
+ getParent: module._getParent,
464
+ getChildCount: module._getChildCount,
465
+ getChild: module._getChild,
466
+ getMaterial: module._getMaterial,
467
+ createARPlane: module._createARPlane,
468
+ updateProjectionMatrixAR: module._updateProjectionMatrixAR,
469
+ getCameraFov: module._getCameraFov,
470
+ // Stored per scene natively and re-applied on every _setViewport — nothing to re-send on resize.
471
+ setCameraProjection: module._setCameraProjection,
472
+
473
+ // Particle systems — 3 methods; all config crosses as one tagged Float32Array
474
+ createParticleSystem: module._createParticleSystem,
475
+ spawnParticles: module._spawnParticles,
476
+ setParticleSystemConfig(entityId: number, data: Float32Array) {
477
+ const ptr: number = module._malloc(data.length * 4)
478
+ module.HEAPF32.set(data, ptr / 4)
479
+ module._setParticleSystemConfig(entityId, ptr, data.length)
480
+ module._free(ptr)
481
+ },
482
+ createNoise: module._createNoise,
483
+ setNoiseFrequency: module._setNoiseFrequency,
484
+ setNoiseOctaves: module._setNoiseOctaves,
485
+ setNoiseFractalLunacrity: module._setNoiseFractalLunacrity,
486
+ setNoiseFractalGain: module._setNoiseFractalGain,
487
+ getNoise2D: module._getNoise2D,
488
+ getNoise3D: module._getNoise3D,
489
+
490
+ createARController(sceneId: number, cameraId: number, mode: string, onTrack: (entityId: number, track: boolean) => void): void {
491
+ throw new Error("createARController is not implemented")
492
+ },
493
+ launchAR(sceneId: number, onComplete: () => void, onReject: () => void): void {
494
+ throw new Error("launchAR is not implemented")
495
+ },
496
+ stopAR(): void {
497
+ throw new Error("stopAR is not implemented")
498
+ },
499
+ createRootAnchor(sceneId: number): number {
500
+ throw new Error("createRootAnchor is not implemented")
501
+ },
502
+ createAnchor(sceneId: number, physicalWidth: number, sourceId: number): number {
503
+ throw new Error("createAnchor is not implemented")
504
+ },
505
+ getSceneMaterial(sceneId: number): number {
506
+ throw new Error("getSceneMaterial is not implemented")
507
+ },
508
+ getName(entityId: number) {
509
+ const namePtr = module._getName(entityId)
510
+ if (namePtr === 0) {
511
+ return ""
512
+ }
513
+ return module.UTF8ToString(namePtr)
514
+ },
515
+ findNode(entityId: number, name: string): number {
516
+ const maxLen = name.length * 4 + 1
517
+ const ptr = module._malloc(maxLen)
518
+ module.stringToUTF8(name, ptr, maxLen)
519
+ const found = module._findNode(entityId, ptr) as number
520
+ module._free(ptr)
521
+ return found
522
+ },
523
+
524
+ setName(entityId: number, name: string) {
525
+ const maxLen = name.length + 1;
526
+ const uniformPtr = module._malloc(maxLen);
527
+ module.stringToUTF8(name, uniformPtr, maxLen);
528
+ module._setName(entityId, uniformPtr)
529
+ module._free(uniformPtr)
530
+ },
531
+
532
+ createMediaPlayerTexture,
533
+
534
+ // Per-frame aspect update(dt) dispatch, fired from inside render(): early = before the physics step,
535
+ // late = after animations/particles, just before draw. Single-slot (the SDK registers one dispatcher
536
+ // per phase that iterates its own updater list). See init.cpp / lib.js.
537
+ setEarlyUpdate: (callback: (dt: number) => void) => module.onEarlyUpdate = callback,
538
+ setLateUpdate: (callback: (dt: number) => void) => module.onLateUpdate = callback,
539
+ // Fixed phase (once per physics substep, dt = 1/60): bound only when the wasm exports the gate, so a
540
+ // wasm predating it leaves the SDK on its own accumulator instead of a phase that never fires.
541
+ ...((module as any)._setFixedUpdateEnabled ? {
542
+ setFixedUpdate: (callback: ((dt: number) => void) | null) => {
543
+ (module as any).onFixedUpdate = callback ?? undefined
544
+ ;(module as any)._setFixedUpdateEnabled(callback ? 1 : 0)
545
+ },
546
+ } : {}),
547
+ // Engine clock multiplier (SDK Time.scale / Time.paused → physics, animators, particles). Bound
548
+ // only when the wasm exports it — the SDK feature-detects and a wasm predating it just keeps its
549
+ // sims at wall-clock speed instead of pretending to scale.
550
+ ...((module as any)._setTimeScale ? { setTimeScale: (scale: number) => (module as any)._setTimeScale(scale) } : {}),
551
+ // Camera exposure (aperture / shutter / ISO) — bound when the wasm exports it.
552
+ ...((module as any)._setCameraExposure ? { setCameraExposure: (sceneId: number, aperture: number, shutterSpeed: number, sensitivity: number) => (module as any)._setCameraExposure(sceneId, aperture, shutterSpeed, sensitivity) } : {}),
553
+ // Display output range / HDR look (creator-gl setDisplayHeadroom & co, filament patch 0018) —
554
+ // bound when the wasm exports them. No web host asks the canvas for an extended-range surface
555
+ // yet (float16 canvas + rec2100 colour spaces are behind flags), so the headroom reads SDR and
556
+ // device.hdr's setters are stored, not seen.
557
+ ...((module as any)._getDisplayHeadroom ? {
558
+ getDisplayHeadroom: () => (module as any)._getDisplayHeadroom(),
559
+ getDisplayMaxHeadroom: () => Math.max(1, (module as any)._getDisplayHeadroom()),
560
+ setHdrStrength: (strength: number) => (module as any)._setHdrStrength(strength),
561
+ getHdrStrength: () => (module as any)._getHdrStrength(),
562
+ setHdrPaperWhite: (paperWhite: number) => (module as any)._setHdrPaperWhite(paperWhite),
563
+ getHdrPaperWhite: () => (module as any)._getHdrPaperWhite(),
564
+ } : {}),
565
+ // Physics contact/sensor events (drained after each step): a/b = entity ids, type 0 enter / 1 exit.
566
+ setOnPhysicsEvent: (callback: (a: number, b: number, type: number) => void) => module.onPhysicsEvent = callback,
567
+ setDefaultIbl(sceneId: number, intensity: number) {
568
+ const ptr = module._malloc(iblBuffer.byteLength);
569
+ module.HEAPU8.set(new Uint8Array(iblBuffer), ptr)
570
+ module._createIblLight(sceneId, ptr, iblBuffer.byteLength, intensity)
571
+ },
572
+ setBloomOptions: module._setBloomOptions,
573
+ setUniformColor(materialId: number, uniform: string, color: number) {
574
+ call("_setUniformRgb", materialId, uniform, color)
575
+ },
576
+ setUniformRgb(materialId: number, uniform: string, color: number) {
577
+ call("_setUniformRgb", materialId, uniform, color)
578
+ },
579
+ setUniformRgba(materialId: number, uniform: string, color: number) {
580
+ call("_setUniformRgba", materialId, uniform, color)
581
+ },
582
+ setUniformFloat(materialId: number, uniform: string, value: number) {
583
+ call("_setUniformFloat", materialId, uniform, value)
584
+ },
585
+ setUniformBoolean(materialId: number, uniform: string, value: boolean) {
586
+ call("_setUniformBoolean", materialId, uniform, value)
587
+ },
588
+ setUniformTexture(materialId: number, uniform: string, systemId: number, wrapS: number, wrapT: number) {
589
+ call("_setUniformTexture", materialId, uniform, systemId, wrapS, wrapT)
590
+ },
591
+ setUniformArray(materialId: number, uniform: string, value: Float32Array) {
592
+ const uniformPtr = module._malloc(value.byteLength);
593
+ module.HEAPF32.set(value, uniformPtr/4)
594
+ call("_setUniformArray", materialId, uniform, uniformPtr, value.length)
595
+ },
596
+ setUniformNull(materialId: number, uniform: string) {
597
+ // material.set(key, null): a SAMPLER_2D uniform resets to the engine's white 1x1 (see
598
+ // creator-gl setUniformNull). Guarded so a stale creator-gl.wasm (built before the export
599
+ // existed) degrades to a no-op instead of a crash.
600
+ if (!module._setUniformNull) return
601
+ const maxLen = uniform.length + 1
602
+ const uniformPtr = module._malloc(maxLen)
603
+ module.stringToUTF8(uniform, uniformPtr, maxLen)
604
+ module._setUniformNull(materialId, uniformPtr)
605
+ module._free(uniformPtr)
606
+ },
607
+ // A built-in material by shader NAME, resolved through the same local-resource path the SDK
608
+ // factories use (Material.lit → fetchLocal("lit.filamat")). The shader must have been
609
+ // preloaded (ViewerOptions.preloads / putLocalBuffer) — same requirement as the factories.
610
+ createMaterialS: (name: string): number => {
611
+ const systemId = _fetchLocal(`${name}.filamat`)
612
+ if (systemId < 0) throw new Error(`createMaterialS: material "${name}.filamat" is not preloaded on this host`)
613
+ return module._createMaterialInstance(compileMaterial(systemId))
614
+ },
615
+ createMaterial: (systemId: number) => {
616
+ return module._createMaterialInstance(compileMaterial(systemId))
617
+ },
618
+ setMesh(entityId: number, materialId: number, vertices: Float32Array, normals: Float32Array, indices: Uint16Array, uv: Float32Array, meshType: number, uv1?: Float32Array) {
619
+ const ptrVertices = module._malloc(vertices.byteLength)
620
+ const ptrNormals = module._malloc(normals.byteLength)
621
+ const ptrIndices = module._malloc(indices.byteLength)
622
+ const ptrUv = module._malloc(uv.byteLength)
623
+ module.HEAPF32.set(vertices, ptrVertices/4)
624
+ module.HEAPF32.set(normals, ptrNormals/4)
625
+ module.HEAPU16.set(indices, ptrIndices/2)
626
+ module.HEAPF32.set(uv, ptrUv/4)
627
+
628
+ // the lightmap UV set rides the newer export; a wasm without it duplicates uv into UV1
629
+ if (uv1 && module._createMeshEx) {
630
+ const ptrUv1 = module._malloc(uv1.byteLength)
631
+ module.HEAPF32.set(uv1, ptrUv1/4)
632
+ module._createMeshEx(entityId, materialId, ptrVertices, ptrNormals, ptrIndices, ptrUv, vertices.length/3, indices.length, meshType, ptrUv1)
633
+ return
634
+ }
635
+ module._createMesh(entityId, materialId, ptrVertices, ptrNormals, ptrIndices, ptrUv, vertices.length/3, indices.length, meshType)
636
+ },
637
+ attachCameraToAR(sceneId: number, cameraId: number, onProjectChange: () => void) {
638
+ module._attachCameraToScene(sceneId, cameraId)
639
+ },
640
+ createGLView() {},
641
+ openScene(sceneId: number) {
642
+ if (!sceneOpened) {
643
+ requestAnimationFrame(render)
644
+ canvas.style.display = "block"
645
+ }
646
+ sceneOpened = true
647
+ module._openScene(sceneId)
648
+ module._setViewport(canvas.width, canvas.height)
649
+ },
650
+ _onResize(width: number, height: number, factor = 1) {
651
+ resFactor = factor
652
+ logicalW = width
653
+ logicalH = height
654
+ // Backing store in device px, CSS box pinned to logical px (host-shared crispness pattern).
655
+ canvas.width = Math.max(1, Math.round(width * factor))
656
+ canvas.height = Math.max(1, Math.round(height * factor))
657
+ canvas.style.width = width + "px"
658
+ canvas.style.height = height + "px"
659
+ module._setViewport(canvas.width, canvas.height)
660
+ },
661
+ closeScene() {
662
+ canvas.style.display = "none"
663
+ sceneOpened = false
664
+ },
665
+ traverse(entityId: number, callback: (entityId: number) => void) {
666
+ module.traverseCallback = callback
667
+ module._traverseEm(entityId)
668
+ },
669
+ setMatrix(entityId: number, matrix: Float32Array) {
670
+ const ptrMatrix = module._malloc(matrix.byteLength)
671
+ module.HEAPF32.set(matrix, ptrMatrix/4)
672
+ module._setMatrix(entityId, ptrMatrix)
673
+ module._free(ptrMatrix)
674
+ },
675
+ setWorldMatrix(entityId: number, matrix: Float32Array) {
676
+ const ptrMatrix = module._malloc(matrix.byteLength)
677
+ module.HEAPF32.set(matrix, ptrMatrix/4)
678
+ module._setWorldMatrix(entityId, ptrMatrix)
679
+ module._free(ptrMatrix)
680
+ },
681
+ getMatrix(entityId: number, matrix: Float32Array) {
682
+ readInto(matrix, 16, ptr => module._getMatrix(entityId, ptr))
683
+ },
684
+ getWorldMatrix(entityId: number, matrix: Float32Array) {
685
+ readInto(matrix, 16, ptr => module._getWorldMatrix(entityId, ptr))
686
+ },
687
+ getWorldPosition(entityId: number, matrix: Float32Array) {
688
+ readInto(matrix, 3, ptr => module._getWorldPosition(entityId, ptr))
689
+ },
690
+ getWorldDirection(entityId: number, mode: number, matrix: Float32Array) {
691
+ readInto(matrix, 3, ptr => module._getWorldDirection(entityId, mode, ptr))
692
+ },
693
+ getWorldMatrixInverse(entityId: number, matrix: Float32Array) {
694
+ readInto(matrix, 16, ptr => module._getWOrldMatrixInverse(entityId, ptr))
695
+ },
696
+ getCameraViewDirection(sceneId: number, screenX: number, screenY: number, outVec: Float32Array) {
697
+ // Screen inputs are logical px; the native viewport is backing px → scale up by the factor.
698
+ readInto(outVec, 3, ptr => module._getCameraViewDirection(sceneId, screenX * resFactor, screenY * resFactor, ptr))
699
+ },
700
+ getDisplaySize() {
701
+ // Logical (CSS-px) viewport — device.pixelRatio carries the density separately.
702
+ return [ logicalW || canvas.width, logicalH || canvas.height ]
703
+ },
704
+ getChildren(entityId: number) {
705
+
706
+ const childrenIds = []
707
+ const chunk_size = 32
708
+
709
+ for (let i = 0; i < 256; i++) {
710
+ const ptr = module._malloc(chunk_size * 4)/4
711
+ const childCount = module._getChildrenIter(entityId, ptr*4, i * chunk_size, chunk_size)
712
+
713
+ for (let i = 0; i < childCount; i++) {
714
+ childrenIds.push(module.HEAPU32[ptr + i])
715
+ }
716
+ module._free(ptr*4)
717
+ if (childCount < chunk_size) {
718
+ break
719
+ }
720
+ }
721
+
722
+ return childrenIds
723
+ },
724
+ createTexture(systemId: number, onComplete: (textureId: number, width: number, height: number) => void, onReject: any) {
725
+ const blob = getBlob(systemId)
726
+ // KTX2 (Basis Universal): createImageBitmap can't decode it — the wasm core transcodes it
727
+ // (BC3 via WEBGL_compressed_texture_s3tc, else RGBA8) and keeps the file's mip chain.
728
+ blob.slice(0, 12).arrayBuffer().then((head) => {
729
+ if (!isKtx2Bytes(new Uint8Array(head))) return false
730
+ return blob.arrayBuffer().then((buf) => {
731
+ const bytes = new Uint8Array(buf)
732
+ const ptr = module._malloc(bytes.length)
733
+ module.HEAPU8.set(bytes, ptr)
734
+ const texId = module._createTextureFromKtx2(ptr, bytes.length) >>> 0
735
+ module._free(ptr)
736
+ if (texId === 0xFFFFFFFF) throw new Error('KTX2 transcode failed')
737
+ onComplete(texId, module._getTextureWidth(texId), module._getTextureHeight(texId))
738
+ return true
739
+ })
740
+ }).then((handled) => { if (!handled) createBitmapTexture(blob, onComplete, onReject) }, onReject)
741
+ },
742
+ // --- textures from a _creatorCanvas surface (straight sRGB RGBA8 → Filament texture; no decode) ---
743
+ // Same registry as createTexture, so setUniformTexture(matId, uniform, texId, …) consumes it as-is.
744
+ createTextureFromCanvas(surfaceId: number): number {
745
+ const surf = options.canvasHost?.readSurface(surfaceId)
746
+ if (!surf) return 0
747
+ const bytes = new Uint8Array(surf.data.buffer, surf.data.byteOffset, surf.data.byteLength)
748
+ const ptr = module._malloc(bytes.length)
749
+ module.HEAPU8.set(bytes, ptr)
750
+ const texId = module._createTextureFromRGBA(ptr, surf.width, surf.height)
751
+ module._free(ptr)
752
+ return texId
753
+ },
754
+ updateTextureFromCanvas(texId: number, surfaceId: number): void {
755
+ const surf = options.canvasHost?.readSurface(surfaceId)
756
+ if (!surf) return
757
+ const bytes = new Uint8Array(surf.data.buffer, surf.data.byteOffset, surf.data.byteLength)
758
+ const ptr = module._malloc(bytes.length)
759
+ module.HEAPU8.set(bytes, ptr)
760
+ module._updateTextureFromRGBA(texId, ptr, surf.width, surf.height)
761
+ module._free(ptr)
762
+ },
763
+ async createGlb(systemId: number, onComplete: (resp: number) => void, onReject: (err: any) => void) {
764
+ let glbBuffer = 0
765
+ try {
766
+ const buf = getBuffer(systemId)
767
+ glbBuffer = module._malloc(buf.byteLength)
768
+ module.HEAPU8.set(new Uint8Array(buf), glbBuffer)
769
+
770
+ const entityId = module._loadGlb(glbBuffer, buf.byteLength)
771
+ // KTX2/Basis textures transcode asynchronously over several frames. Poll until fully loaded
772
+ // (progress >= 1) instead of a fixed ~100ms window — otherwise slower KTX2 transcodes don't
773
+ // finish in time and those textures render black. Generous cap (~5s) as a safety net.
774
+ for (let i = 0; i < 500; i++) {
775
+ if (module._glbLoadingUpdate() >= 1) break
776
+ await new Promise(res => setTimeout(res, 10))
777
+ }
778
+
779
+ onComplete(entityId)
780
+ } catch(e) {
781
+ // onReject hands the error to app code, so the global handler never sees it.
782
+ symbolizeWasmError(e)
783
+ onReject(e)
784
+ } finally {
785
+ if (glbBuffer !== 0) {
786
+ module._free(glbBuffer)
787
+ }
788
+ }
789
+ },
790
+ // ---- Animation system (docs/animation-plan.md): clip sets + animators ----
791
+ loadClips(systemId: number, onComplete: (clipSetId: number) => void, onReject: (err: unknown) => void) {
792
+ let ptr = 0
793
+ try {
794
+ const buf = getBuffer(systemId)
795
+ ptr = module._malloc(buf.byteLength)
796
+ module.HEAPU8.set(new Uint8Array(buf), ptr)
797
+ onComplete(module._loadClips(ptr, buf.byteLength))
798
+ } catch (e) {
799
+ symbolizeWasmError(e)
800
+ onReject(e)
801
+ } finally {
802
+ if (ptr !== 0) module._free(ptr)
803
+ }
804
+ },
805
+ createClipFromTracks(names: string, data: Float32Array) {
806
+ const nameLen = names.length * 3 + 1
807
+ const namePtr = module._malloc(nameLen)
808
+ module.stringToUTF8(names, namePtr, nameLen)
809
+ const dataPtr = module._malloc(data.byteLength)
810
+ module.HEAPF32.set(data, dataPtr / 4)
811
+ const id = module._createClipFromTracks(namePtr, dataPtr, data.length)
812
+ module._free(dataPtr)
813
+ module._free(namePtr)
814
+ return id
815
+ },
816
+ getClipSetInfo(clipSetId: number) {
817
+ const count = module._getClipSetCount(clipSetId)
818
+ if (count === 0) return []
819
+ const size = count * (4 + 1 + 200 + 2)
820
+ const ptr = module._malloc(size)
821
+ module._getClipSetInfo(clipSetId, ptr)
822
+ const view = new DataView(module.HEAPU8.buffer.slice(ptr, ptr + size))
823
+ module._free(ptr)
824
+ const out: { name: string, duration: number, trackCount: number }[] = []
825
+ let index = 0
826
+ for (let i = 0; i < count; i++) { out.push({ name: "", duration: view.getFloat32(index, true), trackCount: 0 }); index += 4 }
827
+ for (let i = 0; i < count; i++) {
828
+ const len = view.getUint8(index); index += 1
829
+ out[i].name = decoder.decode(new Uint8Array(view.buffer, index, len)); index += len
830
+ }
831
+ for (let i = 0; i < count; i++) { out[i].trackCount = view.getUint16(index, true); index += 2 }
832
+ return out
833
+ },
834
+ getGlbClipSet: (entityId: number) => module._getGlbClipSet(entityId),
835
+ sliceClip: (clipSetId: number, clip: number, start: number, end: number) => module._sliceClip(clipSetId, clip, start, end),
836
+ destroyClipSet: (clipSetId: number) => module._destroyClipSet(clipSetId),
837
+ animatorCreate: (entityId: number) => module._animatorCreate(entityId),
838
+ animatorDestroy: (animatorId: number) => module._animatorDestroy(animatorId),
839
+ setClipEvents(clipSetId: number, clip: number, times: Float32Array) {
840
+ const ptr = module._malloc(Math.max(4, times.length * 4))
841
+ module.HEAPF32.set(times, ptr / 4)
842
+ module._setClipEvents(clipSetId, clip, ptr, times.length)
843
+ module._free(ptr)
844
+ },
845
+ animatorBind: (animatorId: number, clipSetId: number, clipIndex: number, layer: number) =>
846
+ module._animatorBind(animatorId, clipSetId, clipIndex, layer),
847
+ animatorBoundTracks: (animatorId: number, slot: number) => module._animatorBoundTracks(animatorId, slot),
848
+ animatorSetBlend(animatorId: number, layer: number, dims: number, slots: Uint16Array, positions: Float32Array, fade: number, speed: number) {
849
+ const n = Math.min(slots.length, Math.floor(positions.length / dims))
850
+ const sp = module._malloc(Math.max(2, n * 2)), pp = module._malloc(Math.max(4, n * dims * 4))
851
+ module.HEAPU16.set(slots.subarray(0, n), sp / 2)
852
+ module.HEAPF32.set(positions.subarray(0, n * dims), pp / 4)
853
+ module._animatorSetBlend(animatorId, layer, dims, sp, pp, n, fade, speed)
854
+ module._free(sp); module._free(pp)
855
+ },
856
+ animatorSetBlendValue: (animatorId: number, layer: number, x: number, y: number) => module._animatorSetBlendValue(animatorId, layer, x, y),
857
+ animatorPlay: (animatorId: number, slot: number, loop: boolean, speed: number, fadeIn: number, fadeOut: number, restart: boolean) =>
858
+ module._animatorPlay(animatorId, slot, loop, speed, fadeIn, fadeOut, restart),
859
+ animatorStop: (animatorId: number, layer: number, slot: number, fade: number) => module._animatorStop(animatorId, layer, slot, fade),
860
+ animatorSeek: (animatorId: number, slot: number, time: number) => module._animatorSeek(animatorId, slot, time),
861
+ animatorGetSlotTime: (animatorId: number, slot: number) => module._animatorGetSlotTime(animatorId, slot),
862
+ animatorGetSlotWeight: (animatorId: number, slot: number) => module._animatorGetSlotWeight(animatorId, slot),
863
+ animatorSetLayer(animatorId: number, layer: number, weight: number, additive: boolean, maskRoot: string) {
864
+ const len = maskRoot.length * 3 + 1
865
+ const ptr = module._malloc(len)
866
+ module.stringToUTF8(maskRoot, ptr, len)
867
+ module._animatorSetLayer(animatorId, layer, weight, additive, ptr)
868
+ module._free(ptr)
869
+ },
870
+ animatorSetGlobal: (animatorId: number, speed: number, paused: boolean) => module._animatorSetGlobal(animatorId, speed, paused),
871
+ animatorSetRootMotion(animatorId: number, bone: string, apply: number) {
872
+ const len = bone.length * 3 + 1
873
+ const ptr = module._malloc(len)
874
+ module.stringToUTF8(bone, ptr, len)
875
+ module._animatorSetRootMotion(animatorId, ptr, apply)
876
+ module._free(ptr)
877
+ },
878
+ animatorGetRootMotion(animatorId: number, out: Float32Array) {
879
+ const ptr = module._malloc(12)
880
+ module._animatorGetRootMotion(animatorId, ptr)
881
+ out.set(module.HEAPF32.subarray(ptr / 4, ptr / 4 + 3))
882
+ module._free(ptr)
883
+ },
884
+ setOnAnimatorEvent: (callback: (animatorId: number, slot: number, type: number) => void) => module.onAnimatorEvent = callback,
885
+ async loadGlb(systemId: number, onComplete: (resp: ArrayBuffer) => void, onReject: (err: any) => void) {
886
+ let glbBuffer = -1
887
+ let buffer = -1
888
+ try {
889
+ const buf = getBuffer(systemId)
890
+ glbBuffer = module._malloc(buf.byteLength)
891
+ module.HEAPU8.set(new Uint8Array(buf), glbBuffer)
892
+
893
+ const entityId = module._loadGlb(glbBuffer, buf.byteLength)
894
+
895
+ // KTX2/Basis textures transcode asynchronously over several frames. Poll until fully loaded
896
+ // (progress >= 1) instead of a fixed ~100ms window — otherwise slower KTX2 transcodes don't
897
+ // finish in time and those textures render black. Generous cap (~5s) as a safety net.
898
+ for (let i = 0; i < 500; i++) {
899
+ if (module._glbLoadingUpdate() >= 1) break
900
+ await new Promise(res => setTimeout(res, 10))
901
+ }
902
+
903
+ buffer = module._malloc(8192*4)
904
+ const bufferSize = module._getGlbStructure(entityId, buffer)
905
+ const targetBuffer = module.HEAPU8.buffer.slice(buffer, buffer+bufferSize)
906
+
907
+ onComplete(targetBuffer as ArrayBuffer)
908
+ } catch (e) {
909
+ // onReject hands the error to app code, so the global handler never sees it.
910
+ symbolizeWasmError(e)
911
+ onReject(e)
912
+ } finally {
913
+ if (glbBuffer !== 0) {
914
+ module._free(glbBuffer)
915
+ }
916
+ if (buffer !== 0) {
917
+ module._free(buffer)
918
+ }
919
+ }
920
+ },
921
+ stopAllPlayers() {
922
+ removePlayers()
923
+ },
924
+ async createEngine() {},
925
+ async warmRender(sceneId: number, onComplete: () => void) {
926
+ module._setVisibleAll(sceneId, true)
927
+
928
+ for (let i = 0; i < 10; i++) {
929
+ if (module._renderScene(sceneId)) {
930
+ break
931
+ }
932
+ await new Promise(res => setTimeout(res, 10))
933
+ }
934
+
935
+ module._setVisibleAll(sceneId, false)
936
+ onComplete()
937
+ },
938
+ async captureImage(sceneId: number, onComplete: (bufferId: number, name: string, size: number) => void, onReject: () => void) {
939
+ const temp = new OffscreenCanvas(canvas.width, canvas.height)
940
+ const ctx = temp.getContext("2d")!
941
+ ctx.drawImage(canvas, 0, 0,)
942
+ const blob = await temp.convertToBlob({ type: "image/png" })
943
+ const buffer = await blob.arrayBuffer()
944
+ const bufferId = putBuffer(buffer)
945
+ onComplete(bufferId, "image.png", blob.size)
946
+ },
947
+ /** Draw one frame synchronously; true when a frame was actually drawn. Hosts call this
948
+ * immediately before reading the canvas (the simulator's "Save image") — without it the
949
+ * drawing buffer is already discarded and the 3D layer reads back empty. Filament may DECLINE
950
+ * a frame, so callers retry until it returns true (same shape as warmRender). */
951
+ _renderNow: renderOnce,
952
+ _fetch, // Здесь функция для внутреннего использования (в Simulator.vue)
953
+ _getPlayer: getPlayer,
954
+ _setARController(newARController: any) {
955
+ arController = newARController
956
+ },
957
+ _raycastView(offsetX: number, offsetY: number) {
958
+ return module._raycastView(offsetX * resFactor, offsetY * resFactor)
959
+ },
960
+ _createGLTexture(canvas: HTMLCanvasElement | OffscreenCanvas) {
961
+ const glTextureId = module.GL.getNewId(module.GL.textures)
962
+ const glTexture = gl.createTexture()
963
+ module.GL.textures[glTextureId] = glTexture
964
+
965
+ gl.bindTexture(gl.TEXTURE_2D, glTexture)
966
+ gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
967
+ gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
968
+ gl.pixelStorei(gl.UNPACK_COLORSPACE_CONVERSION_WEBGL, gl.NONE );
969
+ gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGBA8, canvas.width, canvas.height)
970
+
971
+ const textureId = module._importTexture(glTextureId, canvas.width, canvas.height)
972
+
973
+ return { gl, glTexture, textureId }
974
+ },
975
+ _dispose() {
976
+ this.closeScene()
977
+ this.stopAllPlayers()
978
+
979
+ // Release filament's headless swapchain while the context is still valid, then force the
980
+ // WebGL2 context to be freed now. Without loseContext the context lingers until the module is
981
+ // GC'd, and the browser caps live contexts (~16) — so reloads eventually drop the oldest.
982
+ try { module._destroySwapChain?.() } catch {}
983
+ gl.getExtension("WEBGL_lose_context")?.loseContext()
984
+
985
+ // These globals hold the context (and its canvas) alive process-wide; drop them so nothing
986
+ // outlives this run. Guard so a newer run's context (already swapped in) is never cleared.
987
+ if ((window as any).filament_glContext === gl) {
988
+ ;(window as any).filament_glContext = null
989
+ ;(window as any).filament_glOptions = null
990
+ }
991
+ }
992
+ }
993
+ }