@mindexec/cli 0.2.445 → 0.2.447

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 (34) hide show
  1. package/README.md +2 -2
  2. package/package.json +6 -3
  3. package/remote-fast/osx-arm64/mindexec-remote-fast.dll +0 -0
  4. package/remote-fast/osx-x64/mindexec-remote-fast.dll +0 -0
  5. package/remote-fast/win-x64/mindexec-remote-fast.dll +0 -0
  6. package/remote-hub.js +171 -6
  7. package/scripts/remote-fast-live-rate-smoke.mjs +1 -0
  8. package/scripts/remote-fleet-render-smoke.mjs +74 -36
  9. package/scripts/remote-hub-smoke.mjs +7 -0
  10. package/server.js +2 -2
  11. package/wwwroot/_content/MindExecution.Shared/js/background-themes.js +1 -3
  12. package/wwwroot/_content/MindExecution.Shared/js/mind-map-core.js +199 -100
  13. package/wwwroot/_content/MindExecution.Shared/js/mind-map-css3d-hotpath.js +902 -0
  14. package/wwwroot/_content/MindExecution.Shared/js/mind-map-css3d-manager.js +186 -360
  15. package/wwwroot/_content/MindExecution.Shared/js/mind-map-interactions.js +124 -179
  16. package/wwwroot/_content/MindExecution.Shared/js/mind-map-lod-renderer.js +167 -24
  17. package/wwwroot/_content/MindExecution.Shared/js/mind-map-nodes.js +96 -74
  18. package/wwwroot/_content/MindExecution.Shared/js/mind-map-pipeline.js +66 -30
  19. package/wwwroot/_content/MindExecution.Shared/js/mind-map-remote-codecs.js +6 -3
  20. package/wwwroot/_content/MindExecution.Shared/js/renderers/CSS3DRenderer.js +35 -2
  21. package/wwwroot/_framework/{MindExecution.Core.4moacskf64.dll → MindExecution.Core.mswl4wlkm9.dll} +0 -0
  22. package/wwwroot/_framework/{MindExecution.Kernel.pa251cfsz4.dll → MindExecution.Kernel.zupeldfptg.dll} +0 -0
  23. package/wwwroot/_framework/{MindExecution.Plugins.Admin.dxyuwmxv1s.dll → MindExecution.Plugins.Admin.hq0vyqnqtn.dll} +0 -0
  24. package/wwwroot/_framework/{MindExecution.Plugins.Business.7uj5erqhhn.dll → MindExecution.Plugins.Business.pucw3o7rno.dll} +0 -0
  25. package/wwwroot/_framework/{MindExecution.Plugins.Concept.108rmsxc96.dll → MindExecution.Plugins.Concept.557tzzkrrx.dll} +0 -0
  26. package/wwwroot/_framework/{MindExecution.Plugins.Directory.ccsy3g0fe5.dll → MindExecution.Plugins.Directory.zh1ddg1fn6.dll} +0 -0
  27. package/wwwroot/_framework/{MindExecution.Plugins.PlanMaster.1vjpuoe9ff.dll → MindExecution.Plugins.PlanMaster.0z9pzynpu9.dll} +0 -0
  28. package/wwwroot/_framework/{MindExecution.Plugins.YouTube.q8ml98phj9.dll → MindExecution.Plugins.YouTube.y8g77r54fa.dll} +0 -0
  29. package/wwwroot/_framework/{MindExecution.Shared.2pywq19zjy.dll → MindExecution.Shared.1ymsdbwid2.dll} +0 -0
  30. package/wwwroot/_framework/{MindExecution.Web.qucbls1wfr.dll → MindExecution.Web.bb1txupuv0.dll} +0 -0
  31. package/wwwroot/_framework/blazor.boot.json +21 -21
  32. package/wwwroot/index.html +2 -1
  33. package/wwwroot/service-worker-assets.js +36 -32
  34. package/wwwroot/service-worker.js +1 -1
@@ -51,7 +51,7 @@
51
51
  // ▲▲▲ [Usage] ▲▲▲
52
52
 
53
53
  // Procedural line rendering settings
