@mindexec/cli 0.2.478 → 0.2.480

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 (146) hide show
  1. package/README.md +323 -323
  2. package/codex-model-catalog.js +202 -202
  3. package/codex-runtime.js +1608 -1608
  4. package/desktop-workspace-state.cjs +120 -120
  5. package/electron/main.cjs +517 -517
  6. package/electron/preload.cjs +17 -17
  7. package/electron/recurring-update-owner-smoke.mjs +55 -55
  8. package/electron/recurring-update-owner.cjs +97 -97
  9. package/electron/source-smoke.mjs +108 -108
  10. package/electron/update-feed-resolver.cjs +79 -79
  11. package/electron/update-feed.json +10 -10
  12. package/electron/update-manager-smoke.mjs +146 -146
  13. package/electron/update-manager.cjs +297 -297
  14. package/electron/windows-package-smoke.mjs +156 -156
  15. package/launch-bridge.cjs +236 -236
  16. package/lzo1x.js +110 -110
  17. package/mode4-atlas-worker.js +440 -440
  18. package/mode4-atlas.js +130 -130
  19. package/package.json +1 -1
  20. package/port-guard.cjs +285 -285
  21. package/remote-fast/osx-arm64/MacScreenCaptureKitH264.swift +339 -339
  22. package/remote-fast/osx-arm64/MacSystemAudioCapture.swift +214 -214
  23. package/remote-fast/osx-arm64/UnixRawCaptureHelper.mjs +80 -80
  24. package/remote-fast/osx-arm64/livedesk-raw-capture-resampler.mjs +73 -73
  25. package/remote-fast/osx-arm64/mindexec-remote-fast.dll +0 -0
  26. package/remote-fast/osx-x64/MacScreenCaptureKitH264.swift +339 -339
  27. package/remote-fast/osx-x64/MacSystemAudioCapture.swift +214 -214
  28. package/remote-fast/osx-x64/UnixRawCaptureHelper.mjs +80 -80
  29. package/remote-fast/osx-x64/livedesk-raw-capture-resampler.mjs +73 -73
  30. package/remote-fast/osx-x64/mindexec-remote-fast.dll +0 -0
  31. package/remote-fast/win-x64/mindexec-remote-fast.dll +0 -0
  32. package/remote-hub.js +3698 -3698
  33. package/scripts/auth-session-smoke.mjs +262 -262
  34. package/scripts/board-render-ownership-smoke.mjs +85 -85
  35. package/scripts/codex-model-catalog-smoke.mjs +150 -150
  36. package/scripts/codex-runtime-home-smoke.mjs +58 -58
  37. package/scripts/codex-sdk-image-live-smoke.mjs +43 -43
  38. package/scripts/codex-sdk-image-runtime-smoke.mjs +122 -122
  39. package/scripts/codex-sdk-runtime-smoke.mjs +89 -89
  40. package/scripts/desktop-update-publisher-smoke.mjs +89 -89
  41. package/scripts/desktop-update-worker-smoke.mjs +72 -72
  42. package/scripts/desktop-workspace-state-smoke.mjs +81 -81
  43. package/scripts/drive-installed-update-smoke.mjs +67 -67
  44. package/scripts/npm-fresh-install-smoke.mjs +216 -216
  45. package/scripts/publish-mindexec-desktop-updates.mjs +224 -224
  46. package/scripts/remote-agent-managed-smoke.mjs +318 -318
  47. package/scripts/remote-agent-package-smoke.mjs +64 -64
  48. package/scripts/remote-agent-ws-smoke.mjs +269 -269
  49. package/scripts/remote-fast-live-rate-smoke.mjs +343 -343
  50. package/scripts/remote-fast-mdm-browser-smoke.mjs +453 -453
  51. package/scripts/remote-fleet-render-smoke.mjs +1350 -1350
  52. package/scripts/remote-frame-ws-smoke.mjs +234 -234
  53. package/scripts/remote-http-smoke.mjs +592 -592
  54. package/scripts/remote-hub-identity-smoke.mjs +146 -146
  55. package/scripts/remote-hub-scale-smoke.mjs +124 -124
  56. package/scripts/remote-hub-smoke.mjs +699 -699
  57. package/scripts/remote-input-ws-smoke.mjs +263 -263
  58. package/scripts/remote-registry-follower-smoke.mjs +815 -815
  59. package/scripts/setup-tree-sitter-grammars.mjs +59 -59
  60. package/server.js +7510 -7510
  61. package/start-bridge.bat +32 -32
  62. package/start-bridge.sh +81 -81
  63. package/tree-sitter-grammars/README.md +18 -0
  64. package/tree-sitter-grammars/tree-sitter-c_sharp.wasm +0 -0
  65. package/tree-sitter-grammars/tree-sitter-go.wasm +0 -0
  66. package/tree-sitter-grammars/tree-sitter-java.wasm +0 -0
  67. package/tree-sitter-grammars/tree-sitter-javascript.wasm +0 -0
  68. package/tree-sitter-grammars/tree-sitter-python.wasm +0 -0
  69. package/tree-sitter-grammars/tree-sitter-rust.wasm +0 -0
  70. package/tree-sitter-grammars/tree-sitter-tsx.wasm +0 -0
  71. package/tree-sitter-grammars/tree-sitter-typescript.wasm +0 -0
  72. package/wwwroot/_content/MindExecution.Plugins.Admin/css/admin-dashboard.css +546 -546
  73. package/wwwroot/_content/MindExecution.Plugins.Directory/background.png +0 -0
  74. package/wwwroot/_content/MindExecution.Plugins.YouTube/css/youtube-search.css +251 -251
  75. package/wwwroot/_content/MindExecution.Shared/css/mind-map-overrides.css +3789 -3789
  76. package/wwwroot/_content/MindExecution.Shared/fonts/NotoSansKR-Bold.ttf +0 -0
  77. package/wwwroot/_content/MindExecution.Shared/fonts/NotoSansKR-Regular.ttf +0 -0
  78. package/wwwroot/_content/MindExecution.Shared/js/agent-visualization.js +27 -27
  79. package/wwwroot/_content/MindExecution.Shared/js/background-themes.js +1449 -1449
  80. package/wwwroot/_content/MindExecution.Shared/js/code-master.js +8316 -8316
  81. package/wwwroot/_content/MindExecution.Shared/js/file-system-helper.js +338 -338
  82. package/wwwroot/_content/MindExecution.Shared/js/marked.min.js +69 -69
  83. package/wwwroot/_content/MindExecution.Shared/js/mind-map-animated-image-preview.js +270 -270
  84. package/wwwroot/_content/MindExecution.Shared/js/mind-map-automation-edge-motion.js +170 -170
  85. package/wwwroot/_content/MindExecution.Shared/js/mind-map-board-render-ownership.js +81 -81
  86. package/wwwroot/_content/MindExecution.Shared/js/mind-map-core.js +10528 -10506
  87. package/wwwroot/_content/MindExecution.Shared/js/mind-map-core.js.backup +1059 -0
  88. package/wwwroot/_content/MindExecution.Shared/js/mind-map-css3d-hotpath.js +902 -902
  89. package/wwwroot/_content/MindExecution.Shared/js/mind-map-css3d-manager.js +24637 -24637
  90. package/wwwroot/_content/MindExecution.Shared/js/mind-map-dev-guards.js +325 -325
  91. package/wwwroot/_content/MindExecution.Shared/js/mind-map-dnd.js +1491 -1491
  92. package/wwwroot/_content/MindExecution.Shared/js/mind-map-dnd.js.bak +434 -0
  93. package/wwwroot/_content/MindExecution.Shared/js/mind-map-glow-shader.js +70 -70
  94. package/wwwroot/_content/MindExecution.Shared/js/mind-map-image-policy.js +218 -218
  95. package/wwwroot/_content/MindExecution.Shared/js/mind-map-interactions.js +7081 -7087
  96. package/wwwroot/_content/MindExecution.Shared/js/mind-map-lod-plan-worker.js +160 -160
  97. package/wwwroot/_content/MindExecution.Shared/js/mind-map-lod-renderer.js +9960 -9958
  98. package/wwwroot/_content/MindExecution.Shared/js/mind-map-lod-video-placeholder.js +107 -107
  99. package/wwwroot/_content/MindExecution.Shared/js/mind-map-logic-workers.js +1004 -1004
  100. package/wwwroot/_content/MindExecution.Shared/js/mind-map-menu-manager.js +1351 -1351
  101. package/wwwroot/_content/MindExecution.Shared/js/mind-map-multi-select.js +865 -865
  102. package/wwwroot/_content/MindExecution.Shared/js/mind-map-node-search-worker.js +554 -554
  103. package/wwwroot/_content/MindExecution.Shared/js/mind-map-nodes.js +4771 -4744
  104. package/wwwroot/_content/MindExecution.Shared/js/mind-map-normal-boundary-optimizer.js +624 -624
  105. package/wwwroot/_content/MindExecution.Shared/js/mind-map-object-manager.js +241 -241
  106. package/wwwroot/_content/MindExecution.Shared/js/mind-map-object-manager.js.backup +372 -0
  107. package/wwwroot/_content/MindExecution.Shared/js/mind-map-pipeline.js +2290 -2290
  108. package/wwwroot/_content/MindExecution.Shared/js/mind-map-remote-codecs.js +202 -202
  109. package/wwwroot/_content/MindExecution.Shared/js/mind-map-render-plan.js +1971 -1971
  110. package/wwwroot/_content/MindExecution.Shared/js/mind-map-text-lod-system.js +93 -93
  111. package/wwwroot/_content/MindExecution.Shared/js/mind-map-texture-factory.js +2391 -2391
  112. package/wwwroot/_content/MindExecution.Shared/js/mind-map-texture-factory.js.backup +1258 -0
  113. package/wwwroot/_content/MindExecution.Shared/js/mind-map-video-playback-controls.js +95 -95
  114. package/wwwroot/_content/MindExecution.Shared/js/mind-map-visibility-worker.js +895 -895
  115. package/wwwroot/_content/MindExecution.Shared/js/mindmap-toolbar.js +636 -636
  116. package/wwwroot/_content/MindExecution.Shared/js/native-drop-handler.js +45 -45
  117. package/wwwroot/_content/MindExecution.Shared/js/plan-master.js +117 -117
  118. package/wwwroot/_content/MindExecution.Shared/js/renderers/CSS3DRenderer.js +114 -114
  119. package/wwwroot/_content/MindExecution.Shared/js/texture-worker-manager.js +10 -10
  120. package/wwwroot/_content/MindExecution.Shared/js/three.min.js +6 -6
  121. package/wwwroot/_content/MindExecution.Shared/js/token-manager.js +7 -7
  122. package/wwwroot/_content/MindExecution.Shared/js/troika-bundle.js +6 -6
  123. package/wwwroot/_content/MindExecution.Shared/js/troika-bundle.js.map +2 -2
  124. package/wwwroot/_content/MindExecution.Shared/lib/font-awesome/css/all.min.css +8 -8
  125. package/wwwroot/_content/MindExecution.Shared/models/vocab.txt +30522 -30522
  126. package/wwwroot/_framework/{Google.Protobuf.33g2gb5is0.dll → Google.Protobuf.9h59ukbel7.dll} +0 -0
  127. package/wwwroot/_framework/{MindExecution.Core.z100xo4mg0.dll → MindExecution.Core.gnj3fo0pr7.dll} +0 -0
  128. package/wwwroot/_framework/{MindExecution.Kernel.i9cldrx8yc.dll → MindExecution.Kernel.1qygzf8o02.dll} +0 -0
  129. package/wwwroot/_framework/{MindExecution.Plugins.Admin.48lzqddb0a.dll → MindExecution.Plugins.Admin.n3uxh144wo.dll} +0 -0
  130. package/wwwroot/_framework/{MindExecution.Plugins.Business.7m8u1q142p.dll → MindExecution.Plugins.Business.vl5dximn25.dll} +0 -0
  131. package/wwwroot/_framework/{MindExecution.Plugins.Concept.cl5wmr53od.dll → MindExecution.Plugins.Concept.rutru9qeoi.dll} +0 -0
  132. package/wwwroot/_framework/{MindExecution.Plugins.Directory.slipi39041.dll → MindExecution.Plugins.Directory.fqbmq85het.dll} +0 -0
  133. package/wwwroot/_framework/{MindExecution.Plugins.PlanMaster.qfge05l671.dll → MindExecution.Plugins.PlanMaster.ko11cc9pll.dll} +0 -0
  134. package/wwwroot/_framework/{MindExecution.Plugins.YouTube.tn7v29ut77.dll → MindExecution.Plugins.YouTube.mt39q0to2j.dll} +0 -0
  135. package/wwwroot/_framework/MindExecution.Shared.umgls362or.dll +0 -0
  136. package/wwwroot/_framework/{MindExecution.Web.rwawrxxk38.dll → MindExecution.Web.1dwoarj2hg.dll} +0 -0
  137. package/wwwroot/_framework/{System.Collections.23yxgetbju.dll → System.Collections.zyvbaq6mpd.dll} +0 -0
  138. package/wwwroot/_framework/blazor.boot.json +25 -25
  139. package/wwwroot/_headers +40 -40
  140. package/wwwroot/_redirects +1 -1
  141. package/wwwroot/index.html +772 -772
  142. package/wwwroot/js/marketing-tool.js +180 -180
  143. package/wwwroot/manifest.webmanifest +23 -23
  144. package/wwwroot/service-worker-assets.js +109 -81
  145. package/wwwroot/service-worker.js +33 -33
  146. package/wwwroot/_framework/MindExecution.Shared.rjqlf0umjn.dll +0 -0
