lecodes-viewer 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/README.md +5 -0
  2. package/dist-embed/VERSION.json +5 -0
  3. package/dist-embed/assets/Roboto-Variable-DHm9jpd-.woff2 +0 -0
  4. package/dist-embed/assets/__vite-browser-external-BIHI7g3E.js +1 -0
  5. package/dist-embed/assets/basis-C64VHDVD.js +1 -0
  6. package/dist-embed/assets/basis_transcoder-VXdx5NbI.wasm +0 -0
  7. package/dist-embed/assets/createViewer2d-D12Wy3VF.js +1 -0
  8. package/dist-embed/assets/createViewerLite-CP-WHGLg.js +707 -0
  9. package/dist-embed/assets/creator-gl-BF2CQzeI.wasm +0 -0
  10. package/dist-embed/assets/creator-gl.js-B_0GrQRe.symbols +5379 -0
  11. package/dist-embed/assets/creator-ui-BX5Fvnbt.js +1 -0
  12. package/dist-embed/assets/creator-ui-CzKHLH2Q.wasm +0 -0
  13. package/dist-embed/assets/creator2d-BtQ-1_B0.wasm +0 -0
  14. package/dist-embed/assets/draco-BiISTFcR.js +118 -0
  15. package/dist-embed/assets/draco_decoder-DsQ12WqX.wasm +0 -0
  16. package/dist-embed/assets/host-CWKB1WDd.js +162 -0
  17. package/dist-embed/assets/index-CIK8PnUA.js +2 -0
  18. package/dist-embed/assets/lit-DUE9rhY9.filamat +0 -0
  19. package/dist-embed/assets/loadCreatorUiNode-UAdhjRTd.js +1 -0
  20. package/dist-embed/assets/neutral_ibl128-D-lzdkLN.ktx +0 -0
  21. package/dist-embed/assets/particles-DxYDmI1O.filamat +0 -0
  22. package/dist-embed/assets/particles-quad-DwYo9yCK.filamat +0 -0
  23. package/dist-embed/assets/shadow-C9xJpqlY.filamat +0 -0
  24. package/dist-embed/assets/uberarchive-uXjRYa9p.bin +0 -0
  25. package/dist-embed/assets/unlit-pe4tFY5b.filamat +0 -0
  26. package/dist-embed/assets/unlit-transparent-D9OyaOBR.filamat +0 -0
  27. package/dist-embed/assets/video-eedvhMvS.filamat +0 -0
  28. package/dist-embed/assets/worker-Caf-yYEI.js +2 -0
  29. package/dist-embed/embed.html +48 -0
  30. package/dist-embed/embed.js +187 -0
  31. package/embed.html +48 -0
  32. package/package.json +35 -0
  33. package/src/assets/banner.png +0 -0
  34. package/src/assets/image2.png +0 -0
  35. package/src/assets/landing.mp4 +0 -0
  36. package/src/assets/neutral_ibl128.ktx +0 -0
  37. package/src/assets/neutral_ibl256.ktx +0 -0
  38. package/src/assets/output.mp4 +0 -0
  39. package/src/assets/output2.mp4 +0 -0
  40. package/src/assets/texture.png +0 -0
  41. package/src/assets/vue.svg +1 -0
  42. package/src/canvas/createCanvasHost.ts +2 -0
  43. package/src/createViewer.ts +103 -0
  44. package/src/embed.ts +515 -0
  45. package/src/host.ts +428 -0
  46. package/src/lite-test.ts +160 -0
  47. package/src/main.ts +77 -0
  48. package/src/sourcemapError.ts +2 -0
  49. package/src/ui/CuiProps.gen.ts +103 -0
  50. package/src/ui/UIScreen.ts +486 -0
  51. package/src/ui/createBareUI.ts +32 -0
  52. package/src/ui/createMeasureSystem.ts +220 -0
  53. package/src/ui/createNodeElement.ts +77 -0
  54. package/src/ui/createPagerSystem.ts +345 -0
  55. package/src/ui/createTouchSystem.ts +2 -0
  56. package/src/ui/createUI.ts +806 -0
  57. package/src/ui/creator-ui.js +14 -0
  58. package/src/ui/creator-ui.wasm +0 -0
  59. package/src/ui/defaultStyle.css +176 -0
  60. package/src/ui/layers.ts +9 -0
  61. package/src/ui/mountCanvasUI.ts +213 -0
  62. package/src/ui/nativeViewSystem.ts +91 -0
  63. package/src/ui/presentPlayer.ts +156 -0
  64. package/src/ui/props-apply.ts +403 -0
  65. package/src/ui/router.ts +2 -0
  66. package/src/ui/safeAreaSpec.ts +3 -0
  67. package/src/ui/shells.ts +37 -0
  68. package/src/ui/toasts.ts +2 -0
  69. package/src/ui/transitions.ts +250 -0
  70. package/src/ui/ui.d.ts +78 -0
  71. package/src/ui/vListSystem.ts +212 -0
  72. package/src/ui/viewScale.ts +2 -0
  73. package/src/utils/animate.ts +2 -0
  74. package/src/utils/createUtils.ts +2 -0
  75. package/src/utils/fetch.ts +3 -0
  76. package/src/utils/fonts.ts +2 -0
  77. package/src/utils/globals.ts +2 -0
  78. package/src/utils/imageNode.ts +260 -0
  79. package/src/utils/openFilePicker.ts +2 -0
  80. package/src/utils/svg.ts +2 -0
  81. package/src/utils/videoNode.ts +2 -0
  82. package/src/utils/ws.ts +2 -0
  83. package/src/viewer/createViewer.ts +993 -0
  84. package/src/viewer/creator-gl.js +14 -0
  85. package/src/viewer/creator-gl.js.symbols +5379 -0
  86. package/src/viewer/creator-gl.wasm +0 -0
  87. package/src/viewer/lit.filamat +0 -0
  88. package/src/viewer/mediaplayer.ts +3 -0
  89. package/src/viewer/particles-quad.filamat +0 -0
  90. package/src/viewer/particles.filamat +0 -0
  91. package/src/viewer/physics.ts +140 -0
  92. package/src/viewer/shadow.filamat +0 -0
  93. package/src/viewer/uberarchive.bin +0 -0
  94. package/src/viewer/unlit-transparent.filamat +0 -0
  95. package/src/viewer/unlit.filamat +0 -0
  96. package/src/viewer/validateGlb.ts +262 -0
  97. package/src/viewer/video.filamat +0 -0
  98. package/src/viewer/wasmCrashSymbolizer.ts +133 -0
  99. package/src/viewer2d/createViewer2d.ts +438 -0
  100. package/src/viewer2d/creator2d.js +14 -0
  101. package/src/viewer2d/creator2d.wasm +0 -0
  102. package/src/viewer2d/sprite2d.ts +80 -0
  103. package/src/vite-env.d.ts +1 -0
