lecodes-viewer 1.1.0 → 1.2.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/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "lecodes-viewer",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "main": "src/createViewer.ts",
5
5
  "dependencies": {
6
- "lecodes-renderer": "1.1.0",
6
+ "lecodes-renderer": "1.2.0",
7
7
  "ogl": "^1.0.11",
8
- "lecodes-viewer-lite": "1.1.0"
8
+ "lecodes-viewer-lite": "1.2.0"
9
9
  },
10
10
  "scripts": {
11
11
  "dev": "vite",
File without changes
@@ -1,212 +1,212 @@
1
- import type { UIScreen } from './UIScreen'
2
-
3
- type MountedItem = {
4
- rootPtr: number // detached yoga root айтема
5
- el: HTMLElement // DOM-корень поддерева
6
- jsSubtree: any // JS-объект поддерева (для null _id при unmount)
7
- }
8
-
9
- export type VListInstance = {
10
- nodeId: number
11
- el: HTMLElement
12
- jsNode: any // VListElement
13
- screen: UIScreen
14
- pendingScroll: number | null
15
- items: Map<string, MountedItem>
16
- }
17
-
18
- export const createVListSystem = (module: any) => {
19
-
20
- const instances = new Map<number, VListInstance>() // yogaId vlist-ноды → instance
21
-
22
- // ---------- marshaling ----------
23
-
24
- const withKey = <R>(key: string, fn: (ptr: number) => R): R => {
25
- const len = module.lengthBytesUTF8(key) + 1
26
- const p = module._malloc(len)
27
- module.stringToUTF8(key, p, len)
28
- try { return fn(p) } finally { module._free(p) }
29
- }
30
-
31
- const withStringArray = <R>(arr: string[], fn: (arrPtr: number) => R): R => {
32
- const ptrs = arr.map(s => {
33
- const len = module.lengthBytesUTF8(s) + 1
34
- const p = module._malloc(len)
35
- module.stringToUTF8(s, p, len)
36
- return p
37
- })
38
- const arrPtr = module._malloc(Math.max(4 * ptrs.length, 4))
39
- ptrs.forEach((p, i) => { module.HEAPU32[(arrPtr >> 2) + i] = p })
40
- try { return fn(arrPtr) } finally {
41
- ptrs.forEach(p => module._free(p))
42
- module._free(arrPtr)
43
- }
44
- }
45
-
46
- const withFloatArray = <R>(arr: number[], fn: (ptr: number) => R): R => {
47
- const p = module._malloc(Math.max(4 * arr.length, 4))
48
- arr.forEach((v, i) => { module.HEAPF32[(p >> 2) + i] = v })
49
- try { return fn(p) } finally { module._free(p) }
50
- }
51
-
52
- const readStringArray = (arrPtr: number, count: number): string[] => {
53
- const out: string[] = []
54
- for (let i = 0; i < count; i++) {
55
- out.push(module.UTF8ToString(module.HEAPU32[(arrPtr >> 2) + i]))
56
- }
57
- return out
58
- }
59
-
60
- // ---------- layout-проход по айтемам ----------
61
-
62
- const refresh = (inst: VListInstance) => {
63
- for (const [key, item] of inst.items) {
64
- inst.screen.updateSizesFor(item.el)
65
- item.el.style.top = withKey(key, p => module._vlistOffsetOf(inst.nodeId, p)) + "px"
66
- item.el.style.left = "0px"
67
- }
68
- const h = module._getContentHeight(inst.nodeId)
69
- inst.el.style.setProperty("--scroll-height", h + "px")
70
- inst.el.style.setProperty("--scroll-width", "0px")
71
-
72
- if (inst.pendingScroll !== null) {
73
- if (Math.abs(inst.el.scrollTop - inst.pendingScroll) >= 1) {
74
- inst.el.scrollTop = inst.pendingScroll // высота уже выставлена — кламп не сработает
75
- }
76
- inst.pendingScroll = null
77
- }
78
- }
79
-
80
- // ---------- колбэки из C++ (регистрируются один раз) ----------
81
-
82
- // g_vlSync
83
- const syncFunc = module.addFunction(
84
- (_screenId: number, nodePtr: number, unmountArr: number, unmountCount: number, mountArr: number, mountCount: number) => {
85
- const inst = instances.get(nodePtr)
86
- if (!inst) return
87
- const unmountKeys = readStringArray(unmountArr, unmountCount)
88
- const mountKeys = readStringArray(mountArr, mountCount)
89
-
90
- // Контракт: интеграционный слой сносит поддеревья ДО вызова JS _syncWindow
91
- for (const key of unmountKeys) {
92
- const item = inst.items.get(key)
93
- if (!item) continue
94
- inst.items.delete(key)
95
- item.el.remove()
96
- inst.screen.releaseSubtree(item.jsSubtree) // nodeMap-чистка + _id = 0
97
- withKey(key, p => module._vlistItemUnmounted(nodePtr, p)) // free yoga root
98
- }
99
-
100
- inst.jsNode._syncWindow(unmountKeys, mountKeys) // mount: render() → vlistMount()
101
- refresh(inst)
102
- }, "viiiiii")
103
-
104
- // g_vlAdjust — якорение / кламп
105
- const adjustFunc = module.addFunction((_screenId: number, nodePtr: number, scrollY: number) => {
106
- const inst = instances.get(nodePtr)
107
- if (!inst) return
108
- inst.pendingScroll = scrollY // применим в refresh, после высоты
109
- }, "viif")
110
-
111
- // g_vlEdge
112
- const edgeFunc = module.addFunction((_screenId: number, nodePtr: number, endEdge: number) => {
113
- const inst = instances.get(nodePtr)
114
- if (!inst) return
115
- const listeners = endEdge ? (inst.jsNode.erl ?? []) : (inst.jsNode.strl ?? [])
116
- for (const cb of listeners) cb()
117
- }, "viii")
118
-
119
- module._setVListHandlers(syncFunc, adjustFunc, edgeFunc)
120
-
121
- // ---------- создание (из UIScreen.addNodeRecursive для type === "vlist") ----------
122
-
123
- const register = (screen: UIScreen, jsNode: any, el: HTMLElement, yogaId: number) => {
124
- const inst: VListInstance = { nodeId: yogaId, el, jsNode, screen, items: new Map(), pendingScroll: null }
125
- instances.set(yogaId, inst)
126
-
127
- module._vlistInit(yogaId, jsNode._overscan, jsNode._inverted ? 1 : 0,
128
- jsNode._startThreshold, jsNode._endThreshold)
129
-
130
- if (jsNode._keys.length > 0) {
131
- setKeysInternal(yogaId, jsNode._keys, jsNode._estimates)
132
- }
133
-
134
- el.addEventListener("scroll", () => {
135
- module._vlistOnScroll(yogaId, el.scrollTop)
136
- refresh(inst)
137
- for (const cb of jsNode.sl ?? []) cb(el.scrollTop)
138
- })
139
- }
140
-
141
- // Вызывается из UIScreen.updateSizes для vlist-элемента (после выставления его размеров)
142
- const updateLayout = (yogaId: number) => {
143
- const inst = instances.get(yogaId)
144
- if (!inst) return
145
- module._vlistUpdateLayout(yogaId) // внутри может дернуть sync → mount
146
- refresh(inst)
147
- }
148
-
149
- const unregister = (yogaId: number) => {
150
- const inst = instances.get(yogaId)
151
- if (!inst) return
152
- module._vlistFree(yogaId) // освобождает detached-корни
153
- instances.delete(yogaId)
154
- }
155
-
156
- const setKeysInternal = (yogaId: number, keys: string[], estimates: number[]) => {
157
- withStringArray(keys, kp =>
158
- withFloatArray(estimates, ep =>
159
- module._vlistSetKeys(yogaId, kp, ep, keys.length)))
160
- }
161
-
162
- // ---------- API для _creatorUI (зовётся из VListElement) ----------
163
-
164
- const api = {
165
- vlistSetKeys: (jsNode: any, keys: string[], estimates: number[]) => {
166
- setKeysInternal(jsNode._id, keys, estimates)
167
- },
168
- vlistInsertKeys: (jsNode: any, index: number, keys: string[], estimates: number[]) => {
169
- withStringArray(keys, kp =>
170
- withFloatArray(estimates, ep =>
171
- module._vlistInsertKeys(jsNode._id, index, kp, ep, keys.length)))
172
- },
173
- vlistRemoveKeys: (jsNode: any, keys: string[]) => {
174
- withStringArray(keys, kp => module._vlistRemoveKeys(jsNode._id, kp, keys.length))
175
- },
176
- vlistInvalidate: (jsNode: any, key: string) => {
177
- withKey(key, p => module._vlistInvalidate(jsNode._id, p))
178
- },
179
- // Вызывается из _syncWindow на каждый mount-ключ
180
- vlistMount: (jsNode: any, key: string, subtree: any) => {
181
- const inst = instances.get(jsNode._id)
182
- if (!inst) return
183
- const rootPtr = withKey(key, p => module._vlistCreateItemRoot(jsNode._id, p))
184
- const el = inst.screen.buildSubtree(subtree, rootPtr) // addNodeRecursive в detached root
185
- inst.el.appendChild(el)
186
- inst.items.set(key, { rootPtr, el, jsSubtree: subtree })
187
- withKey(key, p => module._vlistItemMounted(jsNode._id, p)) // измерение + кэш + якорение
188
- },
189
- command: (jsNode: any, name: string, ...args: any[]) => {
190
- const inst = instances.get(jsNode._id)
191
- if (!inst) return
192
- const behavior = (animated: boolean) => animated ? "smooth" as const : "auto" as const
193
- switch (name) {
194
- case "scrollTo":
195
- inst.el.scrollTo({ top: args[0], behavior: behavior(args[1] ?? true) })
196
- break
197
- case "scrollToKey": {
198
- const off = withKey(args[0], p => module._vlistOffsetOf(inst.nodeId, p))
199
- if (!isNaN(off)) inst.el.scrollTo({ top: off, behavior: behavior(args[1] ?? true) })
200
- break
201
- }
202
- case "scrollToEnd": {
203
- const h = module._getContentHeight(inst.nodeId)
204
- inst.el.scrollTo({ top: h - inst.el.clientHeight, behavior: behavior(args[0] ?? true) })
205
- break
206
- }
207
- }
208
- },
209
- }
210
-
211
- return { register, updateLayout, unregister, refresh, api, instances }
1
+ import type { UIScreen } from './UIScreen'
2
+
3
+ type MountedItem = {
4
+ rootPtr: number // detached yoga root айтема
5
+ el: HTMLElement // DOM-корень поддерева
6
+ jsSubtree: any // JS-объект поддерева (для null _id при unmount)
7
+ }
8
+
9
+ export type VListInstance = {
10
+ nodeId: number
11
+ el: HTMLElement
12
+ jsNode: any // VListElement
13
+ screen: UIScreen
14
+ pendingScroll: number | null
15
+ items: Map<string, MountedItem>
16
+ }
17
+
18
+ export const createVListSystem = (module: any) => {
19
+
20
+ const instances = new Map<number, VListInstance>() // yogaId vlist-ноды → instance
21
+
22
+ // ---------- marshaling ----------
23
+
24
+ const withKey = <R>(key: string, fn: (ptr: number) => R): R => {
25
+ const len = module.lengthBytesUTF8(key) + 1
26
+ const p = module._malloc(len)
27
+ module.stringToUTF8(key, p, len)
28
+ try { return fn(p) } finally { module._free(p) }
29
+ }
30
+
31
+ const withStringArray = <R>(arr: string[], fn: (arrPtr: number) => R): R => {
32
+ const ptrs = arr.map(s => {
33
+ const len = module.lengthBytesUTF8(s) + 1
34
+ const p = module._malloc(len)
35
+ module.stringToUTF8(s, p, len)
36
+ return p
37
+ })
38
+ const arrPtr = module._malloc(Math.max(4 * ptrs.length, 4))
39
+ ptrs.forEach((p, i) => { module.HEAPU32[(arrPtr >> 2) + i] = p })
40
+ try { return fn(arrPtr) } finally {
41
+ ptrs.forEach(p => module._free(p))
42
+ module._free(arrPtr)
43
+ }
44
+ }
45
+
46
+ const withFloatArray = <R>(arr: number[], fn: (ptr: number) => R): R => {
47
+ const p = module._malloc(Math.max(4 * arr.length, 4))
48
+ arr.forEach((v, i) => { module.HEAPF32[(p >> 2) + i] = v })
49
+ try { return fn(p) } finally { module._free(p) }
50
+ }
51
+
52
+ const readStringArray = (arrPtr: number, count: number): string[] => {
53
+ const out: string[] = []
54
+ for (let i = 0; i < count; i++) {
55
+ out.push(module.UTF8ToString(module.HEAPU32[(arrPtr >> 2) + i]))
56
+ }
57
+ return out
58
+ }
59
+
60
+ // ---------- layout-проход по айтемам ----------
61
+
62
+ const refresh = (inst: VListInstance) => {
63
+ for (const [key, item] of inst.items) {
64
+ inst.screen.updateSizesFor(item.el)
65
+ item.el.style.top = withKey(key, p => module._vlistOffsetOf(inst.nodeId, p)) + "px"
66
+ item.el.style.left = "0px"
67
+ }
68
+ const h = module._getContentHeight(inst.nodeId)
69
+ inst.el.style.setProperty("--scroll-height", h + "px")
70
+ inst.el.style.setProperty("--scroll-width", "0px")
71
+
72
+ if (inst.pendingScroll !== null) {
73
+ if (Math.abs(inst.el.scrollTop - inst.pendingScroll) >= 1) {
74
+ inst.el.scrollTop = inst.pendingScroll // высота уже выставлена — кламп не сработает
75
+ }
76
+ inst.pendingScroll = null
77
+ }
78
+ }
79
+
80
+ // ---------- колбэки из C++ (регистрируются один раз) ----------
81
+
82
+ // g_vlSync
83
+ const syncFunc = module.addFunction(
84
+ (_screenId: number, nodePtr: number, unmountArr: number, unmountCount: number, mountArr: number, mountCount: number) => {
85
+ const inst = instances.get(nodePtr)
86
+ if (!inst) return
87
+ const unmountKeys = readStringArray(unmountArr, unmountCount)
88
+ const mountKeys = readStringArray(mountArr, mountCount)
89
+
90
+ // Контракт: интеграционный слой сносит поддеревья ДО вызова JS _syncWindow
91
+ for (const key of unmountKeys) {
92
+ const item = inst.items.get(key)
93
+ if (!item) continue
94
+ inst.items.delete(key)
95
+ item.el.remove()
96
+ inst.screen.releaseSubtree(item.jsSubtree) // nodeMap-чистка + _id = 0
97
+ withKey(key, p => module._vlistItemUnmounted(nodePtr, p)) // free yoga root
98
+ }
99
+
100
+ inst.jsNode._syncWindow(unmountKeys, mountKeys) // mount: render() → vlistMount()
101
+ refresh(inst)
102
+ }, "viiiiii")
103
+
104
+ // g_vlAdjust — якорение / кламп
105
+ const adjustFunc = module.addFunction((_screenId: number, nodePtr: number, scrollY: number) => {
106
+ const inst = instances.get(nodePtr)
107
+ if (!inst) return
108
+ inst.pendingScroll = scrollY // применим в refresh, после высоты
109
+ }, "viif")
110
+
111
+ // g_vlEdge
112
+ const edgeFunc = module.addFunction((_screenId: number, nodePtr: number, endEdge: number) => {
113
+ const inst = instances.get(nodePtr)
114
+ if (!inst) return
115
+ const listeners = endEdge ? (inst.jsNode.erl ?? []) : (inst.jsNode.strl ?? [])
116
+ for (const cb of listeners) cb()
117
+ }, "viii")
118
+
119
+ module._setVListHandlers(syncFunc, adjustFunc, edgeFunc)
120
+
121
+ // ---------- создание (из UIScreen.addNodeRecursive для type === "vlist") ----------
122
+
123
+ const register = (screen: UIScreen, jsNode: any, el: HTMLElement, yogaId: number) => {
124
+ const inst: VListInstance = { nodeId: yogaId, el, jsNode, screen, items: new Map(), pendingScroll: null }
125
+ instances.set(yogaId, inst)
126
+
127
+ module._vlistInit(yogaId, jsNode._overscan, jsNode._inverted ? 1 : 0,
128
+ jsNode._startThreshold, jsNode._endThreshold)
129
+
130
+ if (jsNode._keys.length > 0) {
131
+ setKeysInternal(yogaId, jsNode._keys, jsNode._estimates)
132
+ }
133
+
134
+ el.addEventListener("scroll", () => {
135
+ module._vlistOnScroll(yogaId, el.scrollTop)
136
+ refresh(inst)
137
+ for (const cb of jsNode.sl ?? []) cb(el.scrollTop)
138
+ })
139
+ }
140
+
141
+ // Вызывается из UIScreen.updateSizes для vlist-элемента (после выставления его размеров)
142
+ const updateLayout = (yogaId: number) => {
143
+ const inst = instances.get(yogaId)
144
+ if (!inst) return
145
+ module._vlistUpdateLayout(yogaId) // внутри может дернуть sync → mount
146
+ refresh(inst)
147
+ }
148
+
149
+ const unregister = (yogaId: number) => {
150
+ const inst = instances.get(yogaId)
151
+ if (!inst) return
152
+ module._vlistFree(yogaId) // освобождает detached-корни
153
+ instances.delete(yogaId)
154
+ }
155
+
156
+ const setKeysInternal = (yogaId: number, keys: string[], estimates: number[]) => {
157
+ withStringArray(keys, kp =>
158
+ withFloatArray(estimates, ep =>
159
+ module._vlistSetKeys(yogaId, kp, ep, keys.length)))
160
+ }
161
+
162
+ // ---------- API для _creatorUI (зовётся из VListElement) ----------
163
+
164
+ const api = {
165
+ vlistSetKeys: (jsNode: any, keys: string[], estimates: number[]) => {
166
+ setKeysInternal(jsNode._id, keys, estimates)
167
+ },
168
+ vlistInsertKeys: (jsNode: any, index: number, keys: string[], estimates: number[]) => {
169
+ withStringArray(keys, kp =>
170
+ withFloatArray(estimates, ep =>
171
+ module._vlistInsertKeys(jsNode._id, index, kp, ep, keys.length)))
172
+ },
173
+ vlistRemoveKeys: (jsNode: any, keys: string[]) => {
174
+ withStringArray(keys, kp => module._vlistRemoveKeys(jsNode._id, kp, keys.length))
175
+ },
176
+ vlistInvalidate: (jsNode: any, key: string) => {
177
+ withKey(key, p => module._vlistInvalidate(jsNode._id, p))
178
+ },
179
+ // Вызывается из _syncWindow на каждый mount-ключ
180
+ vlistMount: (jsNode: any, key: string, subtree: any) => {
181
+ const inst = instances.get(jsNode._id)
182
+ if (!inst) return
183
+ const rootPtr = withKey(key, p => module._vlistCreateItemRoot(jsNode._id, p))
184
+ const el = inst.screen.buildSubtree(subtree, rootPtr) // addNodeRecursive в detached root
185
+ inst.el.appendChild(el)
186
+ inst.items.set(key, { rootPtr, el, jsSubtree: subtree })
187
+ withKey(key, p => module._vlistItemMounted(jsNode._id, p)) // измерение + кэш + якорение
188
+ },
189
+ command: (jsNode: any, name: string, ...args: any[]) => {
190
+ const inst = instances.get(jsNode._id)
191
+ if (!inst) return
192
+ const behavior = (animated: boolean) => animated ? "smooth" as const : "auto" as const
193
+ switch (name) {
194
+ case "scrollTo":
195
+ inst.el.scrollTo({ top: args[0], behavior: behavior(args[1] ?? true) })
196
+ break
197
+ case "scrollToKey": {
198
+ const off = withKey(args[0], p => module._vlistOffsetOf(inst.nodeId, p))
199
+ if (!isNaN(off)) inst.el.scrollTo({ top: off, behavior: behavior(args[1] ?? true) })
200
+ break
201
+ }
202
+ case "scrollToEnd": {
203
+ const h = module._getContentHeight(inst.nodeId)
204
+ inst.el.scrollTo({ top: h - inst.el.clientHeight, behavior: behavior(args[0] ?? true) })
205
+ break
206
+ }
207
+ }
208
+ },
209
+ }
210
+
211
+ return { register, updateLayout, unregister, refresh, api, instances }
212
212
  }
File without changes
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
File without changes