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