@mindexec/cli 0.2.431 → 0.2.433

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.
@@ -924,10 +924,10 @@
924
924
  // Create initial texture array
925
925
  this._createTextureArray();
926
926
 
927
- // Line data cache & texture (for actual text-based lines)
928
- this.maxInstances = 10000;
929
- this.lineDataCache = new Map(); // nodeId -> { signature, lineCount, lineLengths }
930
- this.lineDataArray = new Uint8Array(this.maxInstances * MAX_LINE_SAMPLES);
927
+ this.maxInstances = 10000;
928
+ this.lineDataCache = new Map();
929
+ this.lineTextureRows = window.MindMapRenderPlan?.initialLineTextureRows?.(this.maxInstances) || this.maxInstances;
930
+ this.lineDataArray = new Uint8Array(this.lineTextureRows * MAX_LINE_SAMPLES);
931
931
  this.lineDataTexture = null;
932
932
  this.lineDataDirty = true;
933
933
  this._lastInstanceCount = 0;
@@ -1211,7 +1211,7 @@
1211
1211
  return this.imageAtlasFreeSlots.pop();
1212
1212
  }
1213
1213
 
1214
- if (this.imageAtlasNextSlot >= IMAGE_LOD_ATLAS_MAX_SLOTS) {
1214
+ if (this.imageAtlasNextSlot >= (this.imageAtlasMaxSlots || IMAGE_LOD_ATLAS_MAX_SLOTS)) {
1215
1215
  return -1;
1216
1216
  }
1217
1217
 
@@ -1219,9 +1219,10 @@
1219
1219
  }
1220
1220
 
1221
1221
  _getImageAtlasUvRect(slotIndex) {
1222
- const col = slotIndex % IMAGE_LOD_ATLAS_COLUMNS;
1223
- const row = Math.floor(slotIndex / IMAGE_LOD_ATLAS_COLUMNS);
1224
- const invAtlas = 1 / IMAGE_LOD_ATLAS_SIZE;
1222
+ const columns = this.imageAtlasColumns || IMAGE_LOD_ATLAS_COLUMNS;
1223
+ const col = slotIndex % columns;
1224
+ const row = Math.floor(slotIndex / columns);
1225
+ const invAtlas = 1 / (this.imageAtlasSize || IMAGE_LOD_ATLAS_SIZE);
1225
1226
  const inset = 1.0;
1226
1227
  return [
1227
1228
  (col * IMAGE_LOD_THUMBNAIL_SIZE + inset) * invAtlas,
@@ -1232,7 +1233,7 @@
1232
1233
  }
1233
1234
 
1234
1235
  _getImagePlaceholderUvRect() {
1235
- const invAtlas = 1 / IMAGE_LOD_ATLAS_SIZE;
1236
+ const invAtlas = 1 / (this.imageAtlasSize || IMAGE_LOD_ATLAS_SIZE);
1236
1237
  return [invAtlas, invAtlas, invAtlas, invAtlas];
1237
1238
  }
1238
1239
 
@@ -1583,8 +1584,9 @@
1583
1584
  return false;
1584
1585
  }
1585
1586
 
1586
- const col = slot.slotIndex % IMAGE_LOD_ATLAS_COLUMNS;
1587
- const row = Math.floor(slot.slotIndex / IMAGE_LOD_ATLAS_COLUMNS);
1587
+ const columns = this.imageAtlasColumns || IMAGE_LOD_ATLAS_COLUMNS;
1588
+ const col = slot.slotIndex % columns;
1589
+ const row = Math.floor(slot.slotIndex / columns);
1588
1590
  const x = col * IMAGE_LOD_THUMBNAIL_SIZE;
1589
1591
  const y = row * IMAGE_LOD_THUMBNAIL_SIZE;
1590
1592
  const size = IMAGE_LOD_THUMBNAIL_SIZE;
@@ -1712,8 +1714,8 @@
1712
1714
  if (!this.lineDataTexture) {
1713
1715
  this.lineDataTexture = new THREE.DataTexture(
1714
1716
  this.lineDataArray,
1715
- MAX_LINE_SAMPLES,
1716
- this.maxInstances,
1717
+ MAX_LINE_SAMPLES,
1718
+ this.lineTextureRows,
1717
1719
  THREE.RedFormat,
1718
1720
  THREE.UnsignedByteType
1719
1721
  );
@@ -1734,14 +1736,7 @@
1734
1736
  atlasContext.fillRect(0, 0, IMAGE_LOD_ATLAS_SIZE, IMAGE_LOD_ATLAS_SIZE);
1735
1737
  }
1736
1738
  this.imageAtlasTexture = new THREE.CanvasTexture(this.imageAtlasCanvas);
1737
- this.imageAtlasTexture.colorSpace = THREE.SRGBColorSpace;
1738
- this.imageAtlasTexture.minFilter = THREE.LinearFilter;
1739
- this.imageAtlasTexture.magFilter = THREE.LinearFilter;
1740
- this.imageAtlasTexture.wrapS = THREE.ClampToEdgeWrapping;
1741
- this.imageAtlasTexture.wrapT = THREE.ClampToEdgeWrapping;
1742
- this.imageAtlasTexture.flipY = false;
1743
- this.imageAtlasTexture.generateMipmaps = false;
1744
- this.imageAtlasTexture.needsUpdate = true;
1739
+ window.MindMapRenderPlan?.configureImageAtlasTexture?.(this.imageAtlasTexture, THREE);
1745
1740
  }
1746
1741
 
1747
1742
  const nodeVertexShader = `
@@ -1954,6 +1949,7 @@
1954
1949
  polygonOffsetUnits: -10.0
1955
1950
  });
1956
1951
 
1952
+ const imageAtlasOutputColorChunk = window.MindMapRenderPlan?.getImageAtlasOutputColorChunk?.(THREE) || '';
1957
1953
  const imageLodMaterial = new THREE.ShaderMaterial({
1958
1954
  uniforms: {
1959
1955
  uImageAtlas: { value: this.imageAtlasTexture },
@@ -2014,7 +2010,7 @@
2014
2010
  color = mix(color, borderColor, borderMask * (vSelected > 0.5 ? 0.92 : 0.36));
2015
2011
 
2016
2012
  gl_FragColor = vec4(color, 1.0);
2017
- #include <colorspace_fragment>
2013
+ ${imageAtlasOutputColorChunk}
2018
2014
  }
2019
2015
  `,
