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,103 @@
1
+ // AUTO-GENERATED from packages/creator-ui/properties.mjs — DO NOT EDIT. Regenerate: bun run gen:ui-props
2
+ //
3
+ // The per-key host property surface of the creator-ui core (docs/ui-property-registry-plan.md):
4
+ // wire channel + host-reset marking for every add*Handler key. Hosts implement a full Record
5
+ // over CuiHostPropKey — the compiler enforces that every property is either implemented or
6
+ // explicitly "unsupported", and that reset handlers exist exactly where the registry says.
7
+ // transform (matrix) and backgroundGradient (struct) use setMatrixHandler/setGradientHandler.
8
+
9
+ export type CuiChannel = "float" | "uint" | "string"
10
+
11
+ export interface CuiHostPropMeta {
12
+ readonly channel: CuiChannel
13
+ /** On the host ResetHandler surface (presence / theme-fallback props): a reset
14
+ * implementation is REQUIRED (or the whole prop marked "unsupported"). All other props
15
+ * reset through the normal set channel and must NOT register a reset handler. */
16
+ readonly hostReset: boolean
17
+ /** Node-type applicability; a dispatch to any other node type is a defined no-op. */
18
+ readonly nodes: "all" | readonly string[]
19
+ }
20
+
21
+ export const CUI_HOST_PROPS = {
22
+ // ---- layout ----
23
+ display: { channel: "string", hostReset: false, nodes: "all" },
24
+ overflow: { channel: "string", hostReset: false, nodes: "all" },
25
+ // ---- scroll ----
26
+ scrollDirection: { channel: "float", hostReset: false, nodes: ["scrollable"] },
27
+ // ---- border ----
28
+ borderWidth: { channel: "float", hostReset: false, nodes: "all" },
29
+ borderTopWidth: { channel: "float", hostReset: false, nodes: "all" },
30
+ borderRightWidth: { channel: "float", hostReset: false, nodes: "all" },
31
+ borderBottomWidth: { channel: "float", hostReset: false, nodes: "all" },
32
+ borderLeftWidth: { channel: "float", hostReset: false, nodes: "all" },
33
+ borderHorizontalWidth: { channel: "float", hostReset: false, nodes: "all" },
34
+ borderVerticalWidth: { channel: "float", hostReset: false, nodes: "all" },
35
+ borderStartWidth: { channel: "float", hostReset: false, nodes: "all" },
36
+ borderEndWidth: { channel: "float", hostReset: false, nodes: "all" },
37
+ // ---- text ----
38
+ color: { channel: "uint", hostReset: true, nodes: "all" },
39
+ // ---- box ----
40
+ backgroundColor: { channel: "uint", hostReset: false, nodes: "all" },
41
+ // ---- border ----
42
+ borderColor: { channel: "uint", hostReset: false, nodes: "all" },
43
+ borderTopColor: { channel: "uint", hostReset: false, nodes: "all" },
44
+ borderRightColor: { channel: "uint", hostReset: false, nodes: "all" },
45
+ borderBottomColor: { channel: "uint", hostReset: false, nodes: "all" },
46
+ borderLeftColor: { channel: "uint", hostReset: false, nodes: "all" },
47
+ // ---- input ----
48
+ placeholderColor: { channel: "uint", hostReset: true, nodes: ["input"] },
49
+ // ---- scroll ----
50
+ refreshControlColor: { channel: "uint", hostReset: true, nodes: ["scrollable"] },
51
+ // ---- image ----
52
+ tintColor: { channel: "uint", hostReset: true, nodes: ["image"] },
53
+ // ---- box ----
54
+ rippleColor: { channel: "uint", hostReset: false, nodes: ["button"] },
55
+ // ---- widget ----
56
+ overlayColor: { channel: "uint", hostReset: true, nodes: ["widget"] },
57
+ // ---- box ----
58
+ backgroundImage: { channel: "string", hostReset: false, nodes: "all" },
59
+ backgroundSize: { channel: "string", hostReset: false, nodes: "all" },
60
+ backgroundGradientStr: { channel: "string", hostReset: false, nodes: "all" },
61
+ // ---- border ----
62
+ borderRadius: { channel: "float", hostReset: false, nodes: "all" },
63
+ borderTopLeftRadius: { channel: "float", hostReset: false, nodes: "all" },
64
+ borderTopRightRadius: { channel: "float", hostReset: false, nodes: "all" },
65
+ borderBottomRightRadius: { channel: "float", hostReset: false, nodes: "all" },
66
+ borderBottomLeftRadius: { channel: "float", hostReset: false, nodes: "all" },
67
+ // ---- effects ----
68
+ opacity: { channel: "float", hostReset: false, nodes: "all" },
69
+ pointerEvents: { channel: "string", hostReset: false, nodes: "all" },
70
+ // ---- text ----
71
+ fontSize: { channel: "float", hostReset: false, nodes: ["text", "input"] },
72
+ fontWeight: { channel: "float", hostReset: false, nodes: ["text", "input"] },
73
+ fontStyle: { channel: "float", hostReset: false, nodes: ["text", "input"] },
74
+ fontFamily: { channel: "string", hostReset: true, nodes: ["text", "input"] },
75
+ lineHeight: { channel: "float", hostReset: false, nodes: ["text", "input"] },
76
+ letterSpacing: { channel: "float", hostReset: false, nodes: ["text", "input"] },
77
+ textAlign: { channel: "string", hostReset: false, nodes: ["text", "input"] },
78
+ textDecoration: { channel: "string", hostReset: false, nodes: ["text"] },
79
+ lineClamp: { channel: "float", hostReset: false, nodes: ["text"] },
80
+ textOverflow: { channel: "string", hostReset: false, nodes: ["text"] },
81
+ // ---- image ----
82
+ objectFit: { channel: "string", hostReset: false, nodes: ["image", "video"] },
83
+ // ---- input ----
84
+ placeholder: { channel: "string", hostReset: false, nodes: ["input"] },
85
+ type: { channel: "string", hostReset: false, nodes: ["input"] },
86
+ enterKey: { channel: "string", hostReset: false, nodes: ["input"] },
87
+ autocapitalize: { channel: "string", hostReset: false, nodes: ["input"] },
88
+ autocorrect: { channel: "float", hostReset: false, nodes: ["input"] },
89
+ maxLength: { channel: "float", hostReset: false, nodes: ["input"] },
90
+ keyboardShrink: { channel: "float", hostReset: false, nodes: ["input"] },
91
+ keyboardDismiss: { channel: "float", hostReset: false, nodes: ["input"] },
92
+ // ---- scroll ----
93
+ showScrollbar: { channel: "float", hostReset: false, nodes: ["scrollable"] },
94
+ overscrollMode: { channel: "string", hostReset: false, nodes: ["scrollable"] },
95
+ keyboardDismissMode: { channel: "string", hostReset: false, nodes: ["scrollable", "vlist"] },
96
+ snap: { channel: "string", hostReset: false, nodes: ["scrollable"] },
97
+ // ---- widget ----
98
+ sheetDetents: { channel: "string", hostReset: false, nodes: ["widget"] },
99
+ sheetDetent: { channel: "float", hostReset: false, nodes: ["widget"] },
100
+ sheetDismissible: { channel: "float", hostReset: false, nodes: ["widget"] },
101
+ } as const satisfies Record<string, CuiHostPropMeta>
102
+
103
+ export type CuiHostPropKey = keyof typeof CUI_HOST_PROPS
@@ -0,0 +1,486 @@
1
+ import { UINode } from "ui"
2
+ import { TouchSystem } from "./createTouchSystem"
3
+ import { createNodeElement } from "./createNodeElement"
4
+ import {
5
+ configureImage,
6
+ setImageSourceRect,
7
+ tintSvgImage,
8
+ untintSvgImage,
9
+ updateImageSrc,
10
+ refreshCanvasSurface,
11
+ } from "../utils/imageNode"
12
+ import { configureVideo } from "../utils/videoNode"
13
+ import { ImageSrc } from "../utils/svg"
14
+ import { createVListSystem } from "./vListSystem"
15
+ import type { PagerSystem } from "./createPagerSystem"
16
+ import type { NativeViewSystem } from "./nativeViewSystem"
17
+ const LAYER_DEFAULT = 256
18
+
19
+ export type ApplyStyleCallback = (
20
+ layer: number,
21
+ yogaId: number,
22
+ key: string,
23
+ value: string | number,
24
+ ) => void
25
+
26
+ export class UIScreen {
27
+
28
+ module: any
29
+ touchSystem: TouchSystem
30
+ applyStyle: ApplyStyleCallback
31
+ /** Resolve a user style class ("$"+name) to its layer id (shares createUI's interned-key cache). */
32
+ classLayer: (nodeId: number, name: string) => number
33
+ deregister: (screenId: number) => void
34
+ /** Resolve a baked `_creatorCanvas` surface id to a data-uri, for UIImage(canvas). */
35
+ canvasHost?: { surfaceToDataUrl(surfaceId: number): string | null }
36
+
37
+ layoutPtrBytes: number
38
+ layoutPtr: number
39
+ paddingPtrBytes: number
40
+ paddingPtr: number
41
+
42
+ vlistSystem: ReturnType<typeof createVListSystem>
43
+ pagerSystem: PagerSystem
44
+ nativeViewSystem?: NativeViewSystem
45
+
46
+ // Set on widget roots by showWidget(widget, owner): the destination descriptor this widget is
47
+ // attached to. createUI toggles the widget's visibility to track the owner's presentation.
48
+ ownerDesc?: any
49
+
50
+ constructor(module: any, touchSystem: TouchSystem, applyStyle: ApplyStyleCallback, classLayer: (nodeId: number, name: string) => number, vlistSystem: ReturnType<typeof createVListSystem>, pagerSystem: PagerSystem, deregister: (screenId: number) => void, canvasHost?: { surfaceToDataUrl(surfaceId: number): string | null }, nativeViewSystem?: NativeViewSystem) {
51
+ this.module = module
52
+ this.touchSystem = touchSystem
53
+ this.applyStyle = applyStyle
54
+ this.classLayer = classLayer
55
+ this.deregister = deregister
56
+ this.canvasHost = canvasHost
57
+
58
+ this.layoutPtrBytes = module._malloc(4 * 4)
59
+ this.layoutPtr = this.layoutPtrBytes >> 2
60
+ this.paddingPtrBytes = module._malloc(4 * 4)
61
+ this.paddingPtr = this.paddingPtrBytes >> 2
62
+ this.vlistSystem = vlistSystem
63
+ this.pagerSystem = pagerSystem
64
+ this.nativeViewSystem = nativeViewSystem
65
+ }
66
+
67
+ nodeMap = new Map<number, HTMLElement>()
68
+ weakMap = new WeakMap<HTMLElement, number>()
69
+ nodeOnLayoutEventsMap = new Map<number, ((layout: any) => void)[]>()
70
+
71
+ root!: HTMLElement
72
+ rootNodePtr!: number
73
+ screenId!: number
74
+ node: any
75
+ isWidget = false
76
+
77
+ // Overlay layer behind a widget (overlayColor). Managed by createUI's overlayColor
78
+ // handlers; tracked here so teardown removes it even when the widget is hidden
79
+ // without an explicit overlayColor: null reset.
80
+ overlayElement: HTMLElement | null = null
81
+
82
+ private vlistIds: number[] = []
83
+ private pagerIds: number[] = []
84
+
85
+ init(screenId: number, node: any) {
86
+ this.node = node
87
+ this.screenId = screenId
88
+ this.root = this.addNodeRecursive(node)
89
+ this.rootNodePtr = node._id
90
+ }
91
+
92
+ // ===== node creation =====
93
+
94
+ private addNodeRecursive(node: any, parent: number | null = null, index = 0): HTMLElement {
95
+ // Editables are measured leaves too: an input is one intrinsic text line, a textarea's
96
+ // height follows its content (auto-grow) — see createMeasureSystem.
97
+ const hasMeasureFunc = node.type === "text" || node.type === "image"
98
+ || node.type === "input" || node.type === "textarea"
99
+ const yogaId = parent === null
100
+ ? this.module._createNodeRoot(this.screenId)
101
+ : this.module._createNode(parent, index, hasMeasureFunc)
102
+
103
+ const element = createNodeElement(node)
104
+ element.setAttribute("data-node-id", yogaId)
105
+ element.style.position = "absolute"
106
+ // Back-reference for the editor's element inspector: from a DOM hit straight to the SDK node
107
+ // (type, name, _style, and — when the host enabled capture — the _site creation stack).
108
+ ;(element as any)._lecodesNode = node
109
+
110
+ if (node._text) {
111
+ element.textContent = node._text
112
+ element.style.whiteSpace = "pre-wrap"
113
+ element.style.lineHeight = "1.2em"
114
+ element.style.fontSize = "14px"
115
+ element.toggleAttribute("data-using-padding")
116
+ }
117
+ if (node.type === "scrollable" || node.scrollable) {
118
+ element.toggleAttribute("data-using-scroll")
119
+ }
120
+ if (node.type === "image") {
121
+ configureImage(this, node, element)
122
+ }
123
+ if (node.type === "video") {
124
+ configureVideo(element)
125
+ }
126
+ this.attachListeners(node, element)
127
+ // Per-type default styles come from the SDK (DEFAULT_STYLES, merged into node._style at
128
+ // construction) — the host applies only what the node carries.
129
+
130
+ node._id = yogaId
131
+ this.weakMap.set(element, yogaId)
132
+ this.nodeMap.set(node._id, element)
133
+
134
+ for (const [key, value] of Object.entries(node._style)) {
135
+ this.applyStyle(LAYER_DEFAULT, yogaId, key, value as any)
136
+ }
137
+
138
+ // Re-apply style classes the SDK node carries (el.class writes persist them on the element so a
139
+ // detached node re-mounts with them active — screens re-assign _id on every open). No recalc
140
+ // here: the mount runs a full layout afterward.
141
+ if (node._classes) {
142
+ for (const name of node._classes) {
143
+ this.module._enableNodeLayer(yogaId, this.classLayer(yogaId, name))
144
+ }
145
+ }
146
+
147
+ // A value assigned before mount (a pre-filled form) is stashed on the SDK node as `_value` —
148
+ // apply it once every style layer is on the element, so `type` is settled first (a `date` field
149
+ // only keeps a canonical value once it IS a date field). No recalc: the mount lays out
150
+ // afterward, and editables are measured leaves — a pre-filled textarea comes up already grown.
151
+ if ((node.type === "input" || node.type === "textarea") && node._value) {
152
+ (element as HTMLInputElement | HTMLTextAreaElement).value = node._value
153
+ }
154
+
155
+ if (node.type === "vlist") {
156
+ element.toggleAttribute("data-using-scroll")
157
+ element.toggleAttribute("data-vlist")
158
+ element.style.overflowY = "auto" // если createNodeElement не делает это по типу
159
+ this.vlistSystem.register(this, node, element, yogaId)
160
+ this.vlistIds.push(yogaId)
161
+ }
162
+
163
+ // A NativeView embedded as a layout node: the yoga node sizes a cell; the registered web
164
+ // factory's live element mounts inside it (one live instance per NativeView — mounting here
165
+ // steals it from wherever it was, which is the promotion contract).
166
+ if (node.type === "native") {
167
+ element.toggleAttribute("data-native-view")
168
+ element.style.overflow = "hidden"
169
+ this.nativeViewSystem?.mountInto(node, element)
170
+ }
171
+
172
+ // A pager's pages (tab-major `children` partitioned by `_lens`) are separate layout roots
173
+ // built by the pager system, not yoga children — so it registers here and its children are
174
+ // skipped by the recursion below.
175
+ if (node.type === "pager") {
176
+ element.toggleAttribute("data-pager")
177
+ this.pagerSystem.register(this, node, element, yogaId)
178
+ this.pagerIds.push(yogaId)
179
+ }
180
+
181
+ if (node.ll) {
182
+ this.nodeOnLayoutEventsMap.set(yogaId, node.ll)
183
+ }
184
+
185
+ if ("children" in node && node.type !== "pager") {
186
+ let i = 0
187
+ for (const child of node.children as Array<UINode>) {
188
+ if (!child) continue
189
+ element.appendChild(this.addNodeRecursive(child, yogaId, i))
190
+ i++
191
+ }
192
+ }
193
+
194
+ return element
195
+ }
196
+
197
+ private attachListeners(node: any, element: HTMLElement) {
198
+ if (node.type === "button" || node.type === "widget" || node.type === "screen") {
199
+ element.addEventListener("pointerdown", this.touchSystem.onUIButtonTouchStart.bind(null, node))
200
+ if (node.type === "button") {
201
+ element.addEventListener("pointerup", this.touchSystem.onUIButtonTouchEnd.bind(null, node))
202
+ }
203
+ }
204
+
205
+ if (node.type === "input" || node.type === "textarea") {
206
+ element.toggleAttribute("data-using-padding")
207
+ const el = element as HTMLInputElement | HTMLTextAreaElement
208
+ ;(el as any)._lcNode = node // backref for the keyboard-dismiss guard (keyboardDismiss policy)
209
+ el.addEventListener("input", () => {
210
+ // Auto-grow: a content-measured textarea re-measures + relayouts as the user types.
211
+ if (node.type === "textarea") {
212
+ this.module._markNodeDirty(node._id)
213
+ this.recalc()
214
+ }
215
+ for (const listener of node.cl) listener(el.value)
216
+ })
217
+ el.addEventListener("focus", () => {
218
+ // Flips onFocused AND the cascading `$focused` class (creator-ui pairs them).
219
+ this.module._setNodeFocused(node._id, true)
220
+ for (const listener of node.fl) listener()
221
+ })
222
+ el.addEventListener("blur", () => {
223
+ this.module._setNodeFocused(node._id, false)
224
+ for (const listener of node.bl) listener()
225
+ })
226
+ }
227
+ }
228
+
229
+ // ===== images =====
230
+
231
+ updateImageSrc = (el: HTMLElement, src: ImageSrc) => updateImageSrc(this, el, src)
232
+ tintSvgImage = (nodeId: number, tint: string) => tintSvgImage(this, nodeId, tint)
233
+ untintSvgImage = (nodeId: number) => untintSvgImage(this, nodeId)
234
+ setSourceRect = (nodeId: number, x: number, y: number, w: number, h: number) =>
235
+ setImageSourceRect(this, nodeId, x, y, w, h)
236
+ refreshCanvasSurface = (surfaceId: number) => refreshCanvasSurface(this, surfaceId)
237
+
238
+ // ===== layout =====
239
+
240
+ screenWidth = 0
241
+ screenHeight = 0
242
+
243
+ // Dimensions for which the root (screen) onLayout was last fired. The screen's
244
+ // layout depends solely on screen dimensions, which only change in applyLayout —
245
+ // never during recalc(). Tracking them prevents re-firing onLayout on every node update.
246
+ private firedScreenWidth = -1
247
+ private firedScreenHeight = -1
248
+
249
+ // Set when a recalc was requested while this screen had no dimensions (detached).
250
+ // Forces the next applyLayout to re-lay-out even if the dimensions are unchanged, so
251
+ // updates made while the screen was in history are applied when it is shown again.
252
+ private pendingRecalc = false
253
+
254
+ applyLayout(screenWidth: number, screenHeight: number) {
255
+ if (screenWidth === this.screenWidth && screenHeight === this.screenHeight && !this.pendingRecalc) return
256
+ this.pendingRecalc = false
257
+ this.screenWidth = screenWidth
258
+ this.screenHeight = screenHeight
259
+ if (this.isWidget) {
260
+ this.module._calculateWidget(this.rootNodePtr, screenWidth, screenHeight)
261
+ } else {
262
+ this.module._calculate(this.rootNodePtr, screenWidth, screenHeight)
263
+ }
264
+ this.updateSizes(this.root)
265
+ }
266
+
267
+ private fireLayout(yogaId: number, rect: { left: number; top: number; width: number; height: number }) {
268
+ const onLayout = this.nodeOnLayoutEventsMap.get(yogaId)
269
+ if (!onLayout) return
270
+ for (const callback of onLayout) callback(rect)
271
+ }
272
+
273
+ private updateSizes(element: HTMLElement) {
274
+ if (element.style.display === "none") return
275
+
276
+ const yogaId = this.weakMap.get(element)
277
+ if (!yogaId) {
278
+ console.warn("Trying to update size for element without yogaId", element)
279
+ return
280
+ }
281
+
282
+ if (element === this.root && !this.isWidget) {
283
+ if (this.screenWidth !== this.firedScreenWidth || this.screenHeight !== this.firedScreenHeight) {
284
+ this.firedScreenWidth = this.screenWidth
285
+ this.firedScreenHeight = this.screenHeight
286
+ this.fireLayout(yogaId, { left: 0, top: 0, width: this.screenWidth, height: this.screenHeight })
287
+ }
288
+ } else if (this.module._getLayoutDOMPointer(yogaId, this.layoutPtrBytes)) {
289
+ const left = this.module.HEAPF32[this.layoutPtr]
290
+ const top = this.module.HEAPF32[this.layoutPtr + 1]
291
+ const width = this.module.HEAPF32[this.layoutPtr + 2]
292
+ const height = this.module.HEAPF32[this.layoutPtr + 3]
293
+
294
+ element.style.left = left + "px"
295
+ element.style.top = top + "px"
296
+ element.style.width = width + "px"
297
+ element.style.height = height + "px"
298
+ element.style.maxWidth = ""
299
+
300
+ this.fireLayout(yogaId, { left, top, width, height })
301
+ }
302
+
303
+ if (element.hasAttribute("data-using-padding")) {
304
+ this.module._getPaddingPointer(yogaId, this.paddingPtrBytes)
305
+ const left = this.module.HEAPF32[this.paddingPtr]
306
+ const top = this.module.HEAPF32[this.paddingPtr + 1]
307
+ const right = this.module.HEAPF32[this.paddingPtr + 2]
308
+ const bottom = this.module.HEAPF32[this.paddingPtr + 3]
309
+ element.style.padding = `${top}px ${right}px ${bottom}px ${left}px`
310
+ element.style.boxSizing = "border-box"
311
+ }
312
+
313
+ if (element.hasAttribute("data-using-scroll")) {
314
+ if (element.classList.contains("scrollable_horizontal")) {
315
+ const width = this.module._getContentWidth(yogaId)
316
+ element.style.setProperty("--scroll-width", width + "px")
317
+ element.style.setProperty("--scroll-height", "0px")
318
+ } else {
319
+ const height = this.module._getContentHeight(yogaId)
320
+ element.style.setProperty("--scroll-height", height + "px")
321
+ element.style.setProperty("--scroll-width", "0px")
322
+ }
323
+ }
324
+
325
+ if (element.hasAttribute("data-vlist")) {
326
+ this.vlistSystem.updateLayout(this.weakMap.get(element)!)
327
+ return // айтемы кладёт refresh() с top из vlistOffsetOf
328
+ }
329
+
330
+ if (element.hasAttribute("data-pager")) {
331
+ // Pager pages are separate roots the pager system lays out to this element's box — don't
332
+ // recurse into its wrappers (they aren't in this screen's yoga tree).
333
+ this.pagerSystem.updateLayout(this.weakMap.get(element)!)
334
+ return
335
+ }
336
+
337
+ for (const child of element.children) {
338
+ this.updateSizes(child as HTMLElement)
339
+ }
340
+ }
341
+
342
+ // Chain page-lifecycle activation to this screen's pagers: the current tab's top page is "open"
343
+ // only while this screen's chain is active (this screen is the current/visible one). Cascades
344
+ // through nested pagers (a tab screen that itself hosts a pager).
345
+ setPagersActive(active: boolean) {
346
+ for (const id of this.pagerIds) this.pagerSystem.instances.get(id)?.setActive(active)
347
+ }
348
+
349
+ // ===== recalc =====
350
+
351
+ scheduledRecalc = false
352
+
353
+ recalc() {
354
+ if (this.scheduledRecalc) return
355
+ this.scheduledRecalc = true
356
+ queueMicrotask(this.recalcImmediately)
357
+ }
358
+
359
+ recalcImmediately = () => {
360
+ this.scheduledRecalc = false
361
+ // A detached/never-laid-out screen has no dimensions to lay out against. Defer the
362
+ // recalc until it is shown (applyLayout will honor pendingRecalc). This is what was
363
+ // broken: a non-active screen used to recalc the GLOBAL cachedRoot (the active
364
+ // screen) instead of itself, so e.g. a .text change never re-measured this screen.
365
+ if (this.screenWidth === 0) {
366
+ this.pendingRecalc = true
367
+ return
368
+ }
369
+ // Always recalc this screen's OWN root (not the global cachedRoot), so updates to a
370
+ // screen that isn't the active one are still applied to that screen.
371
+ if (this.isWidget) {
372
+ this.module._calculateWidget(this.rootNodePtr, this.screenWidth, this.screenHeight)
373
+ } else {
374
+ this.module._calculate(this.rootNodePtr, this.screenWidth, this.screenHeight)
375
+ }
376
+ this.updateSizes(this.root)
377
+ }
378
+
379
+ // ===== tree mutation =====
380
+
381
+ insertNode = (index: number, nodeId: number, node: any) => {
382
+ const element = this.nodeMap.get(nodeId)
383
+ if (!element) return
384
+ element.appendChild(this.addNodeRecursive(node, nodeId, index))
385
+ this.recalc()
386
+ }
387
+
388
+ removeNode = (nodeId: number, node: any) => {
389
+ const parent = this.nodeMap.get(nodeId)
390
+ if (!parent) return
391
+ this.detachNode(parent, nodeId, node)
392
+ this.recalc()
393
+ }
394
+
395
+ setContent(nodeId: number, oldChildren: any[], newChildren: any[]) {
396
+ const parent = this.nodeMap.get(nodeId)
397
+ if (!parent) return
398
+
399
+ for (const node of oldChildren) {
400
+ this.detachNode(parent, nodeId, node)
401
+ }
402
+
403
+ let index = 0
404
+ for (const child of newChildren) {
405
+ if (!child) continue
406
+ parent.appendChild(this.addNodeRecursive(child, nodeId, index))
407
+ index++
408
+ }
409
+ this.recalc()
410
+ }
411
+
412
+ private detachNode(parentEl: HTMLElement, parentId: number, node: any) {
413
+ const el = this.nodeMap.get(node._id)
414
+ this.nodeMap.delete(node._id)
415
+ if (el) parentEl.removeChild(el)
416
+ this.module._removeNode(parentId, node._id)
417
+ this.setNodeIdNullRecursive(node)
418
+ }
419
+
420
+ private setNodeIdNullRecursive(node: any) {
421
+ node._id = 0
422
+ if ("children" in node) {
423
+ for (const child of node.children as Array<UINode>) {
424
+ if (!child) continue
425
+ this.setNodeIdNullRecursive(child)
426
+ }
427
+ }
428
+ }
429
+
430
+ // построить поддерево айтема как ребёнка detached yoga root
431
+ buildSubtree(subtree: any, itemRootPtr: number): HTMLElement {
432
+ return this.addNodeRecursive(subtree, itemRootPtr, 0)
433
+ }
434
+
435
+ // layout-проход по DOM-поддереву айтема (тот же updateSizes, но от произвольного корня)
436
+ updateSizesFor(element: HTMLElement) {
437
+ this.updateSizes(element)
438
+ }
439
+
440
+ // чистка при unmount: nodeMap + _id = 0
441
+ releaseSubtree(subtree: any) {
442
+ const drop = (n: any) => {
443
+ if (n._id) this.nodeMap.delete(n._id)
444
+ n._id = 0
445
+ if ("children" in n) for (const c of n.children ?? []) if (c) drop(c)
446
+ }
447
+ drop(subtree)
448
+ }
449
+
450
+ // ===== teardown =====
451
+
452
+ remove() {
453
+ this.setPagersActive(false) // fire onClose for active embedded pages before teardown
454
+ for (const id of this.vlistIds) {
455
+ this.vlistSystem.unregister(id)
456
+ }
457
+ for (const id of this.pagerIds) {
458
+ this.pagerSystem.unregister(id) // frees every tab's page roots
459
+ }
460
+
461
+ this.removeFromHTML()
462
+ this.setNodeIdNullRecursive(this.node)
463
+ if (this.isWidget) {
464
+ this.module._freeWidget(this.rootNodePtr)
465
+ } else {
466
+ this.module._freeNode(this.rootNodePtr)
467
+ }
468
+
469
+ this.module._free(this.layoutPtrBytes)
470
+ this.module._free(this.paddingPtrBytes)
471
+ this.nodeMap.clear()
472
+
473
+ this.deregister(this.screenId)
474
+ }
475
+
476
+ removeFromHTML() {
477
+ this.setPagersActive(false) // detaching (e.g. router history) closes embedded pages; restore re-opens
478
+ this.root.remove()
479
+ this.overlayElement?.remove()
480
+ this.overlayElement = null
481
+ this.screenWidth = 0
482
+ this.screenHeight = 0
483
+ this.firedScreenWidth = -1
484
+ this.firedScreenHeight = -1
485
+ }
486
+ }
@@ -0,0 +1,32 @@
1
+ // Minimal `_creatorUI` for engine-only hosts (the 3D scene editor viewport): scene pointer dispatch
2
+ // + resize fan-out, no DOM/canvas UI renderer. Covers exactly the surface the 3D SDK reaches through
3
+ // `_creatorUI` (registerTouchStartEvent/registerTouchEndEvent for scene picking, toast) plus the
4
+ // host-side contract (`_setCreator`, `_onResize`, `_destroy`).
5
+
6
+ import { createTouchSystem } from "./createTouchSystem"
7
+
8
+ export const createBareUI = async (root: HTMLElement) => {
9
+ const touch = createTouchSystem(root)
10
+ const onPointerDown = (e: PointerEvent) => touch.onSceneTouchDown(e)
11
+ const onPointerUp = (e: PointerEvent) => touch.onSceneTouchEnd(e)
12
+ root.addEventListener("pointerdown", onPointerDown)
13
+ root.addEventListener("pointerup", onPointerUp)
14
+
15
+ const resizeCallbacks: Array<(width: number, height: number) => void> = []
16
+
17
+ return {
18
+ toast(_message: string) {},
19
+ registerTouchStartEvent: touch.registerTouchStartEvent,
20
+ registerTouchEndEvent: touch.registerTouchEndEvent,
21
+ registerResizeEvent(cb: (width: number, height: number) => void) { resizeCallbacks.push(cb) },
22
+ _setCreator: touch.setCreator,
23
+ _onResize(width: number, height: number) {
24
+ for (const cb of resizeCallbacks) cb(width, height)
25
+ },
26
+ _destroy() {
27
+ root.removeEventListener("pointerdown", onPointerDown)
28
+ root.removeEventListener("pointerup", onPointerUp)
29
+ touch._destroy()
30
+ },
31
+ }
32
+ }