54
- const LOD_RENDERER_BUILD_ID = '20260713-normal-boundary-delta-v844';
54
+ const LOD_RENDERER_BUILD_ID = '20260725-cold-mid-line-restore-v846';
55
55
  const DirtyKind = Object.freeze({
56
56
  ResidentFullRebuild: 'resident-full-rebuild',
57
57
  ResidentPatch: 'resident-patch',
@@ -585,7 +585,14 @@
585
585
  return !isAnimatedGifImageModel(model);
586
586
  }
587
587
 
588
- if (contentType !== 'embed' && contentType !== 'video') {
588
+ // Every video needs a resident MID/FAR visual. Videos without an
589
+ // explicit preview use a lightweight atlas placeholder so the live
590
+ // <video> element can stay suspended without making the node vanish.
591
+ if (contentType === 'video') {
592
+ return true;
593
+ }
594
+
595
+ if (contentType !== 'embed') {
589
596
  return false;
590
597
  }
591
598
 
@@ -1255,6 +1262,33 @@
1255
1262
  return [invAtlas, invAtlas, invAtlas, invAtlas];
1256
1263
  }
1257
1264
 
1265
+ _getOrCreateImageAtlasSlot(nodeId) {
1266
+ const normalizedId = String(nodeId || '').trim();
1267
+ if (!normalizedId) {
1268
+ return null;
1269
+ }
1270
+
1271
+ let slot = this.imageAtlasSlots.get(normalizedId) || null;
1272
+ if (slot) {
1273
+ return slot;
1274
+ }
1275
+
1276
+ const slotIndex = this._allocateImageAtlasSlot();
1277
+ if (slotIndex < 0) {
1278
+ return null;
1279
+ }
1280
+
1281
+ slot = {
1282
+ slotIndex,
1283
+ key: '',
1284
+ uvRect: this._getImageAtlasUvRect(slotIndex),
1285
+ ready: false,
1286
+ unsupported: false
1287
+ };
1288
+ this.imageAtlasSlots.set(normalizedId, slot);
1289
+ return slot;
1290
+ }
1291
+
1258
1292
  _compactImageAtlasLoadQueue(force = false) {
1259
1293
  const cursor = this._imageAtlasLoadCursor || 0;
1260
1294
  if (cursor <= 0) return;
@@ -1653,35 +1687,114 @@
1653
1687
  }
1654
1688
  }
1655
1689
 
