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.
- package/README.md +5 -0
- package/dist-embed/VERSION.json +5 -0
- package/dist-embed/assets/Roboto-Variable-DHm9jpd-.woff2 +0 -0
- package/dist-embed/assets/__vite-browser-external-BIHI7g3E.js +1 -0
- package/dist-embed/assets/basis-C64VHDVD.js +1 -0
- package/dist-embed/assets/basis_transcoder-VXdx5NbI.wasm +0 -0
- package/dist-embed/assets/createViewer2d-D12Wy3VF.js +1 -0
- package/dist-embed/assets/createViewerLite-CP-WHGLg.js +707 -0
- package/dist-embed/assets/creator-gl-BF2CQzeI.wasm +0 -0
- package/dist-embed/assets/creator-gl.js-B_0GrQRe.symbols +5379 -0
- package/dist-embed/assets/creator-ui-BX5Fvnbt.js +1 -0
- package/dist-embed/assets/creator-ui-CzKHLH2Q.wasm +0 -0
- package/dist-embed/assets/creator2d-BtQ-1_B0.wasm +0 -0
- package/dist-embed/assets/draco-BiISTFcR.js +118 -0
- package/dist-embed/assets/draco_decoder-DsQ12WqX.wasm +0 -0
- package/dist-embed/assets/host-CWKB1WDd.js +162 -0
- package/dist-embed/assets/index-CIK8PnUA.js +2 -0
- package/dist-embed/assets/lit-DUE9rhY9.filamat +0 -0
- package/dist-embed/assets/loadCreatorUiNode-UAdhjRTd.js +1 -0
- package/dist-embed/assets/neutral_ibl128-D-lzdkLN.ktx +0 -0
- package/dist-embed/assets/particles-DxYDmI1O.filamat +0 -0
- package/dist-embed/assets/particles-quad-DwYo9yCK.filamat +0 -0
- package/dist-embed/assets/shadow-C9xJpqlY.filamat +0 -0
- package/dist-embed/assets/uberarchive-uXjRYa9p.bin +0 -0
- package/dist-embed/assets/unlit-pe4tFY5b.filamat +0 -0
- package/dist-embed/assets/unlit-transparent-D9OyaOBR.filamat +0 -0
- package/dist-embed/assets/video-eedvhMvS.filamat +0 -0
- package/dist-embed/assets/worker-Caf-yYEI.js +2 -0
- package/dist-embed/embed.html +48 -0
- package/dist-embed/embed.js +187 -0
- package/embed.html +48 -0
- package/package.json +35 -0
- package/src/assets/banner.png +0 -0
- package/src/assets/image2.png +0 -0
- package/src/assets/landing.mp4 +0 -0
- package/src/assets/neutral_ibl128.ktx +0 -0
- package/src/assets/neutral_ibl256.ktx +0 -0
- package/src/assets/output.mp4 +0 -0
- package/src/assets/output2.mp4 +0 -0
- package/src/assets/texture.png +0 -0
- package/src/assets/vue.svg +1 -0
- package/src/canvas/createCanvasHost.ts +2 -0
- package/src/createViewer.ts +103 -0
- package/src/embed.ts +515 -0
- package/src/host.ts +428 -0
- package/src/lite-test.ts +160 -0
- package/src/main.ts +77 -0
- package/src/sourcemapError.ts +2 -0
- package/src/ui/CuiProps.gen.ts +103 -0
- package/src/ui/UIScreen.ts +486 -0
- package/src/ui/createBareUI.ts +32 -0
- package/src/ui/createMeasureSystem.ts +220 -0
- package/src/ui/createNodeElement.ts +77 -0
- package/src/ui/createPagerSystem.ts +345 -0
- package/src/ui/createTouchSystem.ts +2 -0
- package/src/ui/createUI.ts +806 -0
- package/src/ui/creator-ui.js +14 -0
- package/src/ui/creator-ui.wasm +0 -0
- package/src/ui/defaultStyle.css +176 -0
- package/src/ui/layers.ts +9 -0
- package/src/ui/mountCanvasUI.ts +213 -0
- package/src/ui/nativeViewSystem.ts +91 -0
- package/src/ui/presentPlayer.ts +156 -0
- package/src/ui/props-apply.ts +403 -0
- package/src/ui/router.ts +2 -0
- package/src/ui/safeAreaSpec.ts +3 -0
- package/src/ui/shells.ts +37 -0
- package/src/ui/toasts.ts +2 -0
- package/src/ui/transitions.ts +250 -0
- package/src/ui/ui.d.ts +78 -0
- package/src/ui/vListSystem.ts +212 -0
- package/src/ui/viewScale.ts +2 -0
- package/src/utils/animate.ts +2 -0
- package/src/utils/createUtils.ts +2 -0
- package/src/utils/fetch.ts +3 -0
- package/src/utils/fonts.ts +2 -0
- package/src/utils/globals.ts +2 -0
- package/src/utils/imageNode.ts +260 -0
- package/src/utils/openFilePicker.ts +2 -0
- package/src/utils/svg.ts +2 -0
- package/src/utils/videoNode.ts +2 -0
- package/src/utils/ws.ts +2 -0
- package/src/viewer/createViewer.ts +993 -0
- package/src/viewer/creator-gl.js +14 -0
- package/src/viewer/creator-gl.js.symbols +5379 -0
- package/src/viewer/creator-gl.wasm +0 -0
- package/src/viewer/lit.filamat +0 -0
- package/src/viewer/mediaplayer.ts +3 -0
- package/src/viewer/particles-quad.filamat +0 -0
- package/src/viewer/particles.filamat +0 -0
- package/src/viewer/physics.ts +140 -0
- package/src/viewer/shadow.filamat +0 -0
- package/src/viewer/uberarchive.bin +0 -0
- package/src/viewer/unlit-transparent.filamat +0 -0
- package/src/viewer/unlit.filamat +0 -0
- package/src/viewer/validateGlb.ts +262 -0
- package/src/viewer/video.filamat +0 -0
- package/src/viewer/wasmCrashSymbolizer.ts +133 -0
- package/src/viewer2d/createViewer2d.ts +438 -0
- package/src/viewer2d/creator2d.js +14 -0
- package/src/viewer2d/creator2d.wasm +0 -0
- package/src/viewer2d/sprite2d.ts +80 -0
- 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
|
+
}
|