lecodes-viewer 0.20.0 → 0.20.2

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": "0.20.2",
4
4
  "main": "src/createViewer.ts",
5
5
  "dependencies": {
6
- "lecodes-renderer": "0.20.0",
6
+ "lecodes-renderer": "0.20.2",
7
7
  "ogl": "^1.0.11",
8
- "lecodes-viewer-lite": "0.20.0"
8
+ "lecodes-viewer-lite": "0.20.2"
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
@@ -440,6 +440,17 @@ export const createViewer = async (canvas: HTMLCanvasElement, options: ViewerOpt
440
440
  // Screen inputs are logical px; scale to backing px for the native (viewport-space) pick.
441
441
  raycastView: (offsetX: number, offsetY: number) => module._raycastView(offsetX * resFactor, offsetY * resFactor),
442
442
  setCulling: module._setCulling,
443
+ // Draw order / per-instance depth state (Mesh.renderPriority, Material.depthTest / depthWrite) —
444
+ // only once the wasm carries the exports; the SDK feature-detects them.
445
+ setRenderPriority: module._setRenderPriority,
446
+ setMaterialDepthTest: module._setMaterialDepthTest ? (id: number, on: boolean) => module._setMaterialDepthTest(id, on ? 1 : 0) : undefined,
447
+ setMaterialDepthWrite: module._setMaterialDepthWrite ? (id: number, on: boolean) => module._setMaterialDepthWrite(id, on ? 1 : 0) : undefined,
448
+ setMaterialCulling: module._setMaterialCulling,
449
+ setMaterialStencil: module._setMaterialStencil
450
+ ? (id: number, write: boolean, test: number, ref: number, onPass: number, onFail: number, onDepthFail: number, readMask: number, writeMask: number) =>
451
+ module._setMaterialStencil(id, write ? 1 : 0, test, ref, onPass, onFail, onDepthFail, readMask, writeMask)
452
+ : undefined,
453
+ setSceneStencil: module._setSceneStencil ? (sceneId: number, on: boolean) => module._setSceneStencil(sceneId, on ? 1 : 0) : undefined,
443
454
  setGlbCulling: module._setGlbCulling,
444
455
  // Culling default for animated GLBs (Model.load): only once the wasm carries the export — an older
445
456
  // binary leaves the SDK on the always-draw default (see bridges.d.ts).
@@ -615,7 +626,7 @@ export const createViewer = async (canvas: HTMLCanvasElement, options: ViewerOpt
615
626
  createMaterial: (systemId: number) => {
616
627
  return module._createMaterialInstance(compileMaterial(systemId))
617
628
  },
618
- setMesh(entityId: number, materialId: number, vertices: Float32Array, normals: Float32Array, indices: Uint16Array, uv: Float32Array, meshType: number, uv1?: Float32Array) {
629
+ setMesh(entityId: number, materialId: number, vertices: Float32Array, normals: Float32Array, indices: Uint16Array, uv: Float32Array, meshType: number, uv1?: Float32Array, colors?: Uint8Array) {
619
630
  const ptrVertices = module._malloc(vertices.byteLength)
620
631
  const ptrNormals = module._malloc(normals.byteLength)
621
632
  const ptrIndices = module._malloc(indices.byteLength)
@@ -625,6 +636,15 @@ export const createViewer = async (canvas: HTMLCanvasElement, options: ViewerOpt
625
636
  module.HEAPU16.set(indices, ptrIndices/2)
626
637
  module.HEAPF32.set(uv, ptrUv/4)
627
638
 
639
+ // the vertex colours ride the newest export (createMeshC); a wasm without it draws white
640
+ if (colors && module._createMeshC) {
641
+ const ptrUv1 = uv1 ? module._malloc(uv1.byteLength) : 0
642
+ if (uv1 && ptrUv1) module.HEAPF32.set(uv1, ptrUv1 / 4)
643
+ const ptrColors = module._malloc(colors.byteLength)
644
+ module.HEAPU8.set(colors, ptrColors)
645
+ module._createMeshC(entityId, materialId, ptrVertices, ptrNormals, ptrIndices, ptrUv, vertices.length / 3, indices.length, meshType, ptrUv1, ptrColors)
646
+ return
647
+ }
628
648
  // the lightmap UV set rides the newer export; a wasm without it duplicates uv into UV1
629
649
  if (uv1 && module._createMeshEx) {
630
650
  const ptrUv1 = module._malloc(uv1.byteLength)
@@ -832,7 +852,11 @@ export const createViewer = async (canvas: HTMLCanvasElement, options: ViewerOpt
832
852
  return out
833
853
  },
834
854
  getGlbClipSet: (entityId: number) => module._getGlbClipSet(entityId),
835
- sliceClip: (clipSetId: number, clip: number, start: number, end: number) => module._sliceClip(clipSetId, clip, start, end),
855
+ deriveClip: (clipSetId: number, clip: number, mirror: boolean, start: number, end: number) => {
856
+ if (typeof module._deriveClip === "function") return module._deriveClip(clipSetId, clip, mirror ? 1 : 0, start, end)
857
+ if (mirror) { console.warn("creator-gl: this wasm cannot mirror clips — rebuild it"); return 0 } // a wasm from before 2026-09-06
858
+ return module._sliceClip(clipSetId, clip, start < 0 ? 0 : start, end < 0 ? 1e9 : end)
859
+ },
836
860
  destroyClipSet: (clipSetId: number) => module._destroyClipSet(clipSetId),
837
861
  animatorCreate: (entityId: number) => module._animatorCreate(entityId),
838
862
  animatorDestroy: (animatorId: number) => module._animatorDestroy(animatorId),
File without changes
Binary file
Binary file
File without changes