lecodes-viewer 0.20.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "lecodes-viewer",
3
- "version": "0.20.0",
3
+ "version": "1.0.0",
4
4
  "main": "src/createViewer.ts",
5
5
  "dependencies": {
6
- "lecodes-renderer": "0.20.0",
6
+ "lecodes-renderer": "1.0.0",
7
7
  "ogl": "^1.0.11",
8
- "lecodes-viewer-lite": "0.20.0"
8
+ "lecodes-viewer-lite": "1.0.0"
9
9
  },
10
10
  "scripts": {
11
11
  "dev": "vite",
@@ -18,6 +18,8 @@ export { getBuffer } from './utils/fetch'
18
18
  export { decodeKtx2Image, isKtx2Bytes } from 'lecodes-viewer-lite/src/gl/decoders'
19
19
  export type { CreatorMethods, EngineMode, Host, HostOptions, Run, RunOptions, UIFactory } from './host'
20
20
  export type { PreviewError, MappedFrame } from './sourcemapError'
21
+ // The render tree the canvas UI hands back (`_creatorUI.renderTree()`) — the embedder's verify gates walk it.
22
+ export type { RenderNode } from 'lecodes-renderer'
21
23
  export { createCanvasUIRenderer } from './ui/mountCanvasUI'
22
24
  export { createBareUI } from './ui/createBareUI'
23
25
  export type { SafeAreaEdge, SafeAreaSpec } from './ui/safeAreaSpec'
File without changes
@@ -1,212 +1,212 @@
1
- import type { UIScreen } from './UIScreen'
2
-
3
- type MountedItem = {
4
- rootPtr: number // detached yoga root айтема
5
- el: HTMLElement // DOM-корень поддерева
6
- jsSubtree: any // JS-объект поддерева (для null _id при unmount)
7
- }
8
-
9
- export type VListInstance = {
10
- nodeId: number
11
- el: HTMLElement
12
- jsNode: any // VListElement
13
- screen: UIScreen
14
- pendingScroll: number | null
15
- items: Map<string, MountedItem>
16
- }
17
-
18
- export const createVListSystem = (module: any) => {
19
-
20
- const instances = new Map<number, VListInstance>() // yogaId vlist-ноды → instance
21
-
22
- // ---------- marshaling ----------
23
-
24
- const withKey = <R>(key: string, fn: (ptr: number) => R): R => {
25
- const len = module.lengthBytesUTF8(key) + 1
26
- const p = module._malloc(len)
27
- module.stringToUTF8(key, p, len)
28
- try { return fn(p) } finally { module._free(p) }
29
- }
30
-
31
- const withStringArray = <R>(arr: string[], fn: (arrPtr: number) => R): R => {
32
- const ptrs = arr.map(s => {
33
- const len = module.lengthBytesUTF8(s) + 1
34
- const p = module._malloc(len)
35
- module.stringToUTF8(s, p, len)
36
- return p
37
- })
38
- const arrPtr = module._malloc(Math.max(4 * ptrs.length, 4))
39
- ptrs.forEach((p, i) => { module.HEAPU32[(arrPtr >> 2) + i] = p })
40
- try { return fn(arrPtr) } finally {
41
- ptrs.forEach(p => module._free(p))
42
- module._free(arrPtr)
43
- }
44
- }
45
-
46
- const withFloatArray = <R>(arr: number[], fn: (ptr: number) => R): R => {
47
- const p = module._malloc(Math.max(4 * arr.length, 4))
48
- arr.forEach((v, i) => { module.HEAPF32[(p >> 2) + i] = v })
49
- try { return fn(p) } finally { module._free(p) }
50
- }
51
-
52
- const readStringArray = (arrPtr: number, count: number): string[] => {
53
- const out: string[] = []
54
- for (let i = 0; i < count; i++) {
55
- out.push(module.UTF8ToString(module.HEAPU32[(arrPtr >> 2) + i]))
56
- }
57
- return out
58
- }
59
-
60
- // ---------- layout-проход по айтемам ----------
61
-
62
- const refresh = (inst: VListInstance) => {
63
- for (const [key, item] of inst.items) {
64
- inst.screen.updateSizesFor(item.el)
65
- item.el.style.top = withKey(key, p => module._vlistOffsetOf(inst.nodeId, p)) + "px"
66
- item.el.style.left = "0px"
67
- }
68
- const h = module._getContentHeight(inst.nodeId)
69
- inst.el.style.setProperty("--scroll-height", h + "px")
70
- inst.el.style.setProperty("--scroll-width", "0px")
71
-
72
- if (inst.pendingScroll !== null) {
73
- if (Math.abs(inst.el.scrollTop - inst.pendingScroll) >= 1) {
74
- inst.el.scrollTop = inst.pendingScroll // высота уже выставлена — кламп не сработает
75
- }
76
- inst.pendingScroll = null
77
- }
78
- }
79
-
80
- // ---------- колбэки из C++ (регистрируются один раз) ----------
81
-
82
- // g_vlSync
83
- const syncFunc = module.addFunction(
84
- (_screenId: number, nodePtr: number, unmountArr: number, unmountCount: number, mountArr: number, mountCount: number) => {
85
- const inst = instances.get(nodePtr)
86
- if (!inst) return
87
- const unmountKeys = readStringArray(unmountArr, unmountCount)
88
- const mountKeys = readStringArray(mountArr, mountCount)
89
-
90
- // Контракт: интеграционный слой сносит поддеревья ДО вызова JS _syncWindow
91
- for (const key of unmountKeys) {
92
- const item = inst.items.get(key)
93
- if (!item) continue
94
- inst.items.delete(key)
95
- item.el.remove()
96
- inst.screen.releaseSubtree(item.jsSubtree) // nodeMap-чистка + _id = 0
97
- withKey(key, p => module._vlistItemUnmounted(nodePtr, p)) // free yoga root
98
- }
99
-
100
- inst.jsNode._syncWindow(unmountKeys, mountKeys) // mount: render() → vlistMount()
101
- refresh(inst)
102
- }, "viiiiii")
103
-
104
- // g_vlAdjust — якорение / кламп
105
- const adjustFunc = module.addFunction((_screenId: number, nodePtr: number, scrollY: number) => {
106
- const inst = instances.get(nodePtr)
107
- if (!inst) return
108
- inst.pendingScroll = scrollY // применим в refresh, после высоты
109
- }, "viif")
110
-
111
- // g_vlEdge
112
- const edgeFunc = module.addFunction((_screenId: number, nodePtr: number, endEdge: number) => {
113
- const inst = instances.get(nodePtr)
114
- if (!inst) return
115
- const listeners = endEdge ? (inst.jsNode.erl ?? []) : (inst.jsNode.strl ?? [])
116
- for (const cb of listeners) cb()
117
- }, "viii")
118
-
119
- module._setVListHandlers(syncFunc, adjustFunc, edgeFunc)
120
-
121
- // ---------- создание (из UIScreen.addNodeRecursive для type === "vlist") ----------
122
-
123
- const register = (screen: UIScreen, jsNode: any, el: HTMLElement, yogaId: number) => {
124
- const inst: VListInstance = { nodeId: yogaId, el, jsNode, screen, items: new Map(), pendingScroll: null }
125
- instances.set(yogaId, inst)
126
-
127
- module._vlistInit(yogaId, jsNode._overscan, jsNode._inverted ? 1 : 0,
128
- jsNode._startThreshold, jsNode._endThreshold)
129
-
130
- if (jsNode._keys.length > 0) {
131
- setKeysInternal(yogaId, jsNode._keys, jsNode._estimates)
132
- }
133
-
134
- el.addEventListener("scroll", () => {
135
- module._vlistOnScroll(yogaId, el.scrollTop)
136
- refresh(inst)
137
- for (const cb of jsNode.sl ?? []) cb(el.scrollTop)
138
- })
139
- }
140
-
141
- // Вызывается из UIScreen.updateSizes для vlist-элемента (после выставления его размеров)
142
- const updateLayout = (yogaId: number) => {
143
- const inst = instances.get(yogaId)
144
- if (!inst) return
145
- module._vlistUpdateLayout(yogaId) // внутри может дернуть sync → mount
146
- refresh(inst)
147
- }
148
-
149
- const unregister = (yogaId: number) => {
150
- const inst = instances.get(yogaId)
151
- if (!inst) return
152
- module._vlistFree(yogaId) // освобождает detached-корни
153
- instances.delete(yogaId)
154
- }
155
-
156
- const setKeysInternal = (yogaId: number, keys: string[], estimates: number[]) => {
157
- withStringArray(keys, kp =>
158
- withFloatArray(estimates, ep =>
159
- module._vlistSetKeys(yogaId, kp, ep, keys.length)))
160
- }
161
-
162
- // ---------- API для _creatorUI (зовётся из VListElement) ----------
163
-
164
- const api = {
165
- vlistSetKeys: (jsNode: any, keys: string[], estimates: number[]) => {
166
- setKeysInternal(jsNode._id, keys, estimates)
167
- },
168
- vlistInsertKeys: (jsNode: any, index: number, keys: string[], estimates: number[]) => {
169
- withStringArray(keys, kp =>
170
- withFloatArray(estimates, ep =>
171
- module._vlistInsertKeys(jsNode._id, index, kp, ep, keys.length)))
172
- },
173
- vlistRemoveKeys: (jsNode: any, keys: string[]) => {
174
- withStringArray(keys, kp => module._vlistRemoveKeys(jsNode._id, kp, keys.length))
175
- },
176
- vlistInvalidate: (jsNode: any, key: string) => {
177
- withKey(key, p => module._vlistInvalidate(jsNode._id, p))
178
- },
179
- // Вызывается из _syncWindow на каждый mount-ключ
180
- vlistMount: (jsNode: any, key: string, subtree: any) => {
181
- const inst = instances.get(jsNode._id)
182
- if (!inst) return
183
- const rootPtr = withKey(key, p => module._vlistCreateItemRoot(jsNode._id, p))
184
- const el = inst.screen.buildSubtree(subtree, rootPtr) // addNodeRecursive в detached root
185
- inst.el.appendChild(el)
186
- inst.items.set(key, { rootPtr, el, jsSubtree: subtree })
187
- withKey(key, p => module._vlistItemMounted(jsNode._id, p)) // измерение + кэш + якорение
188
- },
189
- command: (jsNode: any, name: string, ...args: any[]) => {
190
- const inst = instances.get(jsNode._id)
191
- if (!inst) return
192
- const behavior = (animated: boolean) => animated ? "smooth" as const : "auto" as const
193
- switch (name) {
194
- case "scrollTo":
195
- inst.el.scrollTo({ top: args[0], behavior: behavior(args[1] ?? true) })
196
- break
197
- case "scrollToKey": {
198
- const off = withKey(args[0], p => module._vlistOffsetOf(inst.nodeId, p))
199
- if (!isNaN(off)) inst.el.scrollTo({ top: off, behavior: behavior(args[1] ?? true) })
200
- break
201
- }
202
- case "scrollToEnd": {
203
- const h = module._getContentHeight(inst.nodeId)
204
- inst.el.scrollTo({ top: h - inst.el.clientHeight, behavior: behavior(args[0] ?? true) })
205
- break
206
- }
207
- }
208
- },
209
- }
210
-
211
- return { register, updateLayout, unregister, refresh, api, instances }
1
+ import type { UIScreen } from './UIScreen'
2
+
3
+ type MountedItem = {
4
+ rootPtr: number // detached yoga root айтема
5
+ el: HTMLElement // DOM-корень поддерева
6
+ jsSubtree: any // JS-объект поддерева (для null _id при unmount)
7
+ }
8
+
9
+ export type VListInstance = {
10
+ nodeId: number
11
+ el: HTMLElement
12
+ jsNode: any // VListElement
13
+ screen: UIScreen
14
+ pendingScroll: number | null
15
+ items: Map<string, MountedItem>
16
+ }
17
+
18
+ export const createVListSystem = (module: any) => {
19
+
20
+ const instances = new Map<number, VListInstance>() // yogaId vlist-ноды → instance
21
+
22
+ // ---------- marshaling ----------
23
+
24
+ const withKey = <R>(key: string, fn: (ptr: number) => R): R => {
25
+ const len = module.lengthBytesUTF8(key) + 1
26
+ const p = module._malloc(len)
27
+ module.stringToUTF8(key, p, len)
28
+ try { return fn(p) } finally { module._free(p) }
29
+ }
30
+
31
+ const withStringArray = <R>(arr: string[], fn: (arrPtr: number) => R): R => {
32
+ const ptrs = arr.map(s => {
33
+ const len = module.lengthBytesUTF8(s) + 1
34
+ const p = module._malloc(len)
35
+ module.stringToUTF8(s, p, len)
36
+ return p
37
+ })
38
+ const arrPtr = module._malloc(Math.max(4 * ptrs.length, 4))
39
+ ptrs.forEach((p, i) => { module.HEAPU32[(arrPtr >> 2) + i] = p })
40
+ try { return fn(arrPtr) } finally {
41
+ ptrs.forEach(p => module._free(p))
42
+ module._free(arrPtr)
43
+ }
44
+ }
45
+
46
+ const withFloatArray = <R>(arr: number[], fn: (ptr: number) => R): R => {
47
+ const p = module._malloc(Math.max(4 * arr.length, 4))
48
+ arr.forEach((v, i) => { module.HEAPF32[(p >> 2) + i] = v })
49
+ try { return fn(p) } finally { module._free(p) }
50
+ }
51
+
52
+ const readStringArray = (arrPtr: number, count: number): string[] => {
53
+ const out: string[] = []
54
+ for (let i = 0; i < count; i++) {
55
+ out.push(module.UTF8ToString(module.HEAPU32[(arrPtr >> 2) + i]))
56
+ }
57
+ return out
58
+ }
59
+
60
+ // ---------- layout-проход по айтемам ----------
61
+
62
+ const refresh = (inst: VListInstance) => {
63
+ for (const [key, item] of inst.items) {
64
+ inst.screen.updateSizesFor(item.el)
65
+ item.el.style.top = withKey(key, p => module._vlistOffsetOf(inst.nodeId, p)) + "px"
66
+ item.el.style.left = "0px"
67
+ }
68
+ const h = module._getContentHeight(inst.nodeId)
69
+ inst.el.style.setProperty("--scroll-height", h + "px")
70
+ inst.el.style.setProperty("--scroll-width", "0px")
71
+
72
+ if (inst.pendingScroll !== null) {
73
+ if (Math.abs(inst.el.scrollTop - inst.pendingScroll) >= 1) {
74
+ inst.el.scrollTop = inst.pendingScroll // высота уже выставлена — кламп не сработает
75
+ }
76
+ inst.pendingScroll = null
77
+ }
78
+ }
79
+
80
+ // ---------- колбэки из C++ (регистрируются один раз) ----------
81
+
82
+ // g_vlSync
83
+ const syncFunc = module.addFunction(
84
+ (_screenId: number, nodePtr: number, unmountArr: number, unmountCount: number, mountArr: number, mountCount: number) => {
85
+ const inst = instances.get(nodePtr)
86
+ if (!inst) return
87
+ const unmountKeys = readStringArray(unmountArr, unmountCount)
88
+ const mountKeys = readStringArray(mountArr, mountCount)
89
+
90
+ // Контракт: интеграционный слой сносит поддеревья ДО вызова JS _syncWindow
91
+ for (const key of unmountKeys) {
92
+ const item = inst.items.get(key)
93
+ if (!item) continue
94
+ inst.items.delete(key)
95
+ item.el.remove()
96
+ inst.screen.releaseSubtree(item.jsSubtree) // nodeMap-чистка + _id = 0
97
+ withKey(key, p => module._vlistItemUnmounted(nodePtr, p)) // free yoga root
98
+ }
99
+
100
+ inst.jsNode._syncWindow(unmountKeys, mountKeys) // mount: render() → vlistMount()
101
+ refresh(inst)
102
+ }, "viiiiii")
103
+
104
+ // g_vlAdjust — якорение / кламп
105
+ const adjustFunc = module.addFunction((_screenId: number, nodePtr: number, scrollY: number) => {
106
+ const inst = instances.get(nodePtr)
107
+ if (!inst) return
108
+ inst.pendingScroll = scrollY // применим в refresh, после высоты
109
+ }, "viif")
110
+
111
+ // g_vlEdge
112
+ const edgeFunc = module.addFunction((_screenId: number, nodePtr: number, endEdge: number) => {
113
+ const inst = instances.get(nodePtr)
114
+ if (!inst) return
115
+ const listeners = endEdge ? (inst.jsNode.erl ?? []) : (inst.jsNode.strl ?? [])
116
+ for (const cb of listeners) cb()
117
+ }, "viii")
118
+
119
+ module._setVListHandlers(syncFunc, adjustFunc, edgeFunc)
120
+
121
+ // ---------- создание (из UIScreen.addNodeRecursive для type === "vlist") ----------
122
+
123
+ const register = (screen: UIScreen, jsNode: any, el: HTMLElement, yogaId: number) => {
124
+ const inst: VListInstance = { nodeId: yogaId, el, jsNode, screen, items: new Map(), pendingScroll: null }
125
+ instances.set(yogaId, inst)
126
+
127
+ module._vlistInit(yogaId, jsNode._overscan, jsNode._inverted ? 1 : 0,
128
+ jsNode._startThreshold, jsNode._endThreshold)
129
+
130
+ if (jsNode._keys.length > 0) {
131
+ setKeysInternal(yogaId, jsNode._keys, jsNode._estimates)
132
+ }
133
+
134
+ el.addEventListener("scroll", () => {
135
+ module._vlistOnScroll(yogaId, el.scrollTop)
136
+ refresh(inst)
137
+ for (const cb of jsNode.sl ?? []) cb(el.scrollTop)
138
+ })
139
+ }
140
+
141
+ // Вызывается из UIScreen.updateSizes для vlist-элемента (после выставления его размеров)
142
+ const updateLayout = (yogaId: number) => {
143
+ const inst = instances.get(yogaId)
144
+ if (!inst) return
145
+ module._vlistUpdateLayout(yogaId) // внутри может дернуть sync → mount
146
+ refresh(inst)
147
+ }
148
+
149
+ const unregister = (yogaId: number) => {
150
+ const inst = instances.get(yogaId)
151
+ if (!inst) return
152
+ module._vlistFree(yogaId) // освобождает detached-корни
153
+ instances.delete(yogaId)
154
+ }
155
+
156
+ const setKeysInternal = (yogaId: number, keys: string[], estimates: number[]) => {
157
+ withStringArray(keys, kp =>
158
+ withFloatArray(estimates, ep =>
159
+ module._vlistSetKeys(yogaId, kp, ep, keys.length)))
160
+ }
161
+
162
+ // ---------- API для _creatorUI (зовётся из VListElement) ----------
163
+
164
+ const api = {
165
+ vlistSetKeys: (jsNode: any, keys: string[], estimates: number[]) => {
166
+ setKeysInternal(jsNode._id, keys, estimates)
167
+ },
168
+ vlistInsertKeys: (jsNode: any, index: number, keys: string[], estimates: number[]) => {
169
+ withStringArray(keys, kp =>
170
+ withFloatArray(estimates, ep =>
171
+ module._vlistInsertKeys(jsNode._id, index, kp, ep, keys.length)))
172
+ },
173
+ vlistRemoveKeys: (jsNode: any, keys: string[]) => {
174
+ withStringArray(keys, kp => module._vlistRemoveKeys(jsNode._id, kp, keys.length))
175
+ },
176
+ vlistInvalidate: (jsNode: any, key: string) => {
177
+ withKey(key, p => module._vlistInvalidate(jsNode._id, p))
178
+ },
179
+ // Вызывается из _syncWindow на каждый mount-ключ
180
+ vlistMount: (jsNode: any, key: string, subtree: any) => {
181
+ const inst = instances.get(jsNode._id)
182
+ if (!inst) return
183
+ const rootPtr = withKey(key, p => module._vlistCreateItemRoot(jsNode._id, p))
184
+ const el = inst.screen.buildSubtree(subtree, rootPtr) // addNodeRecursive в detached root
185
+ inst.el.appendChild(el)
186
+ inst.items.set(key, { rootPtr, el, jsSubtree: subtree })
187
+ withKey(key, p => module._vlistItemMounted(jsNode._id, p)) // измерение + кэш + якорение
188
+ },
189
+ command: (jsNode: any, name: string, ...args: any[]) => {
190
+ const inst = instances.get(jsNode._id)
191
+ if (!inst) return
192
+ const behavior = (animated: boolean) => animated ? "smooth" as const : "auto" as const
193
+ switch (name) {
194
+ case "scrollTo":
195
+ inst.el.scrollTo({ top: args[0], behavior: behavior(args[1] ?? true) })
196
+ break
197
+ case "scrollToKey": {
198
+ const off = withKey(args[0], p => module._vlistOffsetOf(inst.nodeId, p))
199
+ if (!isNaN(off)) inst.el.scrollTo({ top: off, behavior: behavior(args[1] ?? true) })
200
+ break
201
+ }
202
+ case "scrollToEnd": {
203
+ const h = module._getContentHeight(inst.nodeId)
204
+ inst.el.scrollTo({ top: h - inst.el.clientHeight, behavior: behavior(args[0] ?? true) })
205
+ break
206
+ }
207
+ }
208
+ },
209
+ }
210
+
211
+ return { register, updateLayout, unregister, refresh, api, instances }
212
212
  }
@@ -99,6 +99,11 @@ function configureImageSrc(host: ImageHost, el: HTMLElement, src: ImageSrc, tint
99
99
  ;(el as any)._canvasSurface = surfaceId
100
100
  el.removeAttribute("data-viewbox")
101
101
  el.src = host.canvasHost?.surfaceToDataUrl(surfaceId) ?? ""
102
+ } else if (typeof src === "object" && "scene2d" in src) {
103
+ // UIImage(scene2d): a live 2D scene drawn into the box — desktop only so far (parity
104
+ // ui-scene2d-image); a blank box here rather than a bogus blob url.
105
+ el.removeAttribute("data-viewbox")
106
+ el.removeAttribute("src")
102
107
  } else {
103
108
  el.removeAttribute("data-viewbox")
104
109
  el.src = typeof src === "object" ? getUrl(src._id) : src
@@ -233,7 +233,7 @@ export const createViewer = async (canvas: HTMLCanvasElement, options: ViewerOpt
233
233
  // Each read-back writes into a caller-owned `out`, so there's no shared-buffer aliasing — we only
234
234
  // drop the per-call malloc/free + view allocation. Read module.HEAPF32 FRESH each call (emscripten
235
235
  // swaps that view on heap growth; the scratch address survives).
236
- const READ_SCRATCH = 64 // largest reader: vehicleGetState = 7 + 3 per wheel
236
+ const READ_SCRATCH = 128 // largest reader: vehicleGetState = 11 + 11 per wheel
237
237
  const readScratchPtr = module._malloc(READ_SCRATCH * 4)
238
238
  const readInto = (out: Float32Array, n: number, fn: (ptr: number) => void): void => {
239
239
  fn(readScratchPtr)
@@ -396,16 +396,18 @@ export const createViewer = async (canvas: HTMLCanvasElement, options: ViewerOpt
396
396
  return id
397
397
  },
398
398
  vehicleDestroy: (vehicleId: number) => module._vehicleDestroy(vehicleId),
399
- vehicleSetTransmission: (vehicleId: number, ratio: number, clutch: number) =>
400
- module._vehicleSetTransmission(vehicleId, ratio, clutch),
401
399
  vehicleSetTuning: (vehicleId: number, settings: Float32Array) => {
402
400
  const ptr = module._malloc(settings.byteLength)
403
401
  module.HEAPF32.set(settings, ptr / 4)
404
402
  module._vehicleSetTuning(vehicleId, ptr, settings.length)
405
403
  module._free(ptr)
406
404
  },
407
- vehicleSetInput: (vehicleId: number, forward: number, right: number, brake: number, handBrake: number) =>
408
- module._vehicleSetInput(vehicleId, forward, right, brake, handBrake),
405
+ vehicleSetInput: (vehicleId: number, input: Float32Array) => {
406
+ const ptr = module._malloc(input.length * 4)
407
+ module.HEAPF32.set(input, ptr >> 2)
408
+ module._vehicleSetInput(vehicleId, ptr, input.length)
409
+ module._free(ptr)
410
+ },
409
411
  vehicleGetState: (vehicleId: number, out: Float32Array) =>
410
412
  readInto(out, Math.min(out.length, READ_SCRATCH), (ptr) => module._vehicleGetState(vehicleId, ptr, Math.min(out.length, READ_SCRATCH))),
411
413
  vehicleReset: (vehicleId: number, x: number, y: number, z: number, qx: number, qy: number, qz: number, qw: number) =>
@@ -440,6 +442,17 @@ export const createViewer = async (canvas: HTMLCanvasElement, options: ViewerOpt
440
442
  // Screen inputs are logical px; scale to backing px for the native (viewport-space) pick.
441
443
  raycastView: (offsetX: number, offsetY: number) => module._raycastView(offsetX * resFactor, offsetY * resFactor),
442
444
  setCulling: module._setCulling,
445
+ // Draw order / per-instance depth state (Mesh.renderPriority, Material.depthTest / depthWrite) —
446
+ // only once the wasm carries the exports; the SDK feature-detects them.
447
+ setRenderPriority: module._setRenderPriority,
448
+ setMaterialDepthTest: module._setMaterialDepthTest ? (id: number, on: boolean) => module._setMaterialDepthTest(id, on ? 1 : 0) : undefined,
449
+ setMaterialDepthWrite: module._setMaterialDepthWrite ? (id: number, on: boolean) => module._setMaterialDepthWrite(id, on ? 1 : 0) : undefined,
450
+ setMaterialCulling: module._setMaterialCulling,
451
+ setMaterialStencil: module._setMaterialStencil
452
+ ? (id: number, write: boolean, test: number, ref: number, onPass: number, onFail: number, onDepthFail: number, readMask: number, writeMask: number) =>
453
+ module._setMaterialStencil(id, write ? 1 : 0, test, ref, onPass, onFail, onDepthFail, readMask, writeMask)
454
+ : undefined,
455
+ setSceneStencil: module._setSceneStencil ? (sceneId: number, on: boolean) => module._setSceneStencil(sceneId, on ? 1 : 0) : undefined,
443
456
  setGlbCulling: module._setGlbCulling,
444
457
  // Culling default for animated GLBs (Model.load): only once the wasm carries the export — an older
445
458
  // binary leaves the SDK on the always-draw default (see bridges.d.ts).
@@ -615,7 +628,7 @@ export const createViewer = async (canvas: HTMLCanvasElement, options: ViewerOpt
615
628
  createMaterial: (systemId: number) => {
616
629
  return module._createMaterialInstance(compileMaterial(systemId))
617
630
  },
618
- setMesh(entityId: number, materialId: number, vertices: Float32Array, normals: Float32Array, indices: Uint16Array, uv: Float32Array, meshType: number, uv1?: Float32Array) {
631
+ setMesh(entityId: number, materialId: number, vertices: Float32Array, normals: Float32Array, indices: Uint16Array, uv: Float32Array, meshType: number, uv1?: Float32Array, colors?: Uint8Array) {
619
632
  const ptrVertices = module._malloc(vertices.byteLength)
620
633
  const ptrNormals = module._malloc(normals.byteLength)
621
634
  const ptrIndices = module._malloc(indices.byteLength)
@@ -625,6 +638,15 @@ export const createViewer = async (canvas: HTMLCanvasElement, options: ViewerOpt
625
638
  module.HEAPU16.set(indices, ptrIndices/2)
626
639
  module.HEAPF32.set(uv, ptrUv/4)
627
640
 
641
+ // the vertex colours ride the newest export (createMeshC); a wasm without it draws white
642
+ if (colors && module._createMeshC) {
643
+ const ptrUv1 = uv1 ? module._malloc(uv1.byteLength) : 0
644
+ if (uv1 && ptrUv1) module.HEAPF32.set(uv1, ptrUv1 / 4)
645
+ const ptrColors = module._malloc(colors.byteLength)
646
+ module.HEAPU8.set(colors, ptrColors)
647
+ module._createMeshC(entityId, materialId, ptrVertices, ptrNormals, ptrIndices, ptrUv, vertices.length / 3, indices.length, meshType, ptrUv1, ptrColors)
648
+ return
649
+ }
628
650
  // the lightmap UV set rides the newer export; a wasm without it duplicates uv into UV1
629
651
  if (uv1 && module._createMeshEx) {
630
652
  const ptrUv1 = module._malloc(uv1.byteLength)
@@ -832,7 +854,11 @@ export const createViewer = async (canvas: HTMLCanvasElement, options: ViewerOpt
832
854
  return out
833
855
  },
834
856
  getGlbClipSet: (entityId: number) => module._getGlbClipSet(entityId),
835
- sliceClip: (clipSetId: number, clip: number, start: number, end: number) => module._sliceClip(clipSetId, clip, start, end),
857
+ deriveClip: (clipSetId: number, clip: number, mirror: boolean, start: number, end: number) => {
858
+ if (typeof module._deriveClip === "function") return module._deriveClip(clipSetId, clip, mirror ? 1 : 0, start, end)
859
+ if (mirror) { console.warn("creator-gl: this wasm cannot mirror clips — rebuild it"); return 0 } // a wasm from before 2026-09-06
860
+ return module._sliceClip(clipSetId, clip, start < 0 ? 0 : start, end < 0 ? 1e9 : end)
861
+ },
836
862
  destroyClipSet: (clipSetId: number) => module._destroyClipSet(clipSetId),
837
863
  animatorCreate: (entityId: number) => module._animatorCreate(entityId),
838
864
  animatorDestroy: (animatorId: number) => module._animatorDestroy(animatorId),
File without changes
Binary file
Binary file
File without changes