@@ -1,895 +1,895 @@
1
- 'use strict';
2
-
3
- const FULL_RES_VISIBILITY_EXIT_MARGIN_SCALE = 1.8;
4
- const MAX_DIRECT_GRID_LOOKUPS = 4096;
5
- const DEFAULT_HIT_TEST_CELL_RADIUS = 1;
6
- const MAX_HIT_TEST_CELL_RADIUS = 3;
7
-
8
- function createState() {
9
- return {
10
- boardId: '',
11
- cellSize: 500,
12
- nodes: new Map(),
13
- spatialGrid: new Map(),
14
- lastVisibleIds: new Set(),
15
- lastPriorityIds: [],
16
- lastVisibilityRect: null,
17
- lastNodeCount: 0,
18
- lastResultSeq: 0
19
- };
20
- }
21
-
22
- let runtime = createState();
23
-
24
- function normalizeBoardId(boardId) {
25
- return String(boardId ?? '').trim();
26
- }
27
-
28
- function normalizeNodeSnapshot(node) {
29
- if (!node || typeof node !== 'object') {
30
- return null;
31
- }
32
-
33
- const id = String(node.id || '').trim();
34
- if (!id) {
35
- return null;
36
- }
37
-
38
- const minX = Number(node.minX || 0);
39
- const maxX = Number(node.maxX || minX);
40
- const minY = Number(node.minY || 0);
41
- const maxY = Number(node.maxY || minY);
42
-
43
- return {
44
- id,
45
- contentType: String(node.contentType || '').trim().toLowerCase(),
46
- textLike: node.textLike === true,
47
- x: Number(node.x || minX),
48
- y: Number(node.y || maxY),
49
- z: Number(node.z || 0),
50
- width: Math.max(1, Number(node.width || (maxX - minX) || 1)),
51
- height: Math.max(1, Number(node.height || (maxY - minY) || 1)),
52
- minX,
53
- maxX,
54
- minY,
55
- maxY,
56
- cellKeys: Array.isArray(node.cellKeys)
57
- ? node.cellKeys.map((key) => String(key || '').trim()).filter(Boolean)
58
- : []
59
- };
60
- }
61
-
62
- function clearState(boardId, cellSize) {
63
- runtime = createState();
64
- runtime.boardId = normalizeBoardId(boardId);
65
- runtime.cellSize = Math.max(1, Number(cellSize || 500));
66
- }
67
-
68
- function removeNodeFromGrid(nodeId, node) {
69
- const normalizedId = String(nodeId || '').trim();
70
- if (!normalizedId || !node?.cellKeys) {
71
- return;
72
- }
73
-
74
- for (let i = 0; i < node.cellKeys.length; i++) {
75
- const key = node.cellKeys[i];
76
- const cell = runtime.spatialGrid.get(key);
77
- if (!cell) {
78
- continue;
79
- }
80
-
81
- cell.delete(normalizedId);
82
- if (cell.size === 0) {
83
- runtime.spatialGrid.delete(key);
84
- }
85
- }
86
- }
87
-
88
- function upsertNode(node) {
89
- const normalizedNode = normalizeNodeSnapshot(node);
90
- if (!normalizedNode) {
91
- return;
92
- }
93
-
94
- const existing = runtime.nodes.get(normalizedNode.id);
95
- if (existing) {
96
- removeNodeFromGrid(normalizedNode.id, existing);
97
- }
98
-
99
- runtime.nodes.set(normalizedNode.id, normalizedNode);
100
- for (let i = 0; i < normalizedNode.cellKeys.length; i++) {
101
- const key = normalizedNode.cellKeys[i];
102
- let cell = runtime.spatialGrid.get(key);
103
- if (!cell) {
104
- cell = new Set();
105
- runtime.spatialGrid.set(key, cell);
106
- }
107
- cell.add(normalizedNode.id);
108
- }
109
- }
110
-
111
- function removeNode(nodeId) {
112
- const normalizedId = String(nodeId || '').trim();
113
- if (!normalizedId) {
114
- return;
115
- }
116
-
117
- const existing = runtime.nodes.get(normalizedId);
118
- if (!existing) {
119
- return;
120
- }
121
-
122
- removeNodeFromGrid(normalizedId, existing);
123
- runtime.nodes.delete(normalizedId);
124
- runtime.lastVisibleIds.delete(normalizedId);
125
- }
126
-
127
- function rebuildBoard(boardId, cellSize, nodes) {
128
- clearState(boardId, cellSize);
129
- const sourceNodes = Array.isArray(nodes) ? nodes : [];
130
- for (let i = 0; i < sourceNodes.length; i++) {
131
- upsertNode(sourceNodes[i]);
132
- }
133
- }
134
-
135
- function getVisibilityMarginPolicy(cameraZ, isPanning = false, isCtrlWheelVerticalScrolling = false) {
136
- let cullingMarginPx = 3000;
137
- let maxMarginCells = 6;
138
- let useCoarseVisibility = false;
139
-
140
- if (cameraZ < 4500) {
141
- cullingMarginPx = 1000;
142
- maxMarginCells = 4;
143
- } else if (cameraZ < 12000) {
144
- cullingMarginPx = 2200;
145
- maxMarginCells = 5;
146
- } else if (cameraZ < 30000) {
147
- cullingMarginPx = 900;
148
- maxMarginCells = 2;
149
- } else {
150
- cullingMarginPx = 240;
151
- maxMarginCells = 1;
152
- useCoarseVisibility = isPanning !== true;
153
- }
154
-
155
- if (isPanning === true) {
156
- if (cameraZ < 4500) {
157
- cullingMarginPx += 500;
158
- maxMarginCells += 1;
159
- } else if (cameraZ < 12000) {
160
- cullingMarginPx += 700;
161
- maxMarginCells += 1;
162
- } else if (cameraZ < 30000) {
163
- cullingMarginPx += 600;
164
- maxMarginCells += 1;
165
- } else {
166
- cullingMarginPx += 320;
167
- maxMarginCells += 1;
168
- }
169
- }
170
-
171
- if (isCtrlWheelVerticalScrolling && cameraZ < 12000) {
172
- cullingMarginPx += (cameraZ < 4500) ? 700 : 900;
173
- maxMarginCells += 2;
174
- }
175
-
176
- return {
177
- cullingMarginPx,
178
- maxMarginCells,
179
- useCoarseVisibility
180
- };
181
- }
182
-
183
- function describeCellRange(rect, cellSize) {
184
- if (!rect) {
185
- return null;
186
- }
187
-
188
- const left = Number(rect.left);
189
- const right = Number(rect.right);
190
- const bottom = Number(rect.bottom);
191
- const top = Number(rect.top);
192
- if (!Number.isFinite(left) || !Number.isFinite(right) || !Number.isFinite(bottom) || !Number.isFinite(top)) {
193
- return null;
194
- }
195
- if (right < left || top < bottom) {
196
- return null;
197
- }
198
-
199
- const startCellX = Math.floor(left / cellSize);
200
- const endCellX = Math.floor(right / cellSize);
201
- const startCellY = Math.floor(bottom / cellSize);
202
- const endCellY = Math.floor(top / cellSize);
203
- const spanX = Math.max(0, endCellX - startCellX + 1);
204
- const spanY = Math.max(0, endCellY - startCellY + 1);
205
-
206
- return {
207
- startCellX,
208
- endCellX,
209
- startCellY,
210
- endCellY,
211
- spanX,
212
- spanY,
213
- directGridLookups: spanX * spanY
214
- };
215
- }
216
-
217
- function intersectsRect(node, rect) {
218
- if (!node || !rect) {
219
- return false;
220
- }
221
-
222
- return !(
223
- node.maxX < rect.left ||
224
- node.minX > rect.right ||
225
- node.maxY < rect.bottom ||
226
- node.minY > rect.top
227
- );
228
- }
229
-
230
- function collectCandidates(range) {
231
- const candidateIds = new Set();
232
- let usedPopulatedCellScan = false;
233
-
234
- if (!range) {
235
- runtime.nodes.forEach((_, nodeId) => candidateIds.add(nodeId));
236
- return {
237
- candidateIds,
238
- usedPopulatedCellScan,
239
- directGridLookups: 0,
240
- gridSpanX: 0,
241
- gridSpanY: 0
242
- };
243
- }
244
-
245
- if (range.directGridLookups > MAX_DIRECT_GRID_LOOKUPS) {
246
- usedPopulatedCellScan = true;
247
- runtime.spatialGrid.forEach((nodeIds, key) => {
248
- const separatorIndex = key.indexOf(':');
249
- if (separatorIndex <= 0) {
250
- return;
251
- }
252
-
253
- const cx = Number(key.slice(0, separatorIndex));
254
- const cy = Number(key.slice(separatorIndex + 1));
255
- if (!Number.isFinite(cx) || !Number.isFinite(cy)) {
256
- return;
257
- }
258
-
259
- if (cx >= range.startCellX && cx <= range.endCellX && cy >= range.startCellY && cy <= range.endCellY) {
260
- nodeIds.forEach((nodeId) => candidateIds.add(nodeId));
261
- }
262
- });
263
- } else {
264
- for (let cx = range.startCellX; cx <= range.endCellX; cx++) {
265
- for (let cy = range.startCellY; cy <= range.endCellY; cy++) {
266
- const cell = runtime.spatialGrid.get(`${cx}:${cy}`);
267
- if (!cell) {
268
- continue;
269
- }
270
- cell.forEach((nodeId) => candidateIds.add(nodeId));
271
- }
272
- }
273
- }
274
-
275
- runtime.lastVisibleIds.forEach((nodeId) => {
276
- if (runtime.nodes.has(nodeId)) {
277
- candidateIds.add(nodeId);
278
- }
279
- });
280
-
281
- return {
282
- candidateIds,
283
- usedPopulatedCellScan,
284
- directGridLookups: range.directGridLookups,
285
- gridSpanX: range.spanX,
286
- gridSpanY: range.spanY
287
- };
288
- }
289
-
290
- function buildCellKeySetFromRange(range, targetSet) {
291
- if (!range || !targetSet) {
292
- return;
293
- }
294
-
295
- for (let cx = range.startCellX; cx <= range.endCellX; cx++) {
296
- for (let cy = range.startCellY; cy <= range.endCellY; cy++) {
297
- targetSet.add(`${cx}:${cy}`);
298
- }
299
- }
300
- }
301
-
302
- function addCandidatesFromCellKeySet(cellKeys, candidateIds, skipPreviouslyVisible = false) {
303
- if (!cellKeys || !candidateIds || cellKeys.size === 0) {
304
- return;
305
- }
306
-
307
- cellKeys.forEach((key) => {
308
- const nodeIds = runtime.spatialGrid.get(key);
309
- if (!nodeIds) {
310
- return;
311
- }
312
-
313
- nodeIds.forEach((nodeId) => {
314
- if (skipPreviouslyVisible && runtime.lastVisibleIds.has(nodeId)) {
315
- return;
316
- }
317
-
318
- candidateIds.add(nodeId);
319
- });
320
- });
321
- }
322
-
323
- function normalizeHitTestCellRadius(cellRadius) {
324
- const normalized = Math.round(Number(cellRadius || DEFAULT_HIT_TEST_CELL_RADIUS));
325
- if (!Number.isFinite(normalized)) {
326
- return DEFAULT_HIT_TEST_CELL_RADIUS;
327
- }
328
-
329
- return Math.max(0, Math.min(MAX_HIT_TEST_CELL_RADIUS, normalized));
330
- }
331
-
332
- function collectHitTestCandidateIds(centerCellX, centerCellY, cellRadius) {
333
- const candidateIds = new Set();
334
- const normalizedCellRadius = normalizeHitTestCellRadius(cellRadius);
335
- let cellLookups = 0;
336
-
337
- for (let offsetX = -normalizedCellRadius; offsetX <= normalizedCellRadius; offsetX++) {
338
- for (let offsetY = -normalizedCellRadius; offsetY <= normalizedCellRadius; offsetY++) {
339
- cellLookups++;
340
- const cell = runtime.spatialGrid.get(`${centerCellX + offsetX}:${centerCellY + offsetY}`);
341
- if (!cell) {
342
- continue;
343
- }
344
-
345
- cell.forEach((nodeId) => candidateIds.add(nodeId));
346
- }
347
- }
348
-
349
- return {
350
- candidateIds,
351
- cellLookups,
352
- cellRadius: normalizedCellRadius
353
- };
354
- }
355
-
356
- function normalizeSelectionRect(rect) {
357
- if (!rect || typeof rect !== 'object') {
358
- return null;
359
- }
360
-
361
- const left = Number(rect.left);
362
- const right = Number(rect.right);
363
- const bottom = Number(rect.bottom);
364
- const top = Number(rect.top);
365
- if (!Number.isFinite(left) || !Number.isFinite(right) || !Number.isFinite(bottom) || !Number.isFinite(top)) {
366
- return null;
367
- }
368
-
369
- return {
370
- left: Math.min(left, right),
371
- right: Math.max(left, right),
372
- bottom: Math.min(bottom, top),
373
- top: Math.max(bottom, top)
374
- };
375
- }
376
-
377
- function buildPriorityIds(visibleIds, camX, camY) {
378
- const buffer = [];
379
- visibleIds.forEach((nodeId) => {
380
- const node = runtime.nodes.get(nodeId);
381
- if (!node) {
382
- return;
383
- }
384
-
385
- const dx = Number(node.x || 0) - camX;
386
- const dy = Number(node.y || 0) - camY;
387
- const distSq = (dx * dx) + (dy * dy);
388
- const textPriority = node.textLike === true ? 0 : 1;
389
- const mediaPriority = node.contentType === 'image' ? 0 : 1;
390
- buffer.push({ nodeId, textPriority, mediaPriority, distSq });
391
- });
392
-
393
- buffer.sort((a, b) => {
394
- if (a.textPriority !== b.textPriority) {
395
- return a.textPriority - b.textPriority;
396
- }
397
- if (a.mediaPriority !== b.mediaPriority) {
398
- return a.mediaPriority - b.mediaPriority;
399
- }
400
- return a.distSq - b.distSq;
401
- });
402
-
403
- return buffer.map((entry) => entry.nodeId);
404
- }
405
-
406
- function buildIncrementalPriorityIds(nextVisibleIds, enteredIds, camX, camY) {
407
- const priorityIds = [];
408
- const used = new Set();
409
-
410
- if (Array.isArray(runtime.lastPriorityIds)) {
411
- for (let i = 0; i < runtime.lastPriorityIds.length; i++) {
412
- const nodeId = String(runtime.lastPriorityIds[i] || '').trim();
413
- if (!nodeId || used.has(nodeId) || !nextVisibleIds.has(nodeId)) {
414
- continue;
415
- }
416
-
417
- priorityIds.push(nodeId);
418
- used.add(nodeId);
419
- }
420
- }
421
-
422
- const enteredSet = new Set();
423
- if (Array.isArray(enteredIds)) {
424
- for (let i = 0; i < enteredIds.length; i++) {
425
- const nodeId = String(enteredIds[i] || '').trim();
426
- if (nodeId && nextVisibleIds.has(nodeId) && !used.has(nodeId)) {
427
- enteredSet.add(nodeId);
428
- }
429
- }
430
- }
431
-
432
- if (enteredSet.size > 0) {
433
- const enteredPriorityIds = buildPriorityIds(enteredSet, camX, camY);
434
- for (let i = 0; i < enteredPriorityIds.length; i++) {
435
- const nodeId = enteredPriorityIds[i];
436
- if (!used.has(nodeId)) {
437
- priorityIds.push(nodeId);
438
- used.add(nodeId);
439
- }
440
- }
441
- }
442
-
443
- if (priorityIds.length !== nextVisibleIds.size) {
444
- return buildPriorityIds(nextVisibleIds, camX, camY);
445
- }
446
-
447
- return priorityIds;
448
- }
449
-
450
- function handleVisibilityQuery(message) {
451
- const boardId = normalizeBoardId(message.boardId);
452
- if (!boardId || boardId !== runtime.boardId) {
453
- return;
454
- }
455
-
456
- const t0 = (typeof performance !== 'undefined' && typeof performance.now === 'function')
457
- ? performance.now()
458
- : Date.now();
459
-
460
- const camera = message.camera || {};
461
- const state = message.state || {};
462
- const seq = Number(message.seq || 0);
463
- const previousResultSeq = Math.max(0, Math.floor(Number(runtime.lastResultSeq || 0)));
464
- const mainLastAppliedSeq = Math.max(0, Math.floor(Number(message.mainLastAppliedSeq || 0)));
465
- const cellSize = Math.max(1, Number(message.cellSize || runtime.cellSize || 500));
466
- const camX = Number(camera.x || 0);
467
- const camY = Number(camera.y || 0);
468
- const camZ = Number(camera.z || 0);
469
- const camNear = Number(camera.near || 0);
470
- const camFar = Number(camera.far || Number.MAX_SAFE_INTEGER);
471
- const viewportW = Math.max(1, Number(camera.viewportW || 1));
472
- const viewportH = Math.max(1, Number(camera.viewportH || 1));
473
- const aspect = Math.max(0.1, Number(camera.aspect || 1));
474
- const fov = Number(camera.fov || 45);
475
- const isPanning = state.isPanning === true;
476
- const isZooming = state.isZooming === true;
477
- const isFullResMode = state.isFullResMode === true;
478
- const isCtrlWheelVerticalScrolling = state.isCtrlWheelVerticalScrolling === true;
479
-
480
- const marginPolicy = getVisibilityMarginPolicy(camZ, isPanning, isCtrlWheelVerticalScrolling);
481
- let marginWorld = 0;
482
- let verticalLeadMarginTopWorld = 0;
483
- let verticalLeadMarginBottomWorld = 0;
484
- let horizontalLeadMarginLeftWorld = 0;
485
- let horizontalLeadMarginRightWorld = 0;
486
-
487
- const vfov = (fov * Math.PI) / 180;
488
- const viewHeight = 2 * Math.tan(vfov / 2) * Math.max(1, camZ);
489
- const viewWidth = viewHeight * aspect;
490
- const worldPerPxX = viewWidth / viewportW;
491
- const worldPerPxY = viewHeight / viewportH;
492
- const rawMarginWorld = marginPolicy.cullingMarginPx * Math.max(worldPerPxX, worldPerPxY);
493
- marginWorld = Math.min(rawMarginWorld, cellSize * marginPolicy.maxMarginCells);
494
-
495
- if (isCtrlWheelVerticalScrolling && camZ < 12000) {
496
- const directionalLeadWorld = Math.min(
497
- rawMarginWorld * 0.8,
498
- cellSize * Math.max(1, marginPolicy.maxMarginCells - 1)
499
- );
500
- const lastCtrlWheelDeltaY = Number(state.lastCtrlWheelDeltaY || 0);
501
- if (lastCtrlWheelDeltaY < -0.01) {
502
- verticalLeadMarginTopWorld = directionalLeadWorld;
503
- } else if (lastCtrlWheelDeltaY > 0.01) {
504
- verticalLeadMarginBottomWorld = directionalLeadWorld;
505
- } else {
506
- verticalLeadMarginTopWorld = directionalLeadWorld * 0.5;
507
- verticalLeadMarginBottomWorld = directionalLeadWorld * 0.5;
508
- }
509
- }
510
-
511
- if (isPanning && camZ < 12000) {
512
- const panLeadWorld = Math.min(
513
- rawMarginWorld * (camZ < 4500 ? 1.35 : 1.1),
514
- cellSize * Math.max(1, marginPolicy.maxMarginCells)
515
- );
516
- const panDx = Number(state.lastPanWorldDeltaX || 0);
517
- const panDy = Number(state.lastPanWorldDeltaY || 0);
518
- if (panDx > 0.01) {
519
- horizontalLeadMarginRightWorld = panLeadWorld;
520
- } else if (panDx < -0.01) {
521
- horizontalLeadMarginLeftWorld = panLeadWorld;
522
- }
523
- if (panDy > 0.01) {
524
- verticalLeadMarginTopWorld = Math.max(verticalLeadMarginTopWorld, panLeadWorld);
525
- } else if (panDy < -0.01) {
526
- verticalLeadMarginBottomWorld = Math.max(verticalLeadMarginBottomWorld, panLeadWorld);
527
- }
528
- }
529
-
530
- const halfVisibleW = viewWidth / 2;
531
- const halfVisibleH = viewHeight / 2;
532
- const viewportLeft = camX - halfVisibleW;
533
- const viewportRight = camX + halfVisibleW;
534
- const viewportBottom = camY - halfVisibleH;
535
- const viewportTop = camY + halfVisibleH;
536
- const enterMarginWorld = marginWorld;
537
- const exitMarginWorld = marginWorld * FULL_RES_VISIBILITY_EXIT_MARGIN_SCALE;
538
-
539
- const currentEnterRect = {
540
- left: viewportLeft - (enterMarginWorld + horizontalLeadMarginLeftWorld),
541
- right: viewportRight + (enterMarginWorld + horizontalLeadMarginRightWorld),
542
- bottom: viewportBottom - (enterMarginWorld + verticalLeadMarginBottomWorld),
543
- top: viewportTop + (enterMarginWorld + verticalLeadMarginTopWorld)
544
- };
545
- const currentPreserveRect = {
546
- left: viewportLeft - (exitMarginWorld + horizontalLeadMarginLeftWorld),
547
- right: viewportRight + (exitMarginWorld + horizontalLeadMarginRightWorld),
548
- bottom: viewportBottom - (exitMarginWorld + verticalLeadMarginBottomWorld),
549
- top: viewportTop + (exitMarginWorld + verticalLeadMarginTopWorld)
550
- };
551
-
552
- const previousVisibilityRect = runtime.lastVisibilityRect || null;
553
- const currentRange = describeCellRange(currentEnterRect, cellSize);
554
- let candidateIds = new Set();
555
- let usedPopulatedCellScan = false;
556
- let directGridLookups = Number(currentRange?.directGridLookups || 0);
557
- let gridSpanX = Number(currentRange?.spanX || 0);
558
- let gridSpanY = Number(currentRange?.spanY || 0);
559
- const previousVisibleCount = runtime.lastVisibleIds.size;
560
- const nextVisibleIds = new Set();
561
- let usedIncrementalPanVisibility = false;
562
- let incrementalPanBandCount = 0;
563
- let incrementalPanCellLookups = 0;
564
- const canAttemptIncrementalFullResPan =
565
- isFullResMode === true &&
566
- isPanning === true &&
567
- isZooming !== true &&
568
- state.isWindowResizing !== true &&
569
- state.isResizing !== true &&
570
- previousVisibilityRect?.mode === 'full-res' &&
571
- runtime.lastNodeCount === runtime.nodes.size &&
572
- Math.abs(Number(previousVisibilityRect?.cameraZ || 0) - camZ) < 0.01 &&
573
- runtime.lastVisibleIds.size > 0;
574
-
575
- if (canAttemptIncrementalFullResPan && currentRange) {
576
- const prevRect = previousVisibilityRect;
577
- const epsilon = 0.01;
578
- const incrementalBands = [];
579
-
580
- if (currentEnterRect.left < Number(prevRect.left) - epsilon) {
581
- incrementalBands.push({
582
- left: currentEnterRect.left,
583
- right: Math.min(currentEnterRect.right, Number(prevRect.left)),
584
- bottom: currentEnterRect.bottom,
585
- top: currentEnterRect.top
586
- });
587
- }
588
- if (currentEnterRect.right > Number(prevRect.right) + epsilon) {
589
- incrementalBands.push({
590
- left: Math.max(currentEnterRect.left, Number(prevRect.right)),
591
- right: currentEnterRect.right,
592
- bottom: currentEnterRect.bottom,
593
- top: currentEnterRect.top
594
- });
595
- }
596
- if (currentEnterRect.bottom < Number(prevRect.bottom) - epsilon) {
597
- incrementalBands.push({
598
- left: currentEnterRect.left,
599
- right: currentEnterRect.right,
600
- bottom: currentEnterRect.bottom,
601
- top: Math.min(currentEnterRect.top, Number(prevRect.bottom))
602
- });
603
- }
604
- if (currentEnterRect.top > Number(prevRect.top) + epsilon) {
605
- incrementalBands.push({
606
- left: currentEnterRect.left,
607
- right: currentEnterRect.right,
608
- bottom: Math.max(currentEnterRect.bottom, Number(prevRect.top)),
609
- top: currentEnterRect.top
610
- });
611
- }
612
-
613
- const incrementalCellKeys = new Set();
614
- for (let i = 0; i < incrementalBands.length; i++) {
615
- const bandRange = describeCellRange(incrementalBands[i], cellSize);
616
- if (!bandRange || bandRange.directGridLookups <= 0) {
617
- continue;
618
- }
619
-
620
- incrementalPanBandCount++;
621
- buildCellKeySetFromRange(bandRange, incrementalCellKeys);
622
- }
623
-
624
- if (incrementalPanBandCount > 0 &&
625
- incrementalCellKeys.size > 0 &&
626
- incrementalCellKeys.size < directGridLookups) {
627
- usedIncrementalPanVisibility = true;
628
- incrementalPanCellLookups = incrementalCellKeys.size;
629
-
630
- runtime.lastVisibleIds.forEach((nodeId) => {
631
- if (runtime.nodes.has(nodeId)) {
632
- nextVisibleIds.add(nodeId);
633
- }
634
- });
635
- addCandidatesFromCellKeySet(incrementalCellKeys, candidateIds, true);
636
- }
637
- }
638
-
639
- if (!usedIncrementalPanVisibility) {
640
- const result = collectCandidates(currentRange);
641
- candidateIds = result.candidateIds;
642
- usedPopulatedCellScan = result.usedPopulatedCellScan;
643
- directGridLookups = result.directGridLookups;
644
- gridSpanX = result.gridSpanX;
645
- gridSpanY = result.gridSpanY;
646
- }
647
-
648
- candidateIds.forEach((nodeId) => {
649
- const node = runtime.nodes.get(nodeId);
650
- if (!node) {
651
- return;
652
- }
653
-
654
- const depth = camZ - node.z;
655
- if (depth <= camNear || depth >= camFar) {
656
- return;
657
- }
658
-
659
- const baseMarginWorld = runtime.lastVisibleIds.has(nodeId)
660
- ? exitMarginWorld
661
- : enterMarginWorld;
662
- const leftBound = viewportLeft - (baseMarginWorld + horizontalLeadMarginLeftWorld);
663
- const rightBound = viewportRight + (baseMarginWorld + horizontalLeadMarginRightWorld);
664
- const bottomBound = viewportBottom - (baseMarginWorld + verticalLeadMarginBottomWorld);
665
- const topBound = viewportTop + (baseMarginWorld + verticalLeadMarginTopWorld);
666
-
667
- const isVisible = !(
668
- node.maxX < leftBound ||
669
- node.minX > rightBound ||
670
- node.maxY < bottomBound ||
671
- node.minY > topBound
672
- );
673
-
674
- if (isVisible) {
675
- nextVisibleIds.add(nodeId);
676
- }
677
- });
678
-
679
- const enteredIds = [];
680
- nextVisibleIds.forEach((nodeId) => {
681
- if (!runtime.lastVisibleIds.has(nodeId)) {
682
- enteredIds.push(nodeId);
683
- }
684
- });
685
-
686
- const exitedIds = [];
687
- runtime.lastVisibleIds.forEach((nodeId) => {
688
- if (!usedIncrementalPanVisibility && !nextVisibleIds.has(nodeId)) {
689
- exitedIds.push(nodeId);
690
- }
691
- });
692
-
693
- const visibilityChanged = enteredIds.length > 0 || exitedIds.length > 0;
694
- const canReusePriorityIds =
695
- visibilityChanged !== true &&
696
- Array.isArray(runtime.lastPriorityIds) &&
697
- runtime.lastPriorityIds.length === nextVisibleIds.size;
698
- const priorityIds = canReusePriorityIds
699
- ? runtime.lastPriorityIds
700
- : (usedIncrementalPanVisibility
701
- ? buildIncrementalPriorityIds(nextVisibleIds, enteredIds, camX, camY)
702
- : buildPriorityIds(nextVisibleIds, camX, camY));
703
-
704
- runtime.lastVisibleIds = nextVisibleIds;
705
- runtime.lastPriorityIds = priorityIds;
706
- runtime.lastVisibilityRect = {
707
- left: currentEnterRect.left,
708
- right: currentEnterRect.right,
709
- bottom: currentEnterRect.bottom,
710
- top: currentEnterRect.top,
711
- mode: isFullResMode === true ? 'full-res' : 'lod',
712
- cameraZ: camZ
713
- };
714
- runtime.lastNodeCount = runtime.nodes.size;
715
- runtime.lastResultSeq = seq;
716
-
717
- const t1 = (typeof performance !== 'undefined' && typeof performance.now === 'function')
718
- ? performance.now()
719
- : Date.now();
720
-
721
- const canOmitFullVisibleIds =
722
- mainLastAppliedSeq > 0 &&
723
- mainLastAppliedSeq === previousResultSeq &&
724
- seq === previousResultSeq + 1;
725
-
726
- self.postMessage({
727
- type: 'VISIBILITY_RESULT',
728
- boardId,
729
- seq,
730
- visibleIds: canOmitFullVisibleIds ? undefined : Array.from(nextVisibleIds),
731
- visibleCount: Number(nextVisibleIds.size || 0),
732
- enteredIds,
733
- exitedIds,
734
- priorityIds: canReusePriorityIds ? undefined : priorityIds,
735
- visibilityChanged,
736
- viewportRect: runtime.lastVisibilityRect,
737
- lastVisibilityNodeCount: runtime.lastNodeCount,
738
- fullResVisibilityReconcilePending: usedIncrementalPanVisibility === true,
739
- profile: {
740
- durationMs: Number(Math.max(0, t1 - t0).toFixed(2)),
741
- cameraZ: Number((camZ || 0).toFixed(2)),
742
- useCoarseVisibility: marginPolicy.useCoarseVisibility === true,
743
- marginWorld: Number((marginWorld || 0).toFixed(2)),
744
- gridSpanX: Number(gridSpanX || 0),
745
- gridSpanY: Number(gridSpanY || 0),
746
- directGridLookups: Number(directGridLookups || 0),
747
- usedPopulatedCellScan: usedPopulatedCellScan === true,
748
- incrementalPanMode: usedIncrementalPanVisibility === true,
749
- incrementalPanBandCount,
750
- incrementalPanCellLookups,
751
- omittedFullVisibleIds: canOmitFullVisibleIds === true,
752
- candidateCount: Number(candidateIds.size || 0),
753
- processedCount: Number(candidateIds.size || 0),
754
- prevVisibleCount: Number(previousVisibleCount || 0),
755
- nextVisibleCount: Number(nextVisibleIds.size || 0),
756
- visibilityChanged: visibilityChanged === true
757
- }
758
- });
759
- }
760
-
761
- function handleHitTestQuery(message) {
762
- const boardId = normalizeBoardId(message.boardId);
763
- if (!boardId || boardId !== runtime.boardId) {
764
- return;
765
- }
766
-
767
- const seq = Number(message.seq || 0);
768
- const cellX = Math.floor(Number(message.cellX || 0));
769
- const cellY = Math.floor(Number(message.cellY || 0));
770
- const t0 = (typeof performance !== 'undefined' && typeof performance.now === 'function')
771
- ? performance.now()
772
- : Date.now();
773
- const { candidateIds, cellLookups, cellRadius } = collectHitTestCandidateIds(
774
- cellX,
775
- cellY,
776
- message.cellRadius
777
- );
778
- const t1 = (typeof performance !== 'undefined' && typeof performance.now === 'function')
779
- ? performance.now()
780
- : Date.now();
781
-
782
- self.postMessage({
783
- type: 'HIT_TEST_RESULT',
784
- boardId,
785
- seq,
786
- cellX,
787
- cellY,
788
- cellRadius,
789
- rangeKey: `${cellX}:${cellY}:${cellRadius}`,
790
- candidateIds: Array.from(candidateIds),
791
- profile: {
792
- durationMs: Number(Math.max(0, t1 - t0).toFixed(2)),
793
- cellLookups,
794
- candidateCount: Number(candidateIds.size || 0),
795
- spatialGridCellCount: Number(runtime.spatialGrid.size || 0)
796
- }
797
- });
798
- }
799
-
800
- function handleSelectionQuery(message) {
801
- const boardId = normalizeBoardId(message.boardId);
802
- if (!boardId || boardId !== runtime.boardId) {
803
- return;
804
- }
805
-
806
- const rect = normalizeSelectionRect(message.selectionRect);
807
- if (!rect) {
808
- return;
809
- }
810
-
811
- const seq = Number(message.seq || 0);
812
- const t0 = (typeof performance !== 'undefined' && typeof performance.now === 'function')
813
- ? performance.now()
814
- : Date.now();
815
- const range = describeCellRange(rect, Math.max(1, Number(message.cellSize || runtime.cellSize || 500)));
816
- const { candidateIds, usedPopulatedCellScan, directGridLookups, gridSpanX, gridSpanY } = collectCandidates(range);
817
- const selectedIds = [];
818
-
819
- candidateIds.forEach((nodeId) => {
820
- const node = runtime.nodes.get(nodeId);
821
- if (!node) {
822
- return;
823
- }
824
-
825
- if (node.minX >= rect.left &&
826
- node.maxX <= rect.right &&
827
- node.minY >= rect.bottom &&
828
- node.maxY <= rect.top) {
829
- selectedIds.push(nodeId);
830
- }
831
- });
832
-
833
- const t1 = (typeof performance !== 'undefined' && typeof performance.now === 'function')
834
- ? performance.now()
835
- : Date.now();
836
-
837
- self.postMessage({
838
- type: 'SELECTION_QUERY_RESULT',
839
- boardId,
840
- seq,
841
- selectedIds,
842
- selectionRect: rect,
843
- profile: {
844
- durationMs: Number(Math.max(0, t1 - t0).toFixed(2)),
845
- candidateCount: Number(candidateIds.size || 0),
846
- selectedCount: Number(selectedIds.length || 0),
847
- gridSpanX: Number(gridSpanX || 0),
848
- gridSpanY: Number(gridSpanY || 0),
849
- directGridLookups: Number(directGridLookups || 0),
850
- usedPopulatedCellScan: usedPopulatedCellScan === true
851
- }
852
- });
853
- }
854
-
855
- self.onmessage = function (event) {
856
- const message = event?.data || null;
857
- if (!message || typeof message !== 'object') {
858
- return;
859
- }
860
-
861
- switch (message.type) {
862
- case 'RESET_BOARD':
863
- clearState(message.boardId, message.cellSize);
864
- break;
865
- case 'REBUILD_BOARD':
866
- rebuildBoard(message.boardId, message.cellSize, message.nodes);
867
- break;
868
- case 'UPSERT_NODE':
869
- if (normalizeBoardId(message.boardId) === runtime.boardId) {
870
- upsertNode(message.node);
871
- }
872
- break;
873
- case 'REMOVE_NODE':
874
- if (normalizeBoardId(message.boardId) === runtime.boardId) {
875
- removeNode(message.nodeId);
876
- }
877
- break;
878
- case 'VISIBILITY_QUERY':
879
- handleVisibilityQuery(message);
880
- break;
881
- case 'HIT_TEST_QUERY':
882
- handleHitTestQuery(message);
883
- break;
884
- case 'SELECTION_QUERY':
885
- handleSelectionQuery(message);
886
- break;
887
- default:
888
- break;
889
- }
890
- };
891
-
892
- self.postMessage({
893
- type: 'READY',
894
- worker: 'visibility'
895
- });
1
+ 'use strict';
2
+
3
+ const FULL_RES_VISIBILITY_EXIT_MARGIN_SCALE = 1.8;
4
+ const MAX_DIRECT_GRID_LOOKUPS = 4096;
5
+ const DEFAULT_HIT_TEST_CELL_RADIUS = 1;
6
+ const MAX_HIT_TEST_CELL_RADIUS = 3;
7
+
8
+ function createState() {
9
+ return {
10
+ boardId: '',
11
+ cellSize: 500,
12
+ nodes: new Map(),
13
+ spatialGrid: new Map(),
14
+ lastVisibleIds: new Set(),
15
+ lastPriorityIds: [],
16
+ lastVisibilityRect: null,
17
+ lastNodeCount: 0,
18
+ lastResultSeq: 0
19
+ };
20
+ }
21
+
22
+ let runtime = createState();
23
+
24
+ function normalizeBoardId(boardId) {
25
+ return String(boardId ?? '').trim();
26
+ }
27
+
28
+ function normalizeNodeSnapshot(node) {
29
+ if (!node || typeof node !== 'object') {
30
+ return null;
31
+ }
32
+
33
+ const id = String(node.id || '').trim();
34
+ if (!id) {
35
+ return null;
36
+ }
37
+
38
+ const minX = Number(node.minX || 0);
39
+ const maxX = Number(node.maxX || minX);
40
+ const minY = Number(node.minY || 0);
41
+ const maxY = Number(node.maxY || minY);
42
+
43
+ return {
44
+ id,
45
+ contentType: String(node.contentType || '').trim().toLowerCase(),
46
+ textLike: node.textLike === true,
47
+ x: Number(node.x || minX),
48
+ y: Number(node.y || maxY),
49
+ z: Number(node.z || 0),
50
+ width: Math.max(1, Number(node.width || (maxX - minX) || 1)),
51
+ height: Math.max(1, Number(node.height || (maxY - minY) || 1)),
52
+ minX,
53
+ maxX,
54
+ minY,
55
+ maxY,
56
+ cellKeys: Array.isArray(node.cellKeys)
57
+ ? node.cellKeys.map((key) => String(key || '').trim()).filter(Boolean)
58
+ : []
59
+ };
60
+ }
61
+
62
+ function clearState(boardId, cellSize) {
63
+ runtime = createState();
64
+ runtime.boardId = normalizeBoardId(boardId);
65
+ runtime.cellSize = Math.max(1, Number(cellSize || 500));
66
+ }
67
+
68
+ function removeNodeFromGrid(nodeId, node) {
69
+ const normalizedId = String(nodeId || '').trim();
70
+ if (!normalizedId || !node?.cellKeys) {
71
+ return;
72
+ }
73
+
74
+ for (let i = 0; i < node.cellKeys.length; i++) {
75
+ const key = node.cellKeys[i];
76
+ const cell = runtime.spatialGrid.get(key);
77
+ if (!cell) {
78
+ continue;
79
+ }
80
+
81
+ cell.delete(normalizedId);
82
+ if (cell.size === 0) {
83
+ runtime.spatialGrid.delete(key);
84
+ }
85
+ }
86
+ }
87
+
88
+ function upsertNode(node) {
89
+ const normalizedNode = normalizeNodeSnapshot(node);
90
+ if (!normalizedNode) {
91
+ return;
92
+ }
93
+
94
+ const existing = runtime.nodes.get(normalizedNode.id);
95
+ if (existing) {
96
+ removeNodeFromGrid(normalizedNode.id, existing);
97
+ }
98
+
99
+ runtime.nodes.set(normalizedNode.id, normalizedNode);
100
+ for (let i = 0; i < normalizedNode.cellKeys.length; i++) {
101
+ const key = normalizedNode.cellKeys[i];
102
+ let cell = runtime.spatialGrid.get(key);
103
+ if (!cell) {
104
+ cell = new Set();
105
+ runtime.spatialGrid.set(key, cell);
106
+ }
107
+ cell.add(normalizedNode.id);
108
+ }
109
+ }
110
+
111
+ function removeNode(nodeId) {
112
+ const normalizedId = String(nodeId || '').trim();
113
+ if (!normalizedId) {
114
+ return;
115
+ }
116
+
117
+ const existing = runtime.nodes.get(normalizedId);
118
+ if (!existing) {
119
+ return;
120
+ }
121
+
122
+ removeNodeFromGrid(normalizedId, existing);
123
+ runtime.nodes.delete(normalizedId);
124
+ runtime.lastVisibleIds.delete(normalizedId);
125
+ }
126
+
127
+ function rebuildBoard(boardId, cellSize, nodes) {
128
+ clearState(boardId, cellSize);
129
+ const sourceNodes = Array.isArray(nodes) ? nodes : [];
130
+ for (let i = 0; i < sourceNodes.length; i++) {
131
+ upsertNode(sourceNodes[i]);
132
+ }
133
+ }
134
+
135
+ function getVisibilityMarginPolicy(cameraZ, isPanning = false, isCtrlWheelVerticalScrolling = false) {
136
+ let cullingMarginPx = 3000;
137
+ let maxMarginCells = 6;
138
+ let useCoarseVisibility = false;
139
+
140
+ if (cameraZ < 4500) {
141
+ cullingMarginPx = 1000;
142
+ maxMarginCells = 4;
143
+ } else if (cameraZ < 12000) {
144
+ cullingMarginPx = 2200;
145
+ maxMarginCells = 5;
146
+ } else if (cameraZ < 30000) {
147
+ cullingMarginPx = 900;
148
+ maxMarginCells = 2;
149
+ } else {
150
+ cullingMarginPx = 240;
151
+ maxMarginCells = 1;
152
+ useCoarseVisibility = isPanning !== true;
153
+ }
154
+
155
+ if (isPanning === true) {
156
+ if (cameraZ < 4500) {
157
+ cullingMarginPx += 500;
158
+ maxMarginCells += 1;
159
+ } else if (cameraZ < 12000) {
160
+ cullingMarginPx += 700;
161
+ maxMarginCells += 1;
162
+ } else if (cameraZ < 30000) {
163
+ cullingMarginPx += 600;
164
+ maxMarginCells += 1;
165
+ } else {
166
+ cullingMarginPx += 320;
167
+ maxMarginCells += 1;
168
+ }
169
+ }
170
+
171
+ if (isCtrlWheelVerticalScrolling && cameraZ < 12000) {
172
+ cullingMarginPx += (cameraZ < 4500) ? 700 : 900;
173
+ maxMarginCells += 2;
174
+ }
175
+
176
+ return {
177
+ cullingMarginPx,
178
+ maxMarginCells,
179
+ useCoarseVisibility
180
+ };
181
+ }
182
+
183
+ function describeCellRange(rect, cellSize) {
184
+ if (!rect) {
185
+ return null;
186
+ }
187
+
188
+ const left = Number(rect.left);
189
+ const right = Number(rect.right);
190
+ const bottom = Number(rect.bottom);
191
+ const top = Number(rect.top);
192
+ if (!Number.isFinite(left) || !Number.isFinite(right) || !Number.isFinite(bottom) || !Number.isFinite(top)) {
193
+ return null;
194
+ }
195
+ if (right < left || top < bottom) {
196
+ return null;
197
+ }
198
+
199
+ const startCellX = Math.floor(left / cellSize);
200
+ const endCellX = Math.floor(right / cellSize);
201
+ const startCellY = Math.floor(bottom / cellSize);
202
+ const endCellY = Math.floor(top / cellSize);
203
+ const spanX = Math.max(0, endCellX - startCellX + 1);
204
+ const spanY = Math.max(0, endCellY - startCellY + 1);
205
+
206
+ return {
207
+ startCellX,
208
+ endCellX,
209
+ startCellY,
210
+ endCellY,
211
+ spanX,
212
+ spanY,
213
+ directGridLookups: spanX * spanY
214
+ };
215
+ }
216
+
217
+ function intersectsRect(node, rect) {
218
+ if (!node || !rect) {
219
+ return false;
220
+ }
221
+
222
+ return !(
223
+ node.maxX < rect.left ||
224
+ node.minX > rect.right ||
225
+ node.maxY < rect.bottom ||
226
+ node.minY > rect.top
227
+ );
228
+ }
229
+
230
+ function collectCandidates(range) {
231
+ const candidateIds = new Set();
232
+ let usedPopulatedCellScan = false;
233
+
234
+ if (!range) {
235
+ runtime.nodes.forEach((_, nodeId) => candidateIds.add(nodeId));
236
+ return {
237
+ candidateIds,
238
+ usedPopulatedCellScan,
239
+ directGridLookups: 0,
240
+ gridSpanX: 0,
241
+ gridSpanY: 0
242
+ };
243
+ }
244
+
245
+ if (range.directGridLookups > MAX_DIRECT_GRID_LOOKUPS) {
246
+ usedPopulatedCellScan = true;
247
+ runtime.spatialGrid.forEach((nodeIds, key) => {
248
+ const separatorIndex = key.indexOf(':');
249
+ if (separatorIndex <= 0) {
250
+ return;
251
+ }
252
+
253
+ const cx = Number(key.slice(0, separatorIndex));
254
+ const cy = Number(key.slice(separatorIndex + 1));
255
+ if (!Number.isFinite(cx) || !Number.isFinite(cy)) {
256
+ return;
257
+ }
258
+
259
+ if (cx >= range.startCellX && cx <= range.endCellX && cy >= range.startCellY && cy <= range.endCellY) {
260
+ nodeIds.forEach((nodeId) => candidateIds.add(nodeId));
261
+ }
262
+ });
263
+ } else {
264
+ for (let cx = range.startCellX; cx <= range.endCellX; cx++) {
265
+ for (let cy = range.startCellY; cy <= range.endCellY; cy++) {
266
+ const cell = runtime.spatialGrid.get(`${cx}:${cy}`);
267
+ if (!cell) {
268
+ continue;
269
+ }
270
+ cell.forEach((nodeId) => candidateIds.add(nodeId));
271
+ }
272
+ }
273
+ }
274
+
275
+ runtime.lastVisibleIds.forEach((nodeId) => {
276
+ if (runtime.nodes.has(nodeId)) {
277
+ candidateIds.add(nodeId);
278
+ }
279
+ });
280
+
281
+ return {
282
+ candidateIds,
283
+ usedPopulatedCellScan,
284
+ directGridLookups: range.directGridLookups,
285
+ gridSpanX: range.spanX,
286
+ gridSpanY: range.spanY
287
+ };
288
+ }
289
+
290
+ function buildCellKeySetFromRange(range, targetSet) {
291
+ if (!range || !targetSet) {
292
+ return;
293
+ }
294
+
295
+ for (let cx = range.startCellX; cx <= range.endCellX; cx++) {
296
+ for (let cy = range.startCellY; cy <= range.endCellY; cy++) {
297
+ targetSet.add(`${cx}:${cy}`);
298
+ }
299
+ }
300
+ }
301
+
302
+ function addCandidatesFromCellKeySet(cellKeys, candidateIds, skipPreviouslyVisible = false) {
303
+ if (!cellKeys || !candidateIds || cellKeys.size === 0) {
304
+ return;
305
+ }
306
+
307
+ cellKeys.forEach((key) => {
308
+ const nodeIds = runtime.spatialGrid.get(key);
309
+ if (!nodeIds) {
310
+ return;
311
+ }
312
+
313
+ nodeIds.forEach((nodeId) => {
314
+ if (skipPreviouslyVisible && runtime.lastVisibleIds.has(nodeId)) {
315
+ return;
316
+ }
317
+
318
+ candidateIds.add(nodeId);
319
+ });
320
+ });
321
+ }
322
+
323
+ function normalizeHitTestCellRadius(cellRadius) {
324
+ const normalized = Math.round(Number(cellRadius || DEFAULT_HIT_TEST_CELL_RADIUS));
325
+ if (!Number.isFinite(normalized)) {
326
+ return DEFAULT_HIT_TEST_CELL_RADIUS;
327
+ }
328
+
329
+ return Math.max(0, Math.min(MAX_HIT_TEST_CELL_RADIUS, normalized));
330
+ }
331
+
332
+ function collectHitTestCandidateIds(centerCellX, centerCellY, cellRadius) {
333
+ const candidateIds = new Set();
334
+ const normalizedCellRadius = normalizeHitTestCellRadius(cellRadius);
335
+ let cellLookups = 0;
336
+
337
+ for (let offsetX = -normalizedCellRadius; offsetX <= normalizedCellRadius; offsetX++) {
338
+ for (let offsetY = -normalizedCellRadius; offsetY <= normalizedCellRadius; offsetY++) {
339
+ cellLookups++;
340
+ const cell = runtime.spatialGrid.get(`${centerCellX + offsetX}:${centerCellY + offsetY}`);
341
+ if (!cell) {
342
+ continue;
343
+ }
344
+
345
+ cell.forEach((nodeId) => candidateIds.add(nodeId));
346
+ }
347
+ }
348
+
349
+ return {
350
+ candidateIds,
351
+ cellLookups,
352
+ cellRadius: normalizedCellRadius
353
+ };
354
+ }
355
+
356
+ function normalizeSelectionRect(rect) {
357
+ if (!rect || typeof rect !== 'object') {
358
+ return null;
359
+ }
360
+
361
+ const left = Number(rect.left);
362
+ const right = Number(rect.right);
363
+ const bottom = Number(rect.bottom);
364
+ const top = Number(rect.top);
365
+ if (!Number.isFinite(left) || !Number.isFinite(right) || !Number.isFinite(bottom) || !Number.isFinite(top)) {
366
+ return null;
367
+ }
368
+
369
+ return {
370
+ left: Math.min(left, right),
371
+ right: Math.max(left, right),
372
+ bottom: Math.min(bottom, top),
373
+ top: Math.max(bottom, top)
374
+ };
375
+ }
376
+
377
+ function buildPriorityIds(visibleIds, camX, camY) {
378
+ const buffer = [];
379
+ visibleIds.forEach((nodeId) => {
380
+ const node = runtime.nodes.get(nodeId);
381
+ if (!node) {
382
+ return;
383
+ }
384
+
385
+ const dx = Number(node.x || 0) - camX;
386
+ const dy = Number(node.y || 0) - camY;
387
+ const distSq = (dx * dx) + (dy * dy);
388
+ const textPriority = node.textLike === true ? 0 : 1;
389
+ const mediaPriority = node.contentType === 'image' ? 0 : 1;
390
+ buffer.push({ nodeId, textPriority, mediaPriority, distSq });
391
+ });
392
+
393
+ buffer.sort((a, b) => {
394
+ if (a.textPriority !== b.textPriority) {
395
+ return a.textPriority - b.textPriority;
396
+ }
397
+ if (a.mediaPriority !== b.mediaPriority) {
398
+ return a.mediaPriority - b.mediaPriority;
399
+ }
400
+ return a.distSq - b.distSq;
401
+ });
402
+
403
+ return buffer.map((entry) => entry.nodeId);
404
+ }
405
+
406
+ function buildIncrementalPriorityIds(nextVisibleIds, enteredIds, camX, camY) {
407
+ const priorityIds = [];
408
+ const used = new Set();
409
+
410
+ if (Array.isArray(runtime.lastPriorityIds)) {
411
+ for (let i = 0; i < runtime.lastPriorityIds.length; i++) {
412
+ const nodeId = String(runtime.lastPriorityIds[i] || '').trim();
413
+ if (!nodeId || used.has(nodeId) || !nextVisibleIds.has(nodeId)) {
414
+ continue;
415
+ }
416
+
417
+ priorityIds.push(nodeId);
418
+ used.add(nodeId);
419
+ }
420
+ }
421
+
422
+ const enteredSet = new Set();
423
+ if (Array.isArray(enteredIds)) {
424
+ for (let i = 0; i < enteredIds.length; i++) {
425
+ const nodeId = String(enteredIds[i] || '').trim();
426
+ if (nodeId && nextVisibleIds.has(nodeId) && !used.has(nodeId)) {
427
+ enteredSet.add(nodeId);
428
+ }
429
+ }
430
+ }
431
+
432
+ if (enteredSet.size > 0) {
433
+ const enteredPriorityIds = buildPriorityIds(enteredSet, camX, camY);
434
+ for (let i = 0; i < enteredPriorityIds.length; i++) {
435
+ const nodeId = enteredPriorityIds[i];
436
+ if (!used.has(nodeId)) {
437
+ priorityIds.push(nodeId);
438
+ used.add(nodeId);
439
+ }
440
+ }
441
+ }
442
+
443
+ if (priorityIds.length !== nextVisibleIds.size) {
444
+ return buildPriorityIds(nextVisibleIds, camX, camY);
445
+ }
446
+
447
+ return priorityIds;
448
+ }
449
+
450
+ function handleVisibilityQuery(message) {
451
+ const boardId = normalizeBoardId(message.boardId);
452
+ if (!boardId || boardId !== runtime.boardId) {
453
+ return;
454
+ }
455
+
456
+ const t0 = (typeof performance !== 'undefined' && typeof performance.now === 'function')
457
+ ? performance.now()
458
+ : Date.now();
459
+
460
+ const camera = message.camera || {};
461
+ const state = message.state || {};
462
+ const seq = Number(message.seq || 0);
463
+ const previousResultSeq = Math.max(0, Math.floor(Number(runtime.lastResultSeq || 0)));
464
+ const mainLastAppliedSeq = Math.max(0, Math.floor(Number(message.mainLastAppliedSeq || 0)));
465
+ const cellSize = Math.max(1, Number(message.cellSize || runtime.cellSize || 500));
466
+ const camX = Number(camera.x || 0);
467
+ const camY = Number(camera.y || 0);
468
+ const camZ = Number(camera.z || 0);
469
+ const camNear = Number(camera.near || 0);
470
+ const camFar = Number(camera.far || Number.MAX_SAFE_INTEGER);
471
+ const viewportW = Math.max(1, Number(camera.viewportW || 1));
472
+ const viewportH = Math.max(1, Number(camera.viewportH || 1));
473
+ const aspect = Math.max(0.1, Number(camera.aspect || 1));
474
+ const fov = Number(camera.fov || 45);
475
+ const isPanning = state.isPanning === true;
476
+ const isZooming = state.isZooming === true;
477
+ const isFullResMode = state.isFullResMode === true;
478
+ const isCtrlWheelVerticalScrolling = state.isCtrlWheelVerticalScrolling === true;
479
+
480
+ const marginPolicy = getVisibilityMarginPolicy(camZ, isPanning, isCtrlWheelVerticalScrolling);
481
+ let marginWorld = 0;
482
+ let verticalLeadMarginTopWorld = 0;
483
+ let verticalLeadMarginBottomWorld = 0;
484
+ let horizontalLeadMarginLeftWorld = 0;
485
+ let horizontalLeadMarginRightWorld = 0;
486
+
487
+ const vfov = (fov * Math.PI) / 180;
488
+ const viewHeight = 2 * Math.tan(vfov / 2) * Math.max(1, camZ);
489
+ const viewWidth = viewHeight * aspect;
490
+ const worldPerPxX = viewWidth / viewportW;
491
+ const worldPerPxY = viewHeight / viewportH;
492
+ const rawMarginWorld = marginPolicy.cullingMarginPx * Math.max(worldPerPxX, worldPerPxY);
493
+ marginWorld = Math.min(rawMarginWorld, cellSize * marginPolicy.maxMarginCells);
494
+
495
+ if (isCtrlWheelVerticalScrolling && camZ < 12000) {
496
+ const directionalLeadWorld = Math.min(
497
+ rawMarginWorld * 0.8,
498
+ cellSize * Math.max(1, marginPolicy.maxMarginCells - 1)
499
+ );
500
+ const lastCtrlWheelDeltaY = Number(state.lastCtrlWheelDeltaY || 0);
501
+ if (lastCtrlWheelDeltaY < -0.01) {
502
+ verticalLeadMarginTopWorld = directionalLeadWorld;
503
+ } else if (lastCtrlWheelDeltaY > 0.01) {
504
+ verticalLeadMarginBottomWorld = directionalLeadWorld;
505
+ } else {
506
+ verticalLeadMarginTopWorld = directionalLeadWorld * 0.5;
507
+ verticalLeadMarginBottomWorld = directionalLeadWorld * 0.5;
508
+ }
509
+ }
510
+
511
+ if (isPanning && camZ < 12000) {
512
+ const panLeadWorld = Math.min(
513
+ rawMarginWorld * (camZ < 4500 ? 1.35 : 1.1),
514
+ cellSize * Math.max(1, marginPolicy.maxMarginCells)
515
+ );
516
+ const panDx = Number(state.lastPanWorldDeltaX || 0);
517
+ const panDy = Number(state.lastPanWorldDeltaY || 0);
518
+ if (panDx > 0.01) {
519
+ horizontalLeadMarginRightWorld = panLeadWorld;
520
+ } else if (panDx < -0.01) {
521
+ horizontalLeadMarginLeftWorld = panLeadWorld;
522
+ }
523
+ if (panDy > 0.01) {
524
+ verticalLeadMarginTopWorld = Math.max(verticalLeadMarginTopWorld, panLeadWorld);
525
+ } else if (panDy < -0.01) {
526
+ verticalLeadMarginBottomWorld = Math.max(verticalLeadMarginBottomWorld, panLeadWorld);
527
+ }
528
+ }
529
+
530
+ const halfVisibleW = viewWidth / 2;
531
+ const halfVisibleH = viewHeight / 2;
532
+ const viewportLeft = camX - halfVisibleW;
533
+ const viewportRight = camX + halfVisibleW;
534
+ const viewportBottom = camY - halfVisibleH;
535
+ const viewportTop = camY + halfVisibleH;
536
+ const enterMarginWorld = marginWorld;
537
+ const exitMarginWorld = marginWorld * FULL_RES_VISIBILITY_EXIT_MARGIN_SCALE;
538
+
539
+ const currentEnterRect = {
540
+ left: viewportLeft - (enterMarginWorld + horizontalLeadMarginLeftWorld),
541
+ right: viewportRight + (enterMarginWorld + horizontalLeadMarginRightWorld),
542
+ bottom: viewportBottom - (enterMarginWorld + verticalLeadMarginBottomWorld),
543
+ top: viewportTop + (enterMarginWorld + verticalLeadMarginTopWorld)
544
+ };
545
+ const currentPreserveRect = {
546
+ left: viewportLeft - (exitMarginWorld + horizontalLeadMarginLeftWorld),
547
+ right: viewportRight + (exitMarginWorld + horizontalLeadMarginRightWorld),
548
+ bottom: viewportBottom - (exitMarginWorld + verticalLeadMarginBottomWorld),
549
+ top: viewportTop + (exitMarginWorld + verticalLeadMarginTopWorld)
550
+ };
551
+
552
+ const previousVisibilityRect = runtime.lastVisibilityRect || null;
553
+ const currentRange = describeCellRange(currentEnterRect, cellSize);
554
+ let candidateIds = new Set();
555
+ let usedPopulatedCellScan = false;
556
+ let directGridLookups = Number(currentRange?.directGridLookups || 0);
557
+ let gridSpanX = Number(currentRange?.spanX || 0);
558
+ let gridSpanY = Number(currentRange?.spanY || 0);
559
+ const previousVisibleCount = runtime.lastVisibleIds.size;
560
+ const nextVisibleIds = new Set();
561
+ let usedIncrementalPanVisibility = false;
562
+ let incrementalPanBandCount = 0;
563
+ let incrementalPanCellLookups = 0;
564
+ const canAttemptIncrementalFullResPan =
565
+ isFullResMode === true &&
566
+ isPanning === true &&
567
+ isZooming !== true &&
568
+ state.isWindowResizing !== true &&
569
+ state.isResizing !== true &&
570
+ previousVisibilityRect?.mode === 'full-res' &&
571
+ runtime.lastNodeCount === runtime.nodes.size &&
572
+ Math.abs(Number(previousVisibilityRect?.cameraZ || 0) - camZ) < 0.01 &&
573
+ runtime.lastVisibleIds.size > 0;
574
+
575
+ if (canAttemptIncrementalFullResPan && currentRange) {
576
+ const prevRect = previousVisibilityRect;
577
+ const epsilon = 0.01;
578
+ const incrementalBands = [];
579
+
580
+ if (currentEnterRect.left < Number(prevRect.left) - epsilon) {
581
+ incrementalBands.push({
582
+ left: currentEnterRect.left,
583
+ right: Math.min(currentEnterRect.right, Number(prevRect.left)),
584
+ bottom: currentEnterRect.bottom,
585
+ top: currentEnterRect.top
586
+ });
587
+ }
588
+ if (currentEnterRect.right > Number(prevRect.right) + epsilon) {
589
+ incrementalBands.push({
590
+ left: Math.max(currentEnterRect.left, Number(prevRect.right)),
591
+ right: currentEnterRect.right,
592
+ bottom: currentEnterRect.bottom,
593
+ top: currentEnterRect.top
594
+ });
595
+ }
596
+ if (currentEnterRect.bottom < Number(prevRect.bottom) - epsilon) {
597
+ incrementalBands.push({
598
+ left: currentEnterRect.left,
599
+ right: currentEnterRect.right,
600
+ bottom: currentEnterRect.bottom,
601
+ top: Math.min(currentEnterRect.top, Number(prevRect.bottom))
602
+ });
603
+ }
604
+ if (currentEnterRect.top > Number(prevRect.top) + epsilon) {
605
+ incrementalBands.push({
606
+ left: currentEnterRect.left,
607
+ right: currentEnterRect.right,
608
+ bottom: Math.max(currentEnterRect.bottom, Number(prevRect.top)),
609
+ top: currentEnterRect.top
610
+ });
611
+ }
612
+
613
+ const incrementalCellKeys = new Set();
614
+ for (let i = 0; i < incrementalBands.length; i++) {
615
+ const bandRange = describeCellRange(incrementalBands[i], cellSize);
616
+ if (!bandRange || bandRange.directGridLookups <= 0) {
617
+ continue;
618
+ }
619
+
620
+ incrementalPanBandCount++;
621
+ buildCellKeySetFromRange(bandRange, incrementalCellKeys);
622
+ }
623
+
624
+ if (incrementalPanBandCount > 0 &&
625
+ incrementalCellKeys.size > 0 &&
626
+ incrementalCellKeys.size < directGridLookups) {
627
+ usedIncrementalPanVisibility = true;
628
+ incrementalPanCellLookups = incrementalCellKeys.size;
629
+
630
+ runtime.lastVisibleIds.forEach((nodeId) => {
631
+ if (runtime.nodes.has(nodeId)) {
632
+ nextVisibleIds.add(nodeId);
633
+ }
634
+ });
635
+ addCandidatesFromCellKeySet(incrementalCellKeys, candidateIds, true);
636
+ }
637
+ }
638
+
639
+ if (!usedIncrementalPanVisibility) {
640
+ const result = collectCandidates(currentRange);
641
+ candidateIds = result.candidateIds;
642
+ usedPopulatedCellScan = result.usedPopulatedCellScan;
643
+ directGridLookups = result.directGridLookups;
644
+ gridSpanX = result.gridSpanX;
645
+ gridSpanY = result.gridSpanY;
646
+ }
647
+
648
+ candidateIds.forEach((nodeId) => {
649
+ const node = runtime.nodes.get(nodeId);
650
+ if (!node) {
651
+ return;
652
+ }
653
+
654
+ const depth = camZ - node.z;
655
+ if (depth <= camNear || depth >= camFar) {
656
+ return;
657
+ }
658
+
659
+ const baseMarginWorld = runtime.lastVisibleIds.has(nodeId)
660
+ ? exitMarginWorld
661
+ : enterMarginWorld;
662
+ const leftBound = viewportLeft - (baseMarginWorld + horizontalLeadMarginLeftWorld);
663
+ const rightBound = viewportRight + (baseMarginWorld + horizontalLeadMarginRightWorld);
664
+ const bottomBound = viewportBottom - (baseMarginWorld + verticalLeadMarginBottomWorld);
665
+ const topBound = viewportTop + (baseMarginWorld + verticalLeadMarginTopWorld);
666
+
667
+ const isVisible = !(
668
+ node.maxX < leftBound ||
669
+ node.minX > rightBound ||
670
+ node.maxY < bottomBound ||
671
+ node.minY > topBound
672
+ );
673
+
674
+ if (isVisible) {
675
+ nextVisibleIds.add(nodeId);
676
+ }
677
+ });
678
+
679
+ const enteredIds = [];
680
+ nextVisibleIds.forEach((nodeId) => {
681
+ if (!runtime.lastVisibleIds.has(nodeId)) {
682
+ enteredIds.push(nodeId);
683
+ }
684
+ });
685
+
686
+ const exitedIds = [];
687
+ runtime.lastVisibleIds.forEach((nodeId) => {
688
+ if (!usedIncrementalPanVisibility && !nextVisibleIds.has(nodeId)) {
689
+ exitedIds.push(nodeId);
690
+ }
691
+ });
692
+
693
+ const visibilityChanged = enteredIds.length > 0 || exitedIds.length > 0;
694
+ const canReusePriorityIds =
695
+ visibilityChanged !== true &&
696
+ Array.isArray(runtime.lastPriorityIds) &&
697
+ runtime.lastPriorityIds.length === nextVisibleIds.size;
698
+ const priorityIds = canReusePriorityIds
699
+ ? runtime.lastPriorityIds
700
+ : (usedIncrementalPanVisibility
701
+ ? buildIncrementalPriorityIds(nextVisibleIds, enteredIds, camX, camY)
702
+ : buildPriorityIds(nextVisibleIds, camX, camY));
703
+
704
+ runtime.lastVisibleIds = nextVisibleIds;
705
+ runtime.lastPriorityIds = priorityIds;
706
+ runtime.lastVisibilityRect = {
707
+ left: currentEnterRect.left,
708
+ right: currentEnterRect.right,
709
+ bottom: currentEnterRect.bottom,
710
+ top: currentEnterRect.top,
711
+ mode: isFullResMode === true ? 'full-res' : 'lod',
712
+ cameraZ: camZ
713
+ };
714
+ runtime.lastNodeCount = runtime.nodes.size;
715
+ runtime.lastResultSeq = seq;
716
+
717
+ const t1 = (typeof performance !== 'undefined' && typeof performance.now === 'function')
718
+ ? performance.now()
719
+ : Date.now();
720
+
721
+ const canOmitFullVisibleIds =
722
+ mainLastAppliedSeq > 0 &&
723
+ mainLastAppliedSeq === previousResultSeq &&
724
+ seq === previousResultSeq + 1;
725
+
726
+ self.postMessage({
727
+ type: 'VISIBILITY_RESULT',
728
+ boardId,
729
+ seq,
730
+ visibleIds: canOmitFullVisibleIds ? undefined : Array.from(nextVisibleIds),
731
+ visibleCount: Number(nextVisibleIds.size || 0),
732
+ enteredIds,
733
+ exitedIds,
734
+ priorityIds: canReusePriorityIds ? undefined : priorityIds,
735
+ visibilityChanged,
736
+ viewportRect: runtime.lastVisibilityRect,
737
+ lastVisibilityNodeCount: runtime.lastNodeCount,
738
+ fullResVisibilityReconcilePending: usedIncrementalPanVisibility === true,
739
+ profile: {
740
+ durationMs: Number(Math.max(0, t1 - t0).toFixed(2)),
741
+ cameraZ: Number((camZ || 0).toFixed(2)),
742
+ useCoarseVisibility: marginPolicy.useCoarseVisibility === true,
743
+ marginWorld: Number((marginWorld || 0).toFixed(2)),
744
+ gridSpanX: Number(gridSpanX || 0),
745
+ gridSpanY: Number(gridSpanY || 0),
746
+ directGridLookups: Number(directGridLookups || 0),
747
+ usedPopulatedCellScan: usedPopulatedCellScan === true,
748
+ incrementalPanMode: usedIncrementalPanVisibility === true,
749
+ incrementalPanBandCount,
750
+ incrementalPanCellLookups,
751
+ omittedFullVisibleIds: canOmitFullVisibleIds === true,
752
+ candidateCount: Number(candidateIds.size || 0),
753
+ processedCount: Number(candidateIds.size || 0),
754
+ prevVisibleCount: Number(previousVisibleCount || 0),
755
+ nextVisibleCount: Number(nextVisibleIds.size || 0),
756
+ visibilityChanged: visibilityChanged === true
757
+ }
758
+ });
759
+ }
760
+
761
+ function handleHitTestQuery(message) {
762
+ const boardId = normalizeBoardId(message.boardId);
763
+ if (!boardId || boardId !== runtime.boardId) {
764
+ return;
765
+ }
766
+
767
+ const seq = Number(message.seq || 0);
768
+ const cellX = Math.floor(Number(message.cellX || 0));
769
+ const cellY = Math.floor(Number(message.cellY || 0));
770
+ const t0 = (typeof performance !== 'undefined' && typeof performance.now === 'function')
771
+ ? performance.now()
772
+ : Date.now();
773
+ const { candidateIds, cellLookups, cellRadius } = collectHitTestCandidateIds(
774
+ cellX,
775
+ cellY,
776
+ message.cellRadius
777
+ );
778
+ const t1 = (typeof performance !== 'undefined' && typeof performance.now === 'function')
779
+ ? performance.now()
780
+ : Date.now();
781
+
782
+ self.postMessage({
783
+ type: 'HIT_TEST_RESULT',
784
+ boardId,
785
+ seq,
786
+ cellX,
787
+ cellY,
788
+ cellRadius,
789
+ rangeKey: `${cellX}:${cellY}:${cellRadius}`,
790
+ candidateIds: Array.from(candidateIds),
791
+ profile: {
792
+ durationMs: Number(Math.max(0, t1 - t0).toFixed(2)),
793
+ cellLookups,
794
+ candidateCount: Number(candidateIds.size || 0),
795
+ spatialGridCellCount: Number(runtime.spatialGrid.size || 0)
796
+ }
797
+ });
798
+ }
799
+
800
+ function handleSelectionQuery(message) {
801
+ const boardId = normalizeBoardId(message.boardId);
802
+ if (!boardId || boardId !== runtime.boardId) {
803
+ return;
804
+ }
805
+
806
+ const rect = normalizeSelectionRect(message.selectionRect);
807
+ if (!rect) {
808
+ return;
809
+ }
810
+
811
+ const seq = Number(message.seq || 0);
812
+ const t0 = (typeof performance !== 'undefined' && typeof performance.now === 'function')
813
+ ? performance.now()
814
+ : Date.now();
815
+ const range = describeCellRange(rect, Math.max(1, Number(message.cellSize || runtime.cellSize || 500)));
816
+ const { candidateIds, usedPopulatedCellScan, directGridLookups, gridSpanX, gridSpanY } = collectCandidates(range);
817
+ const selectedIds = [];
818
+
819
+ candidateIds.forEach((nodeId) => {
820
+ const node = runtime.nodes.get(nodeId);
821
+ if (!node) {
822
+ return;
823
+ }
824
+
825
+ if (node.minX >= rect.left &&
826
+ node.maxX <= rect.right &&
827
+ node.minY >= rect.bottom &&
828
+ node.maxY <= rect.top) {
829
+ selectedIds.push(nodeId);
830
+ }
831
+ });
832
+
833
+ const t1 = (typeof performance !== 'undefined' && typeof performance.now === 'function')
834
+ ? performance.now()
835
+ : Date.now();
836
+
837
+ self.postMessage({
838
+ type: 'SELECTION_QUERY_RESULT',
839
+ boardId,
840
+ seq,
841
+ selectedIds,
842
+ selectionRect: rect,
843
+ profile: {
844
+ durationMs: Number(Math.max(0, t1 - t0).toFixed(2)),
845
+ candidateCount: Number(candidateIds.size || 0),
846
+ selectedCount: Number(selectedIds.length || 0),
847
+ gridSpanX: Number(gridSpanX || 0),
848
+ gridSpanY: Number(gridSpanY || 0),
849
+ directGridLookups: Number(directGridLookups || 0),
850
+ usedPopulatedCellScan: usedPopulatedCellScan === true
851
+ }
852
+ });
853
+ }
854
+
855
+ self.onmessage = function (event) {
856
+ const message = event?.data || null;
857
+ if (!message || typeof message !== 'object') {
858
+ return;
859
+ }
860
+
861
+ switch (message.type) {
862
+ case 'RESET_BOARD':
863
+ clearState(message.boardId, message.cellSize);
864
+ break;
865
+ case 'REBUILD_BOARD':
866
+ rebuildBoard(message.boardId, message.cellSize, message.nodes);
867
+ break;
868
+ case 'UPSERT_NODE':
869
+ if (normalizeBoardId(message.boardId) === runtime.boardId) {
870
+ upsertNode(message.node);
871
+ }
872
+ break;
873
+ case 'REMOVE_NODE':
874
+ if (normalizeBoardId(message.boardId) === runtime.boardId) {
875
+ removeNode(message.nodeId);
876
+ }
877
+ break;
878
+ case 'VISIBILITY_QUERY':
879
+ handleVisibilityQuery(message);
880
+ break;
881
+ case 'HIT_TEST_QUERY':
882
+ handleHitTestQuery(message);
883
+ break;
884
+ case 'SELECTION_QUERY':
885
+ handleSelectionQuery(message);
886
+ break;
887
+ default:
888
+ break;
889
+ }
890
+ };
891
+
892
+ self.postMessage({
893
+ type: 'READY',
894
+ worker: 'visibility'
895
+ });