2020
2016
  transparent: false,
@@ -2154,7 +2150,7 @@
2154
2150
  const lineMaterial = new THREE.ShaderMaterial({
2155
2151
  uniforms: {
2156
2152
  uLineDataTex: { value: this.lineDataTexture },
2157
- uLineDataSize: { value: new THREE.Vector2(MAX_LINE_SAMPLES, this.maxInstances) },
2153
+ uLineDataSize: { value: new THREE.Vector2(MAX_LINE_SAMPLES, this.lineTextureRows) },
2158
2154
  uLineHeightPx: { value: LINE_HEIGHT_PX },
2159
2155
  uLinePaddingPx: { value: LINE_PADDING_PX },
2160
2156
  uLineHalfThicknessPx: { value: LOD_TEXT_LINE_HALF_THICKNESS_SCREEN_PX },
@@ -2727,43 +2723,6 @@
2727
2723
  this._startFullResHandoff(previousBand || this._fullResHandoffPreviousBand || 'MID', 'pending-css', this._fullResPanHandoffMaxMs);
2728
2724
  }
2729
2725
 
2730
- _collectResidentModeCss3dCleanupIds(nodeObjectsById) {
2731
- const ids = new Set();
2732
- if (!nodeObjectsById?.forEach) {
2733
- return ids;
2734
- }
2735
-
2736
- nodeObjectsById.forEach((entry, key) => {
2737
- if (!entry?.cssObject || isLivePortalCssLodModel(entry?.model)) {
2738
- return;
2739
- }
2740
-
2741
- const element = entry.cssObject.element || null;
2742
- const isAttached = !!entry.cssObject.parent;
2743
- const isDisplayed = !!element &&
2744
- element.style.display !== 'none' &&
2745
- element.style.visibility !== 'hidden' &&
2746
- element.style.opacity !== '0';
2747
- const isVisible = entry.cssObject.visible === true || isAttached || isDisplayed;
2748
- if (!isVisible) {
2749
- return;
2750
- }
2751
-
2752
- const nodeId = String(
2753
- entry.model?.id ||
2754
- entry.model?.Id ||
2755
- entry.glObject?.userData?.nodeId ||
2756
- entry.cssObject?.userData?.nodeId ||
2757
- key ||
2758
- ''
2759
- ).trim();
2760
- if (nodeId) {
2761
- ids.add(nodeId);
2762
- }
2763
- });
2764
-
2765
- return ids;
2766
- }
2767
2726
  // ▲▲▲ [New] ▲▲▲
2768
2727
 
2769
2728
  // ═══════════════════════════════════════════════════════════════════════
@@ -3455,7 +3414,7 @@
3455
3414
  }
3456
3415
 
3457
3416
  hasResidentDirtyWork(camera = this._module?.camera, module = this._module) {
3458
- return this.getResidentDirtyState(camera, module).dirty === true;
3417
+ return window.MindMapRenderPlan?.hasResidentDirtyWork?.(this, camera, module, getLodBandForCameraZ, buildLodSelectionKey, isAnimatedGifImageModel) === true;
3459
3418
  }
3460
3419
 
3461
3420
  willLodBandChange(camera) {
@@ -3756,7 +3715,7 @@
3756
3715
  }
3757
3716
  }
3758
3717
 