@@ -0,0 +1,250 @@
1
+ import { ANIMATE_LAYER_BASE } from "./layers"
2
+
3
+ export const createTransitionSystem = (
4
+ module: any,
5
+ styleBlock: HTMLStyleElement,
6
+ getNode: (screenId: number, nodeId: number) => HTMLElement | null | undefined,
7
+ applyStyle: Function,
8
+ getOverlay: (yogaId: number) => HTMLElement | null | undefined
9
+ ) => {
10
+
11
+ const clearTransition = (e: TransitionEvent) => {
12
+ (e.target as HTMLElement).style.transition = ""
13
+ }
14
+
15
+ const applyStyleTransition = (yogaId: number, value: number, layer: number, forceTransition = false, timeoutTransition = false) => {
16
+
17
+ const keys = []
18
+
19
+ module._resetNodeLayer(yogaId, layer)
20
+
21
+ let duration: number | null = null
22
+ let delay: number | null = null
23
+ for (let [ key, _value ] of Object.entries(value)) {
24
+ if (key === "duration") {
25
+ duration = _value
26
+ continue
27
+ }
28
+ if (key === "delay"){
29
+ delay = _value
30
+ continue
31
+ }
32
+ if (META_KEYS.has(key)) continue // options-bag keys are not style targets
33
+ keys.push(key)
34
+ applyStyle(layer, yogaId, key, _value as any)
35
+ }
36
+
37
+ const node = getNode(module._getScreenId(yogaId), yogaId)
38
+ if ((forceTransition || duration!== null || delay !== null) && node) {
39
+
40
+ const buildStr = (props: string[]) => delay !== null?
41
+ props.map(k => `${cssAliases[k] ?? k} ${delay}ms ${duration ?? 225}ms ease-in-out`).join(","):
42
+ props.map(k => `${cssAliases[k] ?? k} ${duration ?? 225}ms ease-in-out`).join(",")
43
+
44
+ // overlayColor lives on a separate overlay element behind the widget, so its
45
+ // transition must target that element (animating its background-color), not the
46
+ // node. Keep it out of the node's transition list.
47
+ const nodeKeys = keys.filter(k => k !== "overlayColor")
48
+ const str = buildStr(nodeKeys)
49
+
50
+ const overlay = keys.includes("overlayColor") ? getOverlay(yogaId) : null
51
+ const overlayStr = overlay ? buildStr(["overlayColor"]) : ""
52
+
53
+ if (timeoutTransition) {
54
+ requestAnimationFrame(() => {
55
+ node.style.transition = str
56
+ if (overlay) overlay.style.transition = overlayStr
57
+ })
58
+ } else {
59
+ if (forceTransition) {
60
+ node.style.transition = str
61
+ if (overlay) overlay.style.transition = overlayStr
62
+ } else {
63
+ const className = "action_" + yogaId + "_" + layer
64
+ styleBlock.sheet?.insertRule(`.${className} { transition: ${str}; }`, styleBlock.sheet.cssRules.length)
65
+ node.classList.add(className)
66
+ if (overlay) overlay.style.transition = overlayStr
67
+ }
68
+ }
69
+ }
70
+
71
+ if (forceTransition) {
72
+ node?.addEventListener("transitionend", clearTransition)
73
+ if (keys.includes("overlayColor")) {
74
+ getOverlay(yogaId)?.addEventListener("transitionend", clearTransition)
75
+ }
76
+ }
77
+ }
78
+
79
+ // ===== looping animateTo (loop: / loopMode:) =====
80
+ // CSS transitions can't repeat, so a loop is a timer-chained sequence of layer
81
+ // enables/disables over the same staged values: ping-pong animates there and back each
82
+ // cycle, restart snaps back and replays forward. Each phase re-checks the node is still
83
+ // alive and attached (removal frees the yoga node; router history detaches the element)
84
+ // and stops silently otherwise — matching iOS, where Core Animation drops the repeating
85
+ // animations when the view leaves the window. A loop never merges the layer: it is an
86
+ // effect, not a state change, and the node ends back at its base style.
87
+
88
+ type Loop = { screenId: number, layer: number, timer: ReturnType<typeof setTimeout> | undefined }
89
+ const loops = new Map<number, Loop>()
90
+
91
+ /// Stop the node's loop, if any, and return it to base. Callers guarantee the node is alive.
92
+ const cancelLoop = (nodeId: number) => {
93
+ const loop = loops.get(nodeId)
94
+ if (!loop) return
95
+ loops.delete(nodeId)
96
+ if (loop.timer !== undefined) clearTimeout(loop.timer)
97
+ const el = getNode(loop.screenId, nodeId)
98
+ if (el) el.style.transition = ""
99
+ module._disableNodeLayer(nodeId, loop.layer)
100
+ module._resetNodeLayer(nodeId, loop.layer)
101
+ }
102
+
103
+ const startLoop = (nodeId: number, style: any, layer: number) => {
104
+ const screenId = module._getScreenId(nodeId)
105
+ const first = getNode(screenId, nodeId)
106
+ if (!first) return
107
+
108
+ const duration: number = style.duration ?? 225
109
+ const delay: number = style.delay ?? 0
110
+ const pingPong = style.loopMode !== "restart"
111
+ let remaining: number = style.loop === true ? Infinity : +style.loop
112
+
113
+ // Stage the target values once — enables/disables reuse them every cycle.
114
+ module._resetNodeLayer(nodeId, layer)
115
+ const keys: string[] = []
116
+ for (const [key, v] of Object.entries(style)) {
117
+ if (META_KEYS.has(key)) continue
118
+ keys.push(key)
119
+ applyStyle(layer, nodeId, key, v as any)
120
+ }
121
+ const str = keys.filter(k => k !== "overlayColor")
122
+ .map(k => `${cssAliases[k] ?? k} ${duration}ms ease-in-out`).join(",")
123
+ const hasOverlay = keys.includes("overlayColor")
124
+
125
+ const loop: Loop = { screenId, layer, timer: undefined }
126
+ loops.set(nodeId, loop)
127
+
128
+ const setTransition = (el: HTMLElement, on: boolean) => {
129
+ el.style.transition = on ? str : ""
130
+ if (hasOverlay) {
131
+ const o = getOverlay(nodeId)
132
+ if (o) o.style.transition = on ? `background-color ${duration}ms ease-in-out` : ""
133
+ }
134
+ }
135
+
136
+ // null = the loop no longer runs. Detached-but-alive (history screen) also restores base,
137
+ // so the node shows its resting style if the screen is later revived.
138
+ const alive = (): HTMLElement | null => {
139
+ if (loops.get(nodeId) !== loop) return null
140
+ const el = getNode(screenId, nodeId)
141
+ if (el && el.isConnected) return el
142
+ loops.delete(nodeId)
143
+ if (loop.timer !== undefined) clearTimeout(loop.timer)
144
+ if (el) {
145
+ el.style.transition = ""
146
+ module._disableNodeLayer(nodeId, layer)
147
+ module._resetNodeLayer(nodeId, layer)
148
+ }
149
+ return null
150
+ }
151
+
152
+ const finish = (el: HTMLElement) => {
153
+ loops.delete(nodeId)
154
+ setTransition(el, false)
155
+ module._resetNodeLayer(nodeId, layer) // layer already disabled — node rests at base
156
+ }
157
+
158
+ // ping-pong: forward(d) → back(d) → count a cycle → again | finish
159
+ const phaseBack = () => {
160
+ const el = alive()
161
+ if (!el) return
162
+ module._disableNodeLayer(nodeId, layer) // transition still set → animates back
163
+ loop.timer = setTimeout(phaseCycleEnd, duration)
164
+ }
165
+ const phaseCycleEnd = () => {
166
+ const el = alive()
167
+ if (!el) return
168
+ remaining -= 1
169
+ if (remaining > 0) {
170
+ module._enableNodeLayer(nodeId, layer) // forward again
171
+ loop.timer = setTimeout(phaseBack, duration)
172
+ } else {
173
+ finish(el)
174
+ }
175
+ }
176
+
177
+ // restart: forward(d) → snap to base → again | finish
178
+ const phaseSnap = () => {
179
+ const el = alive()
180
+ if (!el) return
181
+ remaining -= 1
182
+ setTransition(el, false)
183
+ module._disableNodeLayer(nodeId, layer) // no transition → instant snap
184
+ if (remaining <= 0) {
185
+ finish(el)
186
+ return
187
+ }
188
+ requestAnimationFrame(() => { // let the snap land before re-arming
189
+ const el = alive()
190
+ if (!el) return
191
+ setTransition(el, true)
192
+ requestAnimationFrame(() => module._enableNodeLayer(nodeId, layer))
193
+ loop.timer = setTimeout(phaseSnap, duration)
194
+ })
195
+ }
196
+
197
+ // first forward pass (the only one that honors `delay`)
198
+ const phaseStart = () => {
199
+ const el = alive()
200
+ if (!el) return
201
+ setTransition(el, true)
202
+ requestAnimationFrame(() => module._enableNodeLayer(nodeId, layer))
203
+ loop.timer = setTimeout(pingPong ? phaseBack : phaseSnap, duration)
204
+ }
205
+ if (delay > 0) loop.timer = setTimeout(phaseStart, delay)
206
+ else phaseStart()
207
+ }
208
+
209
+ const animateTo = (nodeId: number, style: any) => {
210
+ const layer = (style.layer ?? 44) + ANIMATE_LAYER_BASE
211
+ cancelLoop(nodeId)
212
+ if (style.loop) {
213
+ startLoop(nodeId, style, layer)
214
+ return
215
+ }
216
+ applyStyleTransition(nodeId, style, layer, true)
217
+ requestAnimationFrame(() => {
218
+ module._enableNodeLayer(nodeId, layer)
219
+ // module._mergeNodeLayer(nodeId, 302, 256)
220
+ })
221
+ }
222
+
223
+ const animateFrom = (nodeId: number, style: any) => {
224
+ const layer = (style.layer ?? 44) + ANIMATE_LAYER_BASE
225
+ cancelLoop(nodeId)
226
+ module._enableNodeLayer(nodeId, layer)
227
+ applyStyleTransition(nodeId, style, layer, true, true)
228
+ requestAnimationFrame(() => {
229
+ module._disableNodeLayer(nodeId, layer)
230
+ })
231
+ }
232
+
233
+
234
+ return {
235
+ applyStyleTransition,
236
+ clearTransition,
237
+ animateTo,
238
+ animateFrom
239
+ }
240
+ }
241
+
242
+ // Options-bag meta keys of animateTo/animateFrom — everything that is not a style target.
243
+ const META_KEYS = new Set(["duration", "delay", "layer", "commit", "loop", "loopMode"])
244
+
245
+ const cssAliases: Record<string, string> = {
246
+ bgColor: "background-color",
247
+ backgroundColor: "background-color",
248
+ borderColor: "border-color",
249
+ overlayColor: "background-color"
250
+ }
package/src/ui/ui.d.ts ADDED
@@ -0,0 +1,78 @@
1
+ declare module "ui" {
2
+ type ContainerStyle = {
3
+ flexDirection?: "row" | "column";
4
+ justifyContent?: "flex-start" | "center" | "flex-end" | "space-between" | "space-evenly";
5
+ alignItems?: "flex-start" | "center" | "flex-end" | "stretch";
6
+ gap?: number;
7
+ backgroundColor?: number;
8
+ };
9
+
10
+ type ElementStyle = {
11
+ width?: number | string;
12
+ height?: number | string;
13
+ margin?: number[];
14
+ padding?: number[];
15
+ };
16
+
17
+ type Listener = () => void;
18
+
19
+ export function UIButton(children: UINode[]): UIButton;
20
+
21
+ export function UIButton(style: UIButton["style"], children: UINode[]): UIButton;
22
+
23
+ export type UIButton = {
24
+ readonly type: "button";
25
+ readonly listeners: Listener[];
26
+ style: ElementStyle & ContainerStyle;
27
+ onClick(callback: Listener): UIButton;
28
+ readonly children: UINode[];
29
+ };
30
+
31
+ type UINode = {
32
+ readonly type: string;
33
+ style: any;
34
+ };
35
+
36
+ export function UIScreen(children: UINode[]): UIScreen;
37
+
38
+ export function UIScreen(style: UIScreen["style"], children: UINode[]): UIScreen;
39
+
40
+ export type UIScreen = {
41
+ readonly type: "screen";
42
+ style: ContainerStyle & {
43
+ padding?: number[];
44
+ };
45
+ open: () => void;
46
+ readonly children: UINode[];
47
+ };
48
+
49
+ export function UISpacer(): UISpacer;
50
+
51
+ export function UISpacer(style: UISpacer["style"]): UISpacer;
52
+
53
+ export type UISpacer = {
54
+ readonly type: "spacer";
55
+ style: {
56
+ flexGrow?: number;
57
+ };
58
+ };
59
+
60
+ export function UIText(text: string): UIText;
61
+
62
+ export function UIText(style: UIText["style"], text: string): UIText;
63
+
64
+ export type UIText = {
65
+ readonly type: "text";
66
+ text: string;
67
+ style: ElementStyle & {
68
+ textAlign?: "start" | "center" | "end";
69
+ fontSize?: number;
70
+ fontWeight?: number;
71
+ font?: string;
72
+ textColor?: string;
73
+ };
74
+ };
75
+
76
+
77
+
78
+ }
@@ -0,0 +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 }
212
+ }
@@ -0,0 +1,2 @@
1
+ // Moved to viewer-lite; re-export shim (one definition, shared by viewer + viewer-lite).
2
+ export * from "lecodes-viewer-lite/src/ui/viewScale"
@@ -0,0 +1,2 @@
1
+ // Moved to viewer-lite; re-export shim.
2
+ export * from "lecodes-viewer-lite/src/utils/animate"
@@ -0,0 +1,2 @@
1
+ // Moved to viewer-lite; re-export shim.
2
+ export * from "lecodes-viewer-lite/src/utils/createUtils"
@@ -0,0 +1,3 @@
1
+ // Moved to the viewer-lite package (the minimal web host); re-exported so viewer-internal
2
+ // imports keep working and both packages share ONE buffer registry instance.
3
+ export * from "lecodes-viewer-lite/src/utils/fetch"
@@ -0,0 +1,2 @@
1
+ // Moved to viewer-lite; re-export shim.
2
+ export * from "lecodes-viewer-lite/src/utils/fonts"
@@ -0,0 +1,2 @@
1
+ // Moved to viewer-lite; re-export shim.
2
+ export * from "lecodes-viewer-lite/src/utils/globals"