1690
+ _drawVideoLodPlaceholder(slot, nodeWidth, nodeHeight) {
1691
+ if (!slot || !this.imageAtlasCanvas || typeof document === 'undefined') {
1692
+ return false;
1693
+ }
1694
+
1695
+ const ctx = this.imageAtlasCanvas.getContext('2d', { alpha: false });
1696
+ if (!ctx) {
1697
+ return false;
1698
+ }
1699
+
1700
+ const columns = this.imageAtlasColumns || IMAGE_LOD_ATLAS_COLUMNS;
1701
+ const col = slot.slotIndex % columns;
1702
+ const row = Math.floor(slot.slotIndex / columns);
1703
+ const x = col * IMAGE_LOD_THUMBNAIL_SIZE;
1704
+ const y = row * IMAGE_LOD_THUMBNAIL_SIZE;
1705
+ const size = IMAGE_LOD_THUMBNAIL_SIZE;
1706
+ const safeWidth = Math.max(1, Number(nodeWidth || 300));
1707
+ const safeHeight = Math.max(1, Number(nodeHeight || 300));
1708
+ const targetAspect = safeWidth / safeHeight;
1709
+
1710
+ try {
1711
+ ctx.save();
1712
+
1713
+ const background = ctx.createLinearGradient(x, y, x, y + size);
1714
+ background.addColorStop(0, '#26364d');
1715
+ background.addColorStop(1, '#0f172a');
1716
+ ctx.fillStyle = background;
1717
+ ctx.fillRect(x, y, size, size);
1718
+
1719
+ const inset = 4;
1720
+ ctx.strokeStyle = 'rgba(148, 163, 184, 0.58)';
1721
+ ctx.lineWidth = 2;
1722
+ ctx.strokeRect(x + inset, y + inset, size - inset * 2, size - inset * 2);
1723
+
1724
+ // Pre-compensate the icon for the square atlas tile being
1725
+ // stretched to the video's world-space aspect ratio.
1726
+ const radiusX = size * 0.19;
1727
+ const radiusY = Math.min(size * 0.34, radiusX * Math.max(0.55, targetAspect));
1728
+ const centerX = x + size * 0.5;
1729
+ const centerY = y + size * 0.48;
1730
+ ctx.beginPath();
1731
+ ctx.ellipse(centerX, centerY, radiusX, radiusY, 0, 0, Math.PI * 2);
1732
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.18)';
1733
+ ctx.fill();
1734
+ ctx.strokeStyle = 'rgba(255, 255, 255, 0.62)';
1735
+ ctx.lineWidth = 2;
1736
+ ctx.stroke();
1737
+
1738
+ const triangleWidth = size * 0.13;
1739
+ const triangleHeight = Math.min(size * 0.24, triangleWidth * Math.max(0.85, targetAspect));
1740
+ ctx.beginPath();
1741
+ ctx.moveTo(centerX - triangleWidth * 0.35, centerY - triangleHeight * 0.5);
1742
+ ctx.lineTo(centerX + triangleWidth * 0.7, centerY);
1743
+ ctx.lineTo(centerX - triangleWidth * 0.35, centerY + triangleHeight * 0.5);
1744
+ ctx.closePath();
1745
+ ctx.fillStyle = '#f8fafc';
1746
+ ctx.fill();
1747
+
1748
+ ctx.fillStyle = 'rgba(226, 232, 240, 0.35)';
1749
+ ctx.fillRect(x + size * 0.18, y + size * 0.82, size * 0.64, 2);
1750
+ ctx.fillStyle = 'rgba(248, 250, 252, 0.78)';
1751
+ ctx.fillRect(x + size * 0.18, y + size * 0.82, size * 0.2, 2);
1752
+ ctx.restore();
1753
+
1754
+ if (this.imageAtlasTexture) {
1755
+ this.imageAtlasTexture.needsUpdate = true;
1756
+ }
1757
+ return true;
1758
+ } catch {
1759
+ try { ctx.restore(); } catch { }
1760
+ return false;
1761
+ }
1762
+ }
1763
+
1656
1764
  _ensureImageLodThumbnail(entry) {
1657
1765
  const nodeId = entry?.model?.id;
1658
1766
  if (!nodeId) {
1659
1767
  return null;
1660
1768
  }
1661
1769
 
1770
+ const contentType = getNodeContentType(entry?.model);
1771
+ const nodeWidth = Math.max(1, Number(entry?.model?.width || entry?.model?.Width || 300));
1772
+ const nodeHeight = Math.max(1, Number(entry?.model?.height || entry?.model?.Height || 300));
1662
1773
  const source = this._getImageLodSource(entry);
1663
1774
  if (!source) {
1775
+ if (contentType === 'video') {
1776
+ const placeholderKey = `video-placeholder|node:${Math.round(nodeWidth)}x${Math.round(nodeHeight)}`;
1777
+ const placeholderSlot = this._getOrCreateImageAtlasSlot(nodeId);
1778
+ if (!placeholderSlot) {
1779
+ return null;
1780
+ }
1781
+ if (placeholderSlot.ready && placeholderSlot.key === placeholderKey) {
1782
+ return placeholderSlot;
1783
+ }
1784
+ if (this._drawVideoLodPlaceholder(placeholderSlot, nodeWidth, nodeHeight)) {
1785
+ placeholderSlot.key = placeholderKey;
1786
+ placeholderSlot.ready = true;
1787
+ placeholderSlot.unsupported = false;
1788
+ return placeholderSlot;
1789
+ }
1790
+ }
1664
1791
  return null;
1665
1792
  }
1666
1793
 
1667
- const nodeWidth = Math.max(1, Number(entry?.model?.width || entry?.model?.Width || 300));
1668
- const nodeHeight = Math.max(1, Number(entry?.model?.height || entry?.model?.Height || 300));
1669
1794
  const sourceKey = `${source.key}|node:${Math.round(nodeWidth)}x${Math.round(nodeHeight)}`;
1670
-
1671
- let slot = this.imageAtlasSlots.get(nodeId) || null;
1795
+ const slot = this._getOrCreateImageAtlasSlot(nodeId);
1672
1796
  if (!slot) {
1673
- const slotIndex = this._allocateImageAtlasSlot();
1674
- if (slotIndex < 0) {
1675
- return null;
1676
- }
1677
- slot = {
1678
- slotIndex,
1679
- key: '',
1680
- uvRect: this._getImageAtlasUvRect(slotIndex),
1681
- ready: false,
1682
- unsupported: false
1683
- };
1684
- this.imageAtlasSlots.set(nodeId, slot);
1797
+ return null;
1685
1798
  }
1686
1799
 
1687
1800
  if (slot.ready && slot.key === sourceKey) {
@@ -1699,6 +1812,14 @@
1699
1812
  return slot;
1700
1813
  }
1701
1814
 
1815
+ if (contentType === 'video' &&
1816
+ this._drawVideoLodPlaceholder(slot, nodeWidth, nodeHeight)) {
1817
+ slot.key = `video-placeholder|source:${sourceKey}`;
1818
+ slot.ready = true;
1819
+ slot.unsupported = false;
1820
+ return slot;
1821
+ }
1822
+
1702
1823
  if (source.fetchFallbackOnDrawFailure === true) {
1703
1824
  slot.key = '';
1704
1825
  slot.ready = false;
@@ -3340,6 +3461,8 @@
3340
3461
  _hasPendingLineDetailRestore(lodBand = this._currentLodBand) {
3341
3462
  return ENABLE_INSTANCED_TEXT_LINES === true &&
3342
3463
  this._lineDetailsPatchPending === true &&
3464
+ this._deferLineRestorePending !== true &&
3465
+ this._disableLineRender !== true &&
3343
3466
  this.isInLODMode === true &&
3344
3467
  lodBand === 'MID' &&
3345
3468
  this.isLoading !== true;
@@ -6642,15 +6765,29 @@
6642
6765
 
6643
6766
  if (disableLines) {
6644
6767
  const cachedLineData = this.lineDataCache.get(nodeId) || null;
6645
- if (cachedLineData) {
6646
- lineCounts.push(cachedLineData.lineCount || 0);
6647
- lineHeights.push(cachedLineData.lineHeightPx || LINE_HEIGHT_PX);
6768
+ const deferredLineData = cachedLineData || (
6769
+ this._deferLineRestorePending === true
6770
+ ? window.MindMapRenderPlan?.resolveResidentLineData?.(
6771
+ this,
6772
+ entry,
6773
+ w,
6774
+ h,
6775
+ false,
6776
+ MAX_LINE_SAMPLES,
6777
+ LINE_HEIGHT_PX,
6778
+ LINE_PADDING_PX
6779
+ ) || null
6780
+ : null
6781
+ );
6782
+ if (deferredLineData) {
6783
+ lineCounts.push(deferredLineData.lineCount || 0);
6784
+ lineHeights.push(deferredLineData.lineHeightPx || LINE_HEIGHT_PX);
6648
6785
  linePaddings.push(
6649
- cachedLineData.paddingX || LINE_PADDING_PX,
6650
- cachedLineData.paddingY || LINE_PADDING_PX
6786
+ deferredLineData.paddingX || LINE_PADDING_PX,
6787
+ deferredLineData.paddingY || LINE_PADDING_PX
6651
6788
  );
6652
6789
  for (let i = 0; i < MAX_LINE_SAMPLES; i++) {
6653
- lineLengths.push(cachedLineData.lineLengths?.[i] || 0);
6790
+ lineLengths.push(deferredLineData.lineLengths?.[i] || 0);
6654
6791
  }
6655
6792
  } else {
6656
6793
  lineCounts.push(0);
@@ -7910,6 +8047,12 @@
7910
8047
  if (!hasResidentInstances) {
7911
8048
  this._setInstancesDirty('initial-board-build');
7912
8049
  }
8050
+ if (shouldDeferLineRestore) {
8051
+ this._scheduleResidentLodWake(
8052
+ 'post-load-line-restore',
8053
+ POST_LOAD_LINE_RESTORE_DELAY_MS + 16
8054
+ );
8055
+ }
7913
8056
  // ▼▼▼ [UX] Show nodes immediately after loading ends ▼▼▼
7914
8057
  if (this.instancedHaloMesh) this.instancedHaloMesh.visible = this.isInLODMode === true && shouldShowInstancedHaloInLodBand(this._currentLodBand);
7915
8058
  if (this.instancedMesh) this.instancedMesh.visible = this.isInLODMode === true;
@@ -525,7 +525,7 @@
525
525
  return nodeEntry.cssObject.userData.spatialGridKeys;
526
526
  }
527
527
 
528
- return new Set();
528
+ return null;
529
529
  }
530
530
 
531
531
  function setNodeSpatialGridKeys(nodeEntry, keys) {
@@ -589,7 +589,7 @@
589
589
  const bounds = getNodeWorldBounds(nodeEntry);
590
590
  if (!bounds) return;
591
591
 
592
- const oldKeys = getNodeSpatialGridKeys(nodeEntry);
592
+ const oldKeys = getNodeSpatialGridKeys(nodeEntry) || new Set();
593
593
  const newKeys = new Set();
594
594
  const startCellX = Math.floor(bounds.minX / module.GRID_CELL_SIZE);
595
595
  const endCellX = Math.floor(bounds.maxX / module.GRID_CELL_SIZE);
@@ -789,7 +789,7 @@
789
789
  const preservedY = Number(oldGroup?.position?.y ?? model.positionY ?? 0);
790
790
  const preservedZ = Number(oldGroup?.position?.z ?? model.positionZ ?? 0);
791
791
  const preservedVisible = oldGroup?.visible !== false;
792
- const preservedSpatialKeys = new Set(getNodeSpatialGridKeys(nodeEntry));
792
+ const preservedSpatialKeys = new Set(getNodeSpatialGridKeys(nodeEntry) || []);
793
793
  const width = Number(model.width || 400);
794
794
  const height = Number(model.height || 200);
795
795
 
@@ -846,21 +846,15 @@
846
846
  function removeNodeFromSpatialGrid(module, nodeId, nodeEntry = null) {
847
847
  if (!module?.spatialGrid || !nodeId) return;
848
848
 
849
- const keys = getNodeSpatialGridKeys(nodeEntry);
850
- if (keys && typeof keys.forEach === 'function') {
851
- keys.forEach(key => {
852
- const cell = module.spatialGrid.get(key);
853
- if (cell) cell.delete(nodeId);
854
- if (cell && cell.size === 0) module.spatialGrid.delete(key);
855
- });
856
- setNodeSpatialGridKeys(nodeEntry, new Set());
857
- return;
858
- }
859
-
860
- module.spatialGrid.forEach((cell, key) => {
849
+ const keys = getNodeSpatialGridKeys(nodeEntry)
850
+ || window.MindMapCss3dHotPath?.getSpatialGridKeysForBounds?.(module, nodeEntry)
851
+ || new Set();
852
+ keys.forEach(key => {
853
+ const cell = module.spatialGrid.get(key);
861
854
  if (!cell?.delete?.(nodeId)) return;
862
855
  if (cell.size === 0) module.spatialGrid.delete(key);
863
856
  });
857
+ setNodeSpatialGridKeys(nodeEntry, new Set());
864
858
  }
865
859
  // ▲▲▲ [New] ▲▲▲
866
860
 
@@ -927,6 +921,7 @@
927
921
  return null;
928
922
  }
929
923
 
924
+ const geometryKey = `${Math.round(width)}x${Math.round(height)}:${IMAGE_SELECTION_GLOW_PAD}`;
930
925
  if (!outline) {
931
926
  outline = new THREE.Mesh(
932
927
  createRoundedRectFilledGeometry(
@@ -943,15 +938,20 @@
943
938
  })
944
939
  );
945
940
  outline.name = 'outline';
941
+ outline.userData.geometrySizeKey = geometryKey;
946
942
  group.add(outline);
943
+ } else {
944
+ window.MindMapCss3dHotPath?.replaceGeometryIfSizeChanged?.(
945
+ outline,
946
+ geometryKey,
947
+ () => createRoundedRectFilledGeometry(
948
+ width + IMAGE_SELECTION_GLOW_PAD,
949
+ height + IMAGE_SELECTION_GLOW_PAD,
950
+ 20 + (IMAGE_SELECTION_GLOW_PAD * 0.5)
951
+ )
952
+ );
947
953
  }
948
954
 
949
- outline.geometry?.dispose?.();
950
- outline.geometry = createRoundedRectFilledGeometry(
951
- width + IMAGE_SELECTION_GLOW_PAD,
952
- height + IMAGE_SELECTION_GLOW_PAD,
953
- 20 + (IMAGE_SELECTION_GLOW_PAD * 0.5)
954
- );
955
955
  outline.position.set(-(IMAGE_SELECTION_GLOW_PAD / 2), IMAGE_SELECTION_GLOW_PAD / 2, 0);
956
956
  outline.renderOrder = Math.max(0, (baseRenderOrder || 1) - 1);
957
957
  outline.visible = !!isSelected;
@@ -3487,6 +3487,66 @@
3487
3487
  window.MindMapTextOverlayV2?.invalidateView?.(module);
3488
3488
  }
3489
3489
 
3490
+ function applyNodeGlRenderOrder(module, entry, targetBaseOrder) {
3491
+ if (!entry?.glObject) return;
3492
+ const renderables = [];
3493
+ let minOrder = Infinity;
3494
+ let maxOrder = -Infinity;
3495
+ entry.glObject.traverse(child => {
3496
+ if (!child.isMesh && !child.isLine) return;
3497
+ const order = Number(child.renderOrder || 0);
3498
+ renderables.push(child);
3499
+ if (order < minOrder) minOrder = order;
3500
+ if (order > maxOrder) maxOrder = order;
3501
+ });
3502
+ if (!Number.isFinite(minOrder)) return;
3503
+ const delta = Number(targetBaseOrder || 0) - minOrder;
3504
+ for (let i = 0; i < renderables.length; i++) {
3505
+ renderables[i].renderOrder = Number(renderables[i].renderOrder || 0) + delta;
3506
+ }
3507
+ module.nodeZCounter = Math.max(Number(module.nodeZCounter || 0), maxOrder + delta + 1);
3508
+ }
3509
+
3510
+ function flushPendingGlRenderOrder(module, entry) {
3511
+ if (!module || !entry || !Number.isFinite(entry._pendingGlRenderOrderBase)) {
3512
+ return false;
3513
+ }
3514
+ applyNodeGlRenderOrder(module, entry, entry._pendingGlRenderOrderBase);
3515
+ delete entry._pendingGlRenderOrderBase;
3516
+ entry._pendingGlRenderOrderScheduled = false;
3517
+ return true;
3518
+ }
3519
+
3520
+ function schedulePendingGlRenderOrder(module, nodeId, entry, attempt = 0) {
3521
+ if (!module || !entry || entry._pendingGlRenderOrderScheduled === true) return;
3522
+ entry._pendingGlRenderOrderScheduled = true;
3523
+ const run = () => {
3524
+ entry._pendingGlRenderOrderScheduled = false;
3525
+ if (module.nodeObjectsById?.get?.(nodeId) !== entry ||
3526
+ !Number.isFinite(entry._pendingGlRenderOrderBase)) {
3527
+ return;
3528
+ }
3529
+ const isInteracting =
3530
+ module.isPanning === true ||
3531
+ module.isZooming === true ||
3532
+ module.isDraggingNode === true ||
3533
+ module.isDraggingMultipleNodes === true ||
3534
+ module.isSelecting === true;
3535
+ if (isInteracting && attempt < 12) {
3536
+ schedulePendingGlRenderOrder(module, nodeId, entry, attempt + 1);
3537
+ return;
3538
+ }
3539
+ if (!isInteracting) {
3540
+ flushPendingGlRenderOrder(module, entry);
3541
+ }
3542
+ };
3543
+ if (typeof requestIdleCallback === 'function') {
3544
+ requestIdleCallback(run, { timeout: 180 });
3545
+ } else {
3546
+ setTimeout(run, 32);
3547
+ }
3548
+ }
3549
+
3490
3550
  function bringNodeToFront(module, nodeId) {
3491
3551
  if (!module || !nodeId) return;
3492
3552
 
@@ -3495,35 +3555,15 @@
3495
3555
  const contentTypeLower = getNodeContentTypeLower(entry.model);
3496
3556
  const usesSafeMediaSelectionChrome = contentTypeLower === 'video' || contentTypeLower === 'embed';
3497
3557
 
3498
- // Keep relative renderOrder offsets (e.g., resize handles above body) and move whole node to top.
3499
- if (entry.glObject) {
3500
- let minOrder = Infinity;
3501
- let maxOrder = -Infinity;
3502
-
3503
- entry.glObject.traverse(child => {
3504
- if (child.isMesh || child.isLine) {
3505
- const order = child.renderOrder || 0;
3506
- if (order < minOrder) minOrder = order;
3507
- if (order > maxOrder) maxOrder = order;
3508
- }
3509
- });
3510
-
3511
- if (!Number.isFinite(minOrder)) minOrder = 0;
3512
- if (!Number.isFinite(maxOrder)) maxOrder = minOrder;
3513
-
3514
- const targetBaseOrder = module.nodeZCounter++;
3515
- const delta = targetBaseOrder - minOrder;
3516
-
3517
- entry.glObject.traverse(child => {
3518
- if (child.isMesh || child.isLine) {
3519
- child.renderOrder = (child.renderOrder || 0) + delta;
3520
- }
3521
- });
3522
-
3523
- const nextTop = maxOrder + delta + 1;
3524
- if (nextTop > module.nodeZCounter) {
3525
- module.nodeZCounter = nextTop;
3526
- }
3558
+ const targetBaseOrder = module.nodeZCounter++;
3559
+ const deferGlOrder = entry.currentType === 'CSS' &&
3560
+ entry.cssObject?.visible === true &&
3561
+ Number(module.camera?.position?.z || 0) < 9000;
3562
+ if (deferGlOrder) {
3563
+ entry._pendingGlRenderOrderBase = targetBaseOrder;
3564
+ schedulePendingGlRenderOrder(module, nodeId, entry);
3565
+ } else {
3566
+ applyNodeGlRenderOrder(module, entry, targetBaseOrder);
3527
3567
  }
3528
3568
 
3529
3569
  // CSS3DObject re-parenting can trigger a one-frame detach flicker.
@@ -4226,30 +4266,10 @@
4226
4266
  }
4227
4267
  // ▲▲▲ [Optimization] ▲▲▲
4228
4268
 
4229
- // ▼▼▼ [Fix] 다른 노드들의 텍스트 선택을 비활성화하여 선택 번짐 방지 ▼▼▼
4230
- module.nodeObjectsById.forEach((otherEntry, otherId) => {
4231
- if (otherId === nodeId) return; // 현재 노드는 건너뛰기
4232
- if (!otherEntry.cssObject?.element) return;
4233
-
4234
- const otherWrapper = otherEntry.cssObject.element;
4235
- // 다른 노드들의 텍스트 선택 비활성화
4236
- const contentEls = otherWrapper.querySelectorAll(NODE_TEXT_SELECTION_SELECTORS);
4237
- contentEls.forEach(el => {
4238
- el.style.userSelect = 'none';
4239
- el.style.webkitUserSelect = 'none';
4240
- });
4241
- });
4242
- // ▲▲▲ [Fix] ▲▲▲
4243
-
4244
- // ▼▼▼ [Fix] 현재 노드의 텍스트 선택 활성화 ▼▼▼
4245
- const currentWrapper = nodeEntry.cssObject.element;
4246
- if (currentWrapper) {
4247
- const contentEls = currentWrapper.querySelectorAll(NODE_TEXT_SELECTION_SELECTORS);
4248
- contentEls.forEach(el => {
4249
- el.style.userSelect = 'text';
4250
- el.style.webkitUserSelect = 'text';
4251
- });
4252
- }
4269
+ // CSS3D text selection ownership is updated as a previous/next delta.
4270
+ // The manager patches only those two wrappers instead of walking the
4271
+ // complete node map on every CSS3D promotion.
4272
+ window.MindMapCss3DManager?.setNativeTextSelectionOwnerClass?.(module, nodeId);
4253
4273
  // ▲▲▲ [Fix] ▲▲▲
4254
4274
 
4255
4275
  // Sync CSS3D scroll position with WebGL/model scrollOffset (ratio-based).
@@ -4315,6 +4335,7 @@
4315
4335
  // ▲▲▲ [Changed] ▲▲▲
4316
4336
 
4317
4337
  if (nodeEntry.currentType === 'GL') {
4338
+ flushPendingGlRenderOrder(module, nodeEntry);
4318
4339
  log(`[MindMapNodes] SwitchToWebGL(${nodeId}) SKIPPED (Already in GL mode).`);
4319
4340
  return;
4320
4341
  }
@@ -4324,6 +4345,7 @@
4324
4345
  if (nodeEntry.isDeferredShell === true) {
4325
4346
  await ensureDeferredNodeMaterialized(module, nodeEntry, { attachToScene: true });
4326
4347
  }
4348
+ flushPendingGlRenderOrder(module, nodeEntry);
4327
4349
 
4328
4350
  // Store CSS3D textarea scroll ratio into WebGL scrollOffset
4329
4351
  const textarea = nodeEntry.cssObject.element.querySelector('textarea');
@@ -146,11 +146,14 @@
146
146
 
147
147
  // ▼▼▼ [Perf] Render queue (time-sliced) ▼▼▼
148
148
  const renderQueue = [];
149
- const renderQueueSet = new Set();
150
- let renderQueueScheduled = false;
151
- let renderModuleRef = null;
152
- const RENDER_BUDGET_MS = 8;
153
- let renderSeq = 0;
149
+ const renderQueueSet = new Set();
150
+ let renderQueueScheduled = false;
151
+ let renderModuleRef = null;
152
+ let renderQueueDirty = false;
153
+ const RENDER_BUDGET_MS = 8;
154
+ let renderSeq = 0;
155
+ let renderQueueSortCount = 0;
156
+ let renderQueueMaxLength = 0;
154
157
 
155
158
  function computeRenderPriority(module, nodeId) {
156
159
  const entry = module?.nodeObjectsById?.get(nodeId);
@@ -178,9 +181,11 @@
178
181
  function enqueueRender(module, nodeId) {
179
182
  if (!nodeId) return;
180
183
  renderModuleRef = module;
181
- if (!renderQueueSet.has(nodeId)) {
182
- renderQueue.push({ nodeId, priority: computeRenderPriority(module, nodeId), seq: renderSeq++ });
183
- renderQueueSet.add(nodeId);
184
+ if (!renderQueueSet.has(nodeId)) {
185
+ renderQueue.push({ nodeId, priority: computeRenderPriority(module, nodeId), seq: renderSeq++ });
186
+ renderQueueSet.add(nodeId);
187
+ renderQueueDirty = true;
188
+ renderQueueMaxLength = Math.max(renderQueueMaxLength, renderQueue.length);
184
189
  }
185
190
  scheduleRenderQueue();
186
191
  }
@@ -195,12 +200,16 @@
195
200
  const start = performance.now();
196
201
 
197
202
  // 우선순위 정렬 (가시/선택/거리)
198
- if (renderQueue.length > 1) {
199
- renderQueue.sort((a, b) => (a.priority - b.priority) || (a.seq - b.seq));
200
- }
201
-
202
- while (renderQueue.length > 0) {
203
- const item = renderQueue.shift();
203
+ if (renderQueueDirty) {
204
+ if (renderQueue.length > 1) {
205
+ renderQueue.sort((a, b) => (b.priority - a.priority) || (b.seq - a.seq));
206
+ renderQueueSortCount++;
207
+ }
208
+ renderQueueDirty = false;
209
+ }
210
+
211
+ while (renderQueue.length > 0) {
212
+ const item = renderQueue.pop();
204
213
  const nodeId = item.nodeId;
205
214
  renderQueueSet.delete(nodeId);
206
215
  if (renderModuleRef) {
@@ -224,7 +233,8 @@
224
233
  // ▲▲▲ [New] ▲▲▲
225
234
 
226
235
  // ▼▼▼ [Perf] Deferred disposal to avoid GC/driver spikes ▼▼▼
227
- const disposeQueue = [];
236
+ const disposeQueue = [];
237
+ let disposeQueueHead = 0;
228
238
  let disposeScheduled = false;
229
239
  const DISPOSE_BUDGET_MS = 6; // soft budget per frame
230
240
 
@@ -246,8 +256,8 @@
246
256
 
247
257
  function processDisposeQueue(deadline) {
248
258
  const start = performance.now();
249
- while (disposeQueue.length > 0) {
250
- const item = disposeQueue.shift();
259
+ while (disposeQueueHead < disposeQueue.length) {
260
+ const item = disposeQueue[disposeQueueHead++];
251
261
  try { item.dispose(); } catch { }
252
262
 
253
263
  if (deadline && typeof deadline.timeRemaining === 'function') {
@@ -258,9 +268,16 @@
258
268
  }
259
269
 
260
270
  disposeScheduled = false;
261
- if (disposeQueue.length > 0) {
262
- scheduleDispose();
263
- }
271
+ if (disposeQueueHead >= disposeQueue.length) {
272
+ disposeQueue.length = 0;
273
+ disposeQueueHead = 0;
274
+ } else if (disposeQueue.length - disposeQueueHead > 64) {
275
+ disposeQueue.splice(0, disposeQueueHead);
276
+ disposeQueueHead = 0;
277
+ }
278
+ if (disposeQueueHead < disposeQueue.length) {
279
+ scheduleDispose();
280
+ }
264
281
  }
265
282
  // ▲▲▲ [Perf] ▲▲▲
266
283
 
@@ -335,6 +352,7 @@
335
352
  if (!group) return null;
336
353
 
337
354
  let outline = group.getObjectByName('outline');
355
+ const geometryKey = `${Math.round(width)}x${Math.round(height)}:${IMAGE_SELECTION_GLOW_PAD}`;
338
356
  if (!outline) {
339
357
  outline = new THREE.Mesh(
340
358
  createRoundedRectFilledGeometry(
@@ -351,16 +369,24 @@
351
369
  })
352
370
  );
353
371
  outline.name = 'outline';
354
- outline.userData = { ...(outline.userData || {}), isImageSelectionGlow: true };
372
+ outline.userData = {
373
+ ...(outline.userData || {}),
374
+ isImageSelectionGlow: true,
375
+ geometrySizeKey: geometryKey
376
+ };
355
377
  group.add(outline);
378
+ } else {
379
+ window.MindMapCss3dHotPath?.replaceGeometryIfSizeChanged?.(
380
+ outline,
381
+ geometryKey,
382
+ () => createRoundedRectFilledGeometry(
383
+ width + IMAGE_SELECTION_GLOW_PAD,
384
+ height + IMAGE_SELECTION_GLOW_PAD,
385
+ 20 + (IMAGE_SELECTION_GLOW_PAD * 0.5)
386
+ )
387
+ );
356
388
  }
357
389
 
358
- outline.geometry?.dispose?.();
359
- outline.geometry = createRoundedRectFilledGeometry(
360
- width + IMAGE_SELECTION_GLOW_PAD,
361
- height + IMAGE_SELECTION_GLOW_PAD,
362
- 20 + (IMAGE_SELECTION_GLOW_PAD * 0.5)
363
- );
364
390
  outline.position.set(-(IMAGE_SELECTION_GLOW_PAD / 2), IMAGE_SELECTION_GLOW_PAD / 2, 0);
365
391
  outline.renderOrder = baseRenderOrder - 1;
366
392
  outline.visible = !!isSelected;
@@ -2186,7 +2212,7 @@
2186
2212
  }
2187
2213
 
2188
2214
  // Expose MindMapPipeline globally
2189
- window.MindMapPipeline = {
2215
+ window.MindMapPipeline = {
2190
2216
  processRenderQueue: processRenderQueue,
2191
2217
  debouncedProcessRenderQueue: debouncedProcessRenderQueue,
2192
2218
  throttledScrollUpdate: throttledScrollUpdate,
@@ -2194,8 +2220,18 @@
2194
2220
  throttledProcessRenderQueueDuringResize: throttledProcessRenderQueueDuringResize,
2195
2221
  startResizing: startResizing,
2196
2222
  doResizing: doResizing,
2197
- stopResizing: stopResizing,
2198
- throttledUpdateScrollInCSharp: throttledUpdateScrollInCSharp
2223
+ stopResizing: stopResizing,
2224
+ getQueueDebugMetrics: () => ({
2225
+ sortCount: renderQueueSortCount,
2226
+ maxLength: renderQueueMaxLength,
2227
+ currentLength: renderQueue.length,
2228
+ disposePending: Math.max(0, disposeQueue.length - disposeQueueHead)
2229
+ }),
2230
+ resetQueueDebugMetrics: () => {
2231
+ renderQueueSortCount = 0;
2232
+ renderQueueMaxLength = renderQueue.length;
2233
+ },
2234
+ throttledUpdateScrollInCSharp: throttledUpdateScrollInCSharp
2199
2235
  };
2200
2236
 
2201
2237
  console.log('? mind-map-pipeline.js loaded');