3759
- for (const nodeId of Array.from(this._lodImageCssFallbackNodeIds)) {
3718
+ for (const nodeId of window.MindMapRenderPlan?.fallbackBatch?.(this,this._lodImageCssFallbackNodeIds)||Array.from(this._lodImageCssFallbackNodeIds)) {
3760
3719
  if (nextFallbackIds.has(nodeId)) {
3761
3720
  continue;
3762
3721
  }
@@ -3779,7 +3738,7 @@
3779
3738
  }
3780
3739
 
3781
3740
  let hiddenCount = 0;
3782
- for (const nodeId of Array.from(this._lodImageCssFallbackNodeIds)) {
3741
+ for (const nodeId of window.MindMapRenderPlan?.fallbackBatch?.(this,this._lodImageCssFallbackNodeIds)||Array.from(this._lodImageCssFallbackNodeIds)) {
3783
3742
  const entry = nodeObjectsById?.get?.(nodeId) || null;
3784
3743
  if (entry) {
3785
3744
  this._hideCssMediaLodFallback(entry);
@@ -4053,17 +4012,10 @@
4053
4012
  }
4054
4013
 
4055
4014
  _clearFullResImageVisuals(nodeObjectsById) {
4056
- if (!this._fullResImageVisualIds || this._fullResImageVisualIds.size === 0) return;
4057
-
4058
- for (const nodeId of this._fullResImageVisualIds) {
4059
- const entry = nodeObjectsById?.get?.(nodeId) || null;
4060
- if (entry) this._hideImageLodFullVisual(entry);
4061
- }
4062
-
4063
- this._fullResImageVisualIds.clear();
4015
+ window.MindMapRenderPlan?.clearFullResImageVisuals?.(this, nodeObjectsById);
4064
4016
  }
4065
4017
 
4066
- _syncFullResImageVisuals(visibleIds, nodeObjectsById, module, lodBand) {
4018
+ _syncFullResImageVisuals(visibleIds, nodeObjectsById, module, lodBand, selectionKey = '') {
4067
4019
  if (this.isInLODMode !== false || !nodeObjectsById) {
4068
4020
  this._clearFullResImageVisuals(nodeObjectsById);
4069
4021
  return;
@@ -4093,7 +4045,10 @@
4093
4045
  if (!entry) continue;
4094
4046
 
4095
4047
  if (supportsCss3dNodeModel(entry?.model)) {
4096
- this._syncCssImageBodyBridge(entry, module);
4048
+ if (window.MindMapRenderPlan?.restoreParkedFullResCss?.(
4049
+ this, entry, module, nodeObjectsById, selectionKey, lodBand, nodeId) !== true) {
4050
+ this._syncCssImageBodyBridge(entry, module);
4051
+ }
4097
4052
  continue;
4098
4053
  }
4099
4054
 
@@ -4554,6 +4509,7 @@
4554
4509
  }
4555
4510
 
4556
4511
  _restoreVisibleCss3dHighDetailForNear(nodeObjectsById, module, lodBand) {
4512
+ // Full-res: NEAR / NORMAL. MID/FAR use resident instances.
4557
4513
  if ((lodBand !== 'NEAR' && lodBand !== 'NORMAL') || !nodeObjectsById) {
4558
4514
  return 0;
4559
4515
  }
@@ -4697,6 +4653,7 @@
4697
4653
  }
4698
4654
 
4699
4655
  entry.cssObject = cssObject;
4656
+ window.MindMapRenderPlan?.cachePreparedResidentCss?.(this, entry, module, module.nodeObjectsById);
4700
4657
  return true;
4701
4658
  }
4702
4659
 
@@ -4706,8 +4663,7 @@
4706
4663
  !Array.isArray(prioritizedVisible) ||
4707
4664
  !nodeObjectsById ||
4708
4665
  !module ||
4709
- this.isLoading ||
4710
- module.isZooming === true) {
4666
+ this.isLoading) {
4711
4667
  return;
4712
4668
  }
4713
4669
 
@@ -4718,12 +4674,12 @@
4718
4674
  if (!nodeId || this._css3dPrewarmQueuedIds.has(nodeId)) continue;
4719
4675
 
4720
4676
  const entry = nodeObjectsById.get(nodeId);
4721
- if (!entry || entry.cssObject || !supportsCss3dNodeModel(entry.model)) continue;
4677
+ if (!entry || (entry.cssObject && entry.isCssDirty !== true) || !supportsCss3dNodeModel(entry.model)) continue;
4722
4678
  if (allowPanTextPrewarm && !isTextLikeModel(entry.model)) continue;
4723
4679
  if (shouldShowAgentConsoleInLodBand(lodBand) && hasAgentConsoleOpenForEntry(entry)) continue;
4724
4680
 
4725
4681
  this._css3dPrewarmQueuedIds.add(nodeId);
4726
- this._css3dPrewarmQueue.push({ nodeId, module });
4682
+ this._css3dPrewarmQueue.push({ nodeId, module, nodeObjectsById, entry });
4727
4683
  }
4728
4684
 
4729
4685
  if (this._css3dPrewarmQueue.length > 0 && !this._css3dPrewarmProcessing) {
@@ -4749,6 +4705,7 @@
4749
4705
  const module = this._module || null;
4750
4706
  const allowPanTextPrewarm = module?.isPanning === true && module?.isZooming !== true;
4751
4707
  if ((module?.isPanning === true && !allowPanTextPrewarm) || module?.isZooming === true) {
4708
+ shouldContinue = this._css3dPrewarmQueue.length > 0;
4752
4709
  return;
4753
4710
  }
4754
4711
 
@@ -4774,11 +4731,17 @@
4774
4731
 
4775
4732
  this._css3dPrewarmQueuedIds.delete(nodeId);
4776
4733
  const itemModule = item.module || this._module;
4734
+ if (itemModule !== this._module || item.nodeObjectsById !== itemModule?.nodeObjectsById) continue;
4777
4735
  const entry = itemModule?.nodeObjectsById?.get(nodeId);
4778
- if (!entry || entry.cssObject || !supportsCss3dNodeModel(entry.model)) continue;
4736
+ if (!entry || entry !== item.entry || (entry.cssObject && entry.isCssDirty !== true) || !supportsCss3dNodeModel(entry.model)) continue;
4779
4737
  if (allowPanTextPrewarm && !isTextLikeModel(entry.model)) continue;
4780
4738
 
4781
- this._ensureHiddenCss3dObject(entry, itemModule);
4739
+ if (!entry.cssObject && !this._ensureHiddenCss3dObject(entry, itemModule)) continue;
4740
+ if (entry.isCssDirty === true && window.MindMapCss3DManager?.syncCss3dContent) {
4741
+ window.MindMapCss3DManager.syncCss3dContent(entry.model, entry.cssObject);
4742
+ entry.isCssDirty = false;
4743
+ }
4744
+ window.MindMapRenderPlan?.cachePreparedResidentCss?.(this, entry, itemModule, item.nodeObjectsById);
4782
4745
  count++;
4783
4746
  }
4784
4747
 
@@ -5071,6 +5034,7 @@
5071
5034
  if (!entry || !isCssMediaFallbackModel(entry?.model)) {
5072
5035
  return false;
5073
5036
  }
5037
+ if (window.MindMapRenderPlan?.reuseActiveCssMediaFallback?.(this,entry,module)) return true;
5074
5038
 
5075
5039
  if (!entry.cssObject?.element && !this._ensureHiddenCss3dObject(entry, module)) {
5076
5040
  return false;
@@ -5187,6 +5151,12 @@
5187
5151
  const isNodeSelected = nodeId
5188
5152
  ? isNodeSelectedForLod(nodeId, multiSelectedIds, selectedNodeId)
5189
5153
  : false;
5154
+ const hasInteractiveOwner = module?.draggedNodeId === nodeId || this._getCurrentHoveredNodeId(module) === nodeId;
5155
+ if (contentType === 'image' && !isAnimatedGifImageModel(entry.model) &&
5156
+ this._hasActiveImageLodInstance(nodeId) && !isNodeSelected && !hasInteractiveOwner) {
5157
+ this._hideCssMediaLodFallback(entry, { forceMediaGlFallback: true });
5158
+ return false;
5159
+ }
5190
5160
  const shouldKeep = contentType === 'image'
5191
5161
  ? (entry.isVisibleInFrustum === true || isNodeSelected === true)
5192
5162
  : entry.isVisibleInFrustum === true;
@@ -5206,7 +5176,7 @@
5206
5176
 
5207
5177
  const allowImageCssFallbacks = this._shouldUseCssImageFallbackInResidentLod(module, lodBand);
5208
5178
 
5209
- for (const nodeId of Array.from(this._lodImageCssFallbackNodeIds)) {
5179
+ for (const nodeId of window.MindMapRenderPlan?.fallbackBatch?.(this, this._lodImageCssFallbackNodeIds) || []) {
5210
5180
  const entry = nodeObjectsById.get(nodeId) || null;
5211
5181
  if (!entry || !isCssMediaFallbackModel(entry.model)) {
5212
5182
  this._lodImageCssFallbackNodeIds.delete(nodeId);
@@ -5345,39 +5315,7 @@
5345
5315
  }
5346
5316
 
5347
5317
  hasPendingNearCssWarmup(module) {
5348
- if (this.isInLODMode !== false) {
5349
- return false;
5350
- }
5351
-
5352
- if (this._css3dProcessing || this._css3dQueuedMap.size > 0 || this._hasCss3dQueueItems()) {
5353
- return true;
5354
- }
5355
-
5356
- if (this._nearEntryBoostFrames > 0) {
5357
- return true;
5358
- }
5359
-
5360
- const moduleRef = module || this._module || null;
5361
- const visibleIds = this._getNearVisibleIds(moduleRef);
5362
- const nodeObjectsById = moduleRef?.nodeObjectsById || null;
5363
- if (!visibleIds || !nodeObjectsById || visibleIds.size === 0) {
5364
- return false;
5365
- }
5366
-
5367
- for (const nodeId of visibleIds) {
5368
- if (this._nearCssVisibleIds.has(nodeId)) {
5369
- continue;
5370
- }
5371
-
5372
- const entry = nodeObjectsById.get(nodeId);
5373
- if (!entry?.glObject || !supportsCss3dNodeModel(entry.model)) {
5374
- continue;
5375
- }
5376
-
5377
- return true;
5378
- }
5379
-
5380
- return false;
5318
+ return window.MindMapRenderPlan?.hasPendingNearCssWarmup?.(this, module, buildLodSelectionKey, isCss3dRendererEnabled, supportsCss3dNodeModel) === true;
5381
5319
  }
5382
5320
 
5383
5321
 
@@ -5702,40 +5640,8 @@
5702
5640
  return { warmMax, queueMax, perIdle, budgetMs };
5703
5641
  }
5704
5642
 
5705
- _getAdaptiveNearEntryBoostFrames(visibleCount = 0, module = this._module) {
5706
- const avgFrameTime = Number(module?._avgFrameTime || 16);
5707
- const fps = Number(module?._currentFps || 60);
5708
- let frames = 12;
5709
-
5710
- if (visibleCount >= 72) {
5711
- frames = 16;
5712
- }
5713
- if (visibleCount >= 144) {
5714
- frames = 20;
5715
- }
5716
- if (visibleCount >= 240) {
5717
- frames = 24;
5718
- }
5719
-
5720
- if (avgFrameTime > 22 || fps < 42) {
5721
- frames = Math.max(10, frames - 4);
5722
- }
5723
-
5724
- return frames;
5725
- }
5726
-
5727
- _getAdaptiveNearCssActivationConfig(visibleCount = 0, module = this._module, isNearBoostFrame = false) {
5728
- const visibleBudget = Number.isFinite(Number(visibleCount))
5729
- ? Math.max(1, Math.ceil(Number(visibleCount)))
5730
- : Number.MAX_SAFE_INTEGER;
5731
-
5732
- // NEAR/NORMAL is the readable full-res CSS3D path. Splitting activation
5733
- // across frames makes some nodes move or reveal later than others, which
5734
- // is visually worse than a bounded one-tick activation cost.
5735
- return {
5736
- maxPerFrame: visibleBudget,
5737
- budgetMs: Number.MAX_SAFE_INTEGER
5738
- };
5643
+ _getAdaptiveNearCssActivationConfig(visibleCount = 0) {
5644
+ return { maxPerFrame: Math.max(1, Math.ceil(Number(visibleCount) || 1)), budgetMs: Number.MAX_SAFE_INTEGER };
5739
5645
  }
5740
5646
 
5741
5647
  updateLOD(camera, nodeObjectsById, module) {
@@ -5826,8 +5732,8 @@
5826
5732
  if (this.instancedImageMesh) this.instancedImageMesh.visible = false;
5827
5733
  return;
5828
5734
  }
5735
+ window.MindMapRenderPlan?.scheduleResidentLinePrewarm?.(this, module, nodeObjectsById);
5829
5736
 
5830
- // Explicit bands: NEAR(DOM overlay + CSS3D), MID(resident nodes+lines), FAR(nodes only)
5831
5737
  const shouldUseLOD = lodBand === 'MID' || lodBand === 'FAR';
5832
5738
  perfMode = shouldUseLOD ? 'lod' : 'fullres';
5833
5739
  const isLodPanFrame = shouldUseLOD &&
@@ -5890,13 +5796,12 @@
5890
5796
  this._markCss3dSyncNeeded(true);
5891
5797
  }
5892
5798
 
5893
- // State changed? Only toggle visibility on state change
5894
- // Also force transition when mode is unknown (after reset/re-activation).
5895
5799
  if (modeChanged) {
5896
- const tModeStart = performance.now(); // ▼ [Timing]
5800
+ const transitionPlan = window.MindMapRenderPlan;
5801
+ transitionPlan?.beginResidentCssTransition?.(
5802
+ this, module, nodeObjectsById, previousLodBand, lodBand, lodSelectionKey);
5897
5803
  this.isInLODMode = shouldUseLOD;
5898
5804
  this._updateMeshVisibility(); // Use helper to respect loading state
5899
- console.log(`[LODRenderer] Mode switched to: ${shouldUseLOD ? 'LOD' : 'FULL-RES'} (Band=${lodBand}, Z=${cameraZ.toFixed(0)})`);
5900
5805
 
5901
5806
  if (this.isInLODMode) {
5902
5807
  this._lodVisualEpoch++;
@@ -5904,14 +5809,14 @@
5904
5809
  }
5905
5810
 
5906
5811
  if (shouldUseLOD) {
5907
- // LOD: instanced nodes are visible, halo is enabled in MID/FAR.
5908
5812
  this._finishFullResHandoff('enter-lod');
5909
5813
  this._clearFullResImageVisuals(nodeObjectsById);
5814
+ if ((this.imageInstanceData?.nodeIds?.length || 0) > 0) {
5815
+ perfTime('lodImageInstances', () => this._applyImageInstanceData());
5816
+ }
5910
5817
  this._fullResMotionHiddenGlIds.clear();
5911
5818
  this._currentLodBand = lodBand;
5912
- if (lodBand === 'MID') {
5913
- this._markLineDetailsDirty();
5914
- }
5819
+ transitionPlan?.ensureResidentLineDetails?.(this, lodBand);
5915
5820
  if (this.instancedHaloMesh) this.instancedHaloMesh.visible = shouldShowInstancedHaloInLodBand(lodBand);
5916
5821
  this.instancedMesh.visible = true;
5917
5822
  if (this.isLoading) {
@@ -5923,7 +5828,6 @@
5923
5828
  this._syncInstancedImageMeshVisibility();
5924
5829
  }
5925
5830
 
5926
- // Keep the resident MID/FAR instance buffers; only real node mutations mark them dirty.
5927
5831
  const cssIdsToHide = new Set(this._nearCssVisibleIds);
5928
5832
  for (let i = this._css3dQueueCursor || 0; i < this._css3dQueue.length; i++) {
5929
5833
  const item = this._css3dQueue[i];
@@ -5933,17 +5837,14 @@
5933
5837
  cssIdsToHide.add(nodeId);
5934
5838
  }
5935
5839
  const currentNodeMap = this._module?.nodeObjectsById;
5936
- for (const nodeId of this._collectResidentModeCss3dCleanupIds(currentNodeMap)) {
5937
- cssIdsToHide.add(nodeId);
5938
- }
5840
+ const activeCssOwnerId = module?.draggedNodeId || this._getCurrentHoveredNodeId(module);
5841
+ for (const nodeId of this._activeFullResCssNodeIds) cssIdsToHide.add(nodeId);
5939
5842
  this._clearCss3dQueue();
5940
5843
  this._clearCss3dPrewarmQueue();
5941
5844
  this._css3dProcessing = false;
5942
5845
  this._nearCssVisibleIds.clear();
5943
5846
  this._lastNearCssNoWorkKey = '';
5944
5847
 
5945
- // Hide only CSS3D nodes that were actually on-screen; the LOD cleanup queue
5946
- // catches any stale off-screen DOM without blocking the wheel transition.
5947
5848
  const visibleIds = this._getNearVisibleIds(module);
5948
5849
  const prioritizedWarm = currentNodeMap && visibleIds
5949
5850
  ? this._getNearVisiblePriority(visibleIds, currentNodeMap, camera)
@@ -5951,14 +5852,35 @@
5951
5852
  this._primeWarmCss3dCache(prioritizedWarm, currentNodeMap, lodBand);
5952
5853
  this._queueCss3dPrewarm(prioritizedWarm, currentNodeMap, module, lodBand);
5953
5854
  if (currentNodeMap && cssIdsToHide.size > 0) {
5954
- const hideStart = performance.now();
5955
5855
  perfTime('lodCleanup', () => {
5956
5856
  for (const nodeId of cssIdsToHide) {
5957
5857
  const entry = currentNodeMap.get(nodeId);
5958
5858
  if (!entry) continue;
5959
5859
  const model = entry.model;
5960
5860
  const isLivePortalCss = isLivePortalCssLodModel(model);
5861
+ const isTextLike = isTextLikeModel(model);
5961
5862
  const shouldKeepConsoleProxy = false;
5863
+ if (!isLivePortalCss && isTextLike) {
5864
+ transitionPlan?.parkFullResCss?.(
5865
+ this, entry, module, currentNodeMap, lodSelectionKey, 'text');
5866
+ if (entry.glObject) this._hideTextLodFullVisual(entry);
5867
+ this._setGlowVisibility(entry, false);
5868
+ continue;
5869
+ }
5870
+ const contentType = getNodeContentType(model);
5871
+ const isNodeSelected = isNodeSelectedForLod(nodeId, multiSelectedIds, selectedNodeId);
5872
+ if (!isLivePortalCss && contentType === 'image' &&
5873
+ transitionPlan?.parkReadyResidentImage?.(
5874
+ this, entry, module, currentNodeMap, lodSelectionKey, nodeId,
5875
+ isAnimatedGifImageModel(model), isNodeSelected, activeCssOwnerId === nodeId)) continue;
5876
+ const element = entry.cssObject?.element || null;
5877
+ if (!isLivePortalCss && isCssMediaFallbackModel(model) &&
5878
+ entry.cssObject?.visible === true && element && element.style.display !== 'none') {
5879
+ this._lodImageCssFallbackNodeIds.add(nodeId);
5880
+ entry.currentType = 'GL';
5881
+ if (entry.glObject) this._hideGlBodyOnly(entry);
5882
+ continue;
5883
+ }
5962
5884
  if (entry.cssObject?.element) {
5963
5885
  this._hideAgentConsoleLodProxy(entry);
5964
5886
  }
@@ -5979,16 +5901,11 @@
5979
5901
  entry.currentType = 'GL';
5980
5902
  }
5981
5903
 
5982
- // Text-like nodes: hide GL body (InstancedMesh renders them)
5983
- const isTextLike = isTextLikeModel(model);
5984
- const contentType = getNodeContentType(model);
5985
5904
  const isImage = isImageLodProxyModel(model);
5986
- const isNodeSelected = isNodeSelectedForLod(nodeId, multiSelectedIds, selectedNodeId);
5987
5905
  if (!isLivePortalCss && isTextLike && entry.glObject) {
5988
5906
  this._hideTextLodFullVisual(entry);
5989
5907
  } else if (isImage) {
5990
5908
  if (shouldKeepCssMediaFallback) {
5991
- // Visible media stays on the CSS fallback path until its resident proxy is ready.
5992
5909
  } else {
5993
5910
  if (contentType === 'image' && !isCss3dRendererEnabled(module)) {
5994
5911
  this._syncImageLodTransitionVisual(entry, lodBand, isNodeSelected, module);
@@ -6000,8 +5917,11 @@
6000
5917
 
6001
5918
  this._setGlowVisibility(entry, false);
6002
5919
  }
5920
+ if (module) {
5921
+ module._css3dVisualChangedThisFrame = true;
5922
+ module._css3dVisualChangedSinceLastRender = true;
5923
+ }
6003
5924
  });
6004
- console.log(`[LODRenderer] CSS3D hidden synchronously in ${(performance.now() - hideStart).toFixed(1)}ms (${cssIdsToHide.size} active)`);
6005
5925
  }
6006
5926
 
6007
5927
  if (module?.renderDebugFlags?.enableCss3dWillChange === true &&
@@ -6016,8 +5936,6 @@
6016
5936
 
6017
5937
  } else {
6018
5938
  perfTime('fullResSetup', () => {
6019
- // Full-res: NEAR / NORMAL. MID/FAR use resident instances.
6020
- const tNearStart = performance.now();
6021
5939
  if (this.instancedHaloMesh) this.instancedHaloMesh.visible = false;
6022
5940
  if (this.instancedConsoleMesh) this.instancedConsoleMesh.visible = false;
6023
5941
  this._startFullResHandoff(previousLodBand, 'enter-full-res', this._fullResHandoffMaxMs);
@@ -6044,15 +5962,13 @@
6044
5962
  this._lastNearCssNoWorkKey = '';
6045
5963
  const nearVisibleIds = this._getNearVisibleIds(module);
6046
5964
  this._clearWarmCss3dCache(this._module?.nodeObjectsById, nearVisibleIds);
6047
- this._nearEntryBoostFrames = this._getAdaptiveNearEntryBoostFrames(nearVisibleIds?.size || 0, module);
5965
+ this._nearEntryBoostFrames = 0;
6048
5966
  this._lodCleanupPending = true;
6049
5967
 
6050
5968
  if (module?.renderDebugFlags?.enableCss3dWillChange === true &&
6051
5969
  window.MindMapCss3DManager?.setWillChange) {
6052
5970
  window.MindMapCss3DManager.setWillChange(true, module);
6053
5971
  }
6054
- const tModeEnd = performance.now();
6055
- console.log(`[LODRenderer] ⏱️ Full-res band=${lodBand}: setup ${(tModeEnd - tNearStart).toFixed(1)}ms (visible-only CSS3D activation)`);
6056
5972
  // ▲▲▲ [FIX] ▲▲▲
6057
5973
  });
6058
5974
  }
@@ -6091,8 +6007,12 @@
6091
6007
  // ▼▼▼ [Fix] Keep CSS3D visibility synced while panning in near mode ▼▼▼
6092
6008
  if (!this.isInLODMode) {
6093
6009
  this._currentLodBand = lodBand;
6094
- this._restoreVisibleCss3dHighDetailForNear(nodeObjectsById, module, lodBand);
6095
6010
  const visibleIds = this._getNearVisibleIds(module);
6011
+ const renderPlan = window.MindMapRenderPlan;
6012
+ const css3dEnabled = isCss3dRendererEnabled(module);
6013
+ const stableFullResKey = renderPlan?.buildFullResKey(lodBand, visibilityVersion, lodSelectionKey, nodeObjectsById.size, visibleIds?.size || 0, this._nearCssVisibleIds.size, this._activeFullResCssNodeIds.size, css3dEnabled, module, this);
6014
+ if (visibleIds?.size > 0 && renderPlan?.shouldSkipStableFullRes(this, module, stableFullResKey, bandChangedWithoutModeSwitch)) return;
6015
+ this._restoreVisibleCss3dHighDetailForNear(nodeObjectsById, module, lodBand);
6096
6016
  if (!visibleIds || visibleIds.size === 0) {
6097
6017
  perfTime('fullResHandoff', () => {
6098
6018
  this._activeFullResCssNodeIds.clear();
@@ -6117,7 +6037,7 @@
6117
6037
  }
6118
6038
 
6119
6039
  perfTime('fullResImages', () => {
6120
- this._syncFullResImageVisuals(visibleIds, nodeObjectsById, module, lodBand);
6040
+ this._syncFullResImageVisuals(visibleIds, nodeObjectsById, module, lodBand, lodSelectionKey);
6121
6041
  });
6122
6042
 
6123
6043
  const prioritizedVisible = this._getNearVisiblePriority(visibleIds, nodeObjectsById, camera);
@@ -6172,17 +6092,8 @@
6172
6092
  return;
6173
6093
  }
6174
6094
 
6175
- const nearCssNoWorkKey = `${lodBand}|${visibilityVersion}|${nodeObjectsById.size}|${this._nearCssVisibleIds.size}|${allowedCssIds.size}`;
6176
- if (this._lastNearCssNoWorkKey === nearCssNoWorkKey &&
6177
- !this._hasCss3dQueueItems() &&
6178
- this._css3dQueuedMap.size === 0 &&
6179
- this._css3dProcessing !== true &&
6180
- this._nearEntryBoostFrames <= 0 &&
6181
- this._fullResHandoffActive !== true &&
6182
- cssReadinessDrift !== true &&
6183
- !bandChangedWithoutModeSwitch) {
6184
- return;
6185
- }
6095
+ const nearCssNoWorkKey = renderPlan?.buildFullResKey(lodBand, visibilityVersion, lodSelectionKey, nodeObjectsById.size, visibleIds.size, this._nearCssVisibleIds.size, allowedCssIds.size, css3dEnabled, module, this);
6096
+ if (renderPlan?.commitStableFullRes(this, module, nearCssNoWorkKey, bandChangedWithoutModeSwitch, cssReadinessDrift)) return;
6186
6097
 
6187
6098
  let hasNearCssWork =
6188
6099
  this._hasCss3dQueueItems() ||
@@ -6260,9 +6171,13 @@
6260
6171
  // visible CSS3D surface in-place so the expanded NORMAL
6261
6172
  // band cannot show a blank low-detail shell while a
6262
6173
  // deferred queue waits for motion/cooldown.
6263
- this._showNodeCss3d(entry, module);
6174
+ if (renderPlan?.restoreParkedFullResCss?.(
6175
+ this, entry, module, nodeObjectsById, lodSelectionKey, lodBand, nodeId) !== true) {
6176
+ this._showNodeCss3d(entry, module);
6177
+ }
6264
6178
  processedCount++;
6265
6179
  }
6180
+ renderPlan?.completeFullResTransition?.(this, allowedCssIds.size);
6266
6181
  });
6267
6182
 
6268
6183
  perfTime('fullResHandoff', () => {
@@ -6425,6 +6340,10 @@
6425
6340
  });
6426
6341
  }
6427
6342
 
6343
+ const committedResidentTextCount = window.MindMapRenderPlan?.syncCommittedResidentTextCount?.(
6344
+ this, hasResidentLodInstances, FORCE_RESIDENT_INSTANCES_IN_MID_FAR === true) ?? -1;
6345
+ if (committedResidentTextCount >= 0) totalTextLikeCount = committedResidentTextCount;
6346
+
6428
6347
  const nextVisibleSubsetMode = shouldUseLOD && this._shouldUseVisibleSubsetLod(totalTextLikeCount, lodBand);
6429
6348
  if (this._lodUsesVisibleSubset !== nextVisibleSubsetMode) {
6430
6349
  this._lodUsesVisibleSubset = nextVisibleSubsetMode;
@@ -6450,9 +6369,7 @@
6450
6369
  } else if (lodBand === 'MID') {
6451
6370
  this._setInstancedBaseColor(0xffffff);
6452
6371
  this._setInstancedDefaultBorderColor(0xd1d5db);
6453
- if (previousResidentBand === 'FAR') {
6454
- this._markLineDetailsDirty();
6455
- }
6372
+ if (previousResidentBand === 'FAR') window.MindMapRenderPlan?.ensureResidentLineDetails?.(this, lodBand);
6456
6373
  }
6457
6374
  }
6458
6375
  const shouldSyncTemplateCss3d =
@@ -6590,7 +6507,7 @@
6590
6507
  }
6591
6508
  this._updateInstanceData(nodeObjectsById);
6592
6509
  this._applyInstanceData();
6593
- this._lineDetailsPatchPending = false;
6510
+ this._lineDetailsPatchPending = this._residentLineFallbackUsed === true;
6594
6511
  if (fullBuildReason !== 'line-details') {
6595
6512
  this._noteResidentFullBuild(fullBuildReason, lodBand);
6596
6513
  }
@@ -6660,8 +6577,9 @@
6660
6577
  }
6661
6578
  }
6662
6579
 
6663
- _updateInstanceData(nodeObjectsById) {
6664
- const positions = [];
6580
+ _updateInstanceData(nodeObjectsById) {
6581
+ this._residentLineFallbackUsed = false;
6582
+ const positions = [];
6665
6583
  const sizes = [];
6666
6584
  const selections = [];
6667
6585
  const seeds = [];
@@ -6872,8 +6790,10 @@
6872
6790
  }
6873
6791
  }
6874
6792
  } else {
6875
- // Build actual line length data from text
6876
- const lineData = this._getLineDataForNode(model, entry.lodSeed, w, h, entry);
6793
+ const lineData = window.MindMapRenderPlan?.resolveResidentLineData?.(
6794
+ this, entry, w, h, this._residentBuilt === true,
6795
+ MAX_LINE_SAMPLES, LINE_HEIGHT_PX, LINE_PADDING_PX
6796
+ ) || this._getLineDataForNode(model, entry.lodSeed, w, h, entry);
6877
6797
  lineCounts.push(lineData.lineCount);
6878
6798
  lineHeights.push(lineData.lineHeightPx || LINE_HEIGHT_PX);
6879
6799
  linePaddings.push(lineData.paddingX || LINE_PADDING_PX, lineData.paddingY || LINE_PADDING_PX);
@@ -6912,7 +6832,7 @@
6912
6832
  this._cachedImageNodeCount = imageNodeIds.length;
6913
6833
  }
6914
6834
 
6915
- _updateImageInstanceData(nodeObjectsById) {
6835
+ _updateImageInstanceData(nodeObjectsById, prepareOnly = false) {
6916
6836
  const imagePositions = [];
6917
6837
  const imageSizes = [];
6918
6838
  const imageSelections = [];
@@ -6939,13 +6859,13 @@
6939
6859
 
6940
6860
  if (contentType === 'image' &&
6941
6861
  this._shouldUseCssImageFallbackInResidentLod(this._module, this._currentLodBand)) {
6942
- this._hideImageLodFullVisual(entry);
6862
+ if (!prepareOnly) this._hideImageLodFullVisual(entry);
6943
6863
  return;
6944
6864
  }
6945
6865
 
6946
6866
  const obj = entry.glObject || entry.cssObject;
6947
6867
  if (!obj) {
6948
- this._hideImageLodFullVisual(entry);
6868
+ if (!prepareOnly) this._hideImageLodFullVisual(entry);
6949
6869
  return;
6950
6870
  }
6951
6871
 
@@ -6956,7 +6876,7 @@
6956
6876
  entry.isVisibleInFrustum === true ||
6957
6877
  isNodeSelected === true;
6958
6878
  if (!shouldKeepImageInstance) {
6959
- this._hideImageLodFullVisual(entry);
6879
+ if (!prepareOnly) this._hideImageLodFullVisual(entry);
6960
6880
  return;
6961
6881
  }
6962
6882
 
@@ -6971,7 +6891,7 @@
6971
6891
  if (!isRetryDeferred && isLoadPending) {
6972
6892
  imageBatchPendingIds.add(nodeId);
6973
6893
  }
6974
- if (contentType === 'image') {
6894
+ if (!prepareOnly && contentType === 'image') {
6975
6895
  this._syncImageLodTransitionVisual(
6976
6896
  entry,
6977
6897
  this._currentLodBand || 'MID',
@@ -6979,7 +6899,7 @@
6979
6899
  this._module,
6980
6900
  interactionState
6981
6901
  );
6982
- } else {
6902
+ } else if (!prepareOnly) {
6983
6903
  this._syncCssMediaLodFallback(entry, this._module);
6984
6904
  }
6985
6905
  return;
@@ -6995,10 +6915,10 @@
6995
6915
  imageUvRects.push(uvRect[0], uvRect[1], uvRect[2], uvRect[3]);
6996
6916
  imageNodeIds.push(nodeId);
6997
6917
  this._imageNodeIdToIndex.set(nodeId, imageNodeIds.length - 1);
6998
- if (contentType !== 'image') {
6918
+ if (!prepareOnly && contentType !== 'image') {
6999
6919
  this._hideCssMediaLodFallback(entry);
7000
6920
  }
7001
- this._hideImageLodFullVisual(entry);
6921
+ if (!prepareOnly) this._hideImageLodFullVisual(entry);
7002
6922
  });
7003
6923
 
7004
6924
  if (imageBatchPendingIds.size > 0) {
@@ -7163,35 +7083,7 @@
7163
7083
  }
7164
7084
 
7165
7085
  _hasAnimatedGifCssFallbackCandidates(module = this._module) {
7166
- const nodeObjectsById = module?.nodeObjectsById || this._module?.nodeObjectsById || null;
7167
- if (!nodeObjectsById) {
7168
- return false;
7169
- }
7170
-
7171
- const candidateIds = [];
7172
- const pushId = (value) => {
7173
- const id = String(value || '').trim();
7174
- if (id) candidateIds.push(id);
7175
- };
7176
-
7177
- module?._visibleIds?.forEach?.(pushId);
7178
- module?._prevVisibleIds?.forEach?.(pushId);
7179
- pushId(module?.selectedNodeIdJs);
7180
- module?.multiSelectedNodeIds?.forEach?.(pushId);
7181
- this._lodImageCssFallbackNodeIds?.forEach?.(pushId);
7182
-
7183
- const seenIds = new Set();
7184
- for (const nodeId of candidateIds) {
7185
- if (seenIds.has(nodeId)) continue;
7186
- seenIds.add(nodeId);
7187
-
7188
- const entry = nodeObjectsById.get(nodeId) || null;
7189
- if (isAnimatedGifImageModel(entry?.model)) {
7190
- return true;
7191
- }
7192
- }
7193
-
7194
- return false;
7086
+ return window.MindMapRenderPlan?.hasAnimatedGifCssFallbackCandidates?.(this, module, isAnimatedGifImageModel) === true;
7195
7087
  }
7196
7088
 
7197
7089
  _shouldPreserveCssMediaFallbackForInteraction(entry, module = this._module) {
@@ -7589,6 +7481,7 @@
7589
7481
  }
7590
7482
 
7591
7483
  const nodeIds = Array.isArray(this.instanceData?.nodeIds) ? this.instanceData.nodeIds : [];
7484
+ if (window.MindMapRenderPlan?.areResidentLinesPrepared?.(this, nodeObjectsById, nodeIds) === false) return false;
7592
7485
  const lineCounts = this.instanceData?.lineCounts || [];
7593
7486
  const lineLengths = this.instanceData?.lineLengths || [];
7594
7487
  const lineHeights = this.instanceData?.lineHeights || [];
@@ -7614,7 +7507,8 @@
7614
7507
  const model = entry.model;
7615
7508
  const w = model.width || 400;
7616
7509
  const h = model.height || 200;
7617
- const lineData = this._getLineDataForNode(model, entry.lodSeed, w, h, entry);
7510
+ const lineData = window.MindMapRenderPlan?.getPreparedLineData?.(this, entry, w, h) ||
7511
+ this._getLineDataForNode(model, entry.lodSeed, w, h, entry);
7618
7512
  const base = i * MAX_LINE_SAMPLES;
7619
7513
 
7620
7514
  lineCounts[i] = lineData.lineCount || 0;
@@ -8410,18 +8304,16 @@
8410
8304
  const consoleCount = consolePositions.length / 3;
8411
8305
  const imageCount = imagePositions.length / 3;
8412
8306
 
8413
- // Resize buffers if needed (simple resize strategy: if full, create bigger)
8414
- // For now assuming 10000 is enough, or it will just slice (visual glitch but no crash)
8415
- const max = positionAttr.count;
8307
+ const max = positionAttr.count;
8416
8308
  if (count > max) {
8417
8309
  console.warn('[LODRenderer] Buffer overflow, need to resize (omitted for safety)');
8418
8310
  }
8419
8311
 
8420
8312
  const safeCount = Math.min(count, max);
8313
+ window.MindMapRenderPlan?.ensureLineTextureCapacity?.(this, safeCount, MAX_LINE_SAMPLES);
8421
8314
  let lineRowsRewritten = false;
8422
8315
  const suspendLineDetailsForPan = this._isPanLineDetailSuspended();
8423
8316
 
8424
- // Update attribute data
8425
8317
  for (let i = 0; i < safeCount; i++) {
8426
8318
  positionAttr.setXYZ(i, positions[i * 3], positions[i * 3 + 1], positions[i * 3 + 2]);
8427
8319
  sizeAttr.setXY(i, sizes[i * 2], sizes[i * 2 + 1]);