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,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
|
+
}
|