@mindexec/cli 0.2.434 → 0.2.436

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mindexec/cli",
3
- "version": "0.2.434",
3
+ "version": "0.2.436",
4
4
  "description": "MindExec local runtime and bridge CLI",
5
5
  "main": "server.js",
6
6
  "type": "module",
@@ -127,7 +127,14 @@
127
127
  const LOD_PAN_VISIBILITY_REFRESH_INTERVAL_MS = 120;
128
128
  const CAMERA_LERP_BASE_FRAME_MS = 1000 / 60;
129
129
  const CAMERA_KEYBOARD_PAN_LERP_60HZ = 0.38;
130
- const CAMERA_MOUSE_PAN_LERP_60HZ = 0.46;
130
+ const CAMERA_MOUSE_PAN_LERP_60HZ = 0.30;
131
+ const CAMERA_MOUSE_PAN_SNAP_SCREEN_PX = 0.1;
132
+ const RECENT_PAN_INPUT_WINDOW_MS = 220;
133
+ const CAMERA_PAN_INPUT_WAKE_REASONS = new Set([
134
+ 'pan-input',
135
+ 'trackpad-pan-input',
136
+ 'ctrl-wheel-pan-input'
137
+ ]);
131
138
  const NODE_ADD_FRAME_BUDGET_MS = 6;
132
139
  const NODE_ADD_LOADING_FRAME_BUDGET_MS = 8;
133
140
  const NODE_ADD_MIN_SLICE = 8;
@@ -388,6 +395,22 @@
388
395
  return 1 - Math.pow(1 - safeBase, frameScale);
389
396
  }
390
397
 
398
+ function getCameraPanSnapWorldThreshold(module) {
399
+ const camera = module?.camera;
400
+ if (!camera?.isPerspectiveCamera) {
401
+ return 0.001;
402
+ }
403
+
404
+ const cameraZ = Math.max(1, Math.abs(Number(camera.position?.z || 1)));
405
+ const viewportHeight = Math.max(
406
+ 1,
407
+ Number(module?._lastViewportHeight || module?._lastContainerHeight || 1080)
408
+ );
409
+ const fovDegrees = Math.max(1, Math.min(179, Number(camera.fov || 45)));
410
+ const viewHeight = 2 * Math.tan((fovDegrees * Math.PI) / 360) * cameraZ;
411
+ return Math.max(0.001, (viewHeight / viewportHeight) * CAMERA_MOUSE_PAN_SNAP_SCREEN_PX);
412
+ }
413
+
391
414
  const _mindCanvasPerfClassState = {
392
415
  moving: false,
393
416
  dense: false,
@@ -970,9 +993,10 @@
970
993
  const cameraZ = Number(camera.position?.z || 0);
971
994
  const cameraX = Number(camera.position?.x || 0);
972
995
  const cameraY = Number(camera.position?.y || 0);
996
+ const isPanMotionActive = module.isPanning === true || module._panSmoothingActive === true;
973
997
  const marginPolicy = getVisibilityMarginPolicy(
974
998
  cameraZ,
975
- module.isPanning === true,
999
+ isPanMotionActive,
976
1000
  isCtrlWheelVerticalScrolling
977
1001
  );
978
1002
  const cullingMarginPx = marginPolicy.cullingMarginPx;
@@ -989,7 +1013,7 @@
989
1013
  let leadTopCells = 0;
990
1014
  let leadBottomCells = 0;
991
1015
 
992
- if (module.isPanning === true && cameraZ < 12000) {
1016
+ if (isPanMotionActive && cameraZ < 12000) {
993
1017
  const panLeadWorld = Math.min(
994
1018
  rawMarginWorld * (cameraZ < 4500 ? 1.35 : 1.1),
995
1019
  cellSize * Math.max(1, maxMarginCells)
@@ -3044,6 +3068,8 @@ ${summaryLines.map(line => `<div>${escapeNodeFrameDebugHtml(line)}</div>`).join(
3044
3068
  this.nodeObjectsById = new Map();
3045
3069
  this.lodUpdateQueue = []; // Initialize lodUpdateQueue
3046
3070
  this.isPanning = false;
3071
+ this._panSmoothingActive = false;
3072
+ this._resetPanLerpDeltaOnNextFrame = false;
3047
3073
  this._currentDpr = null;
3048
3074
  this._lastViewportWidth = 0;
3049
3075
  this._lastViewportHeight = 0;
@@ -4105,12 +4131,24 @@ ${summaryLines.map(line => `<div>${escapeNodeFrameDebugHtml(line)}</div>`).join(
4105
4131
  }
4106
4132
 
4107
4133
  requestAnimationWake(reason = 'runtime-wake') {
4134
+ const wakeReason = String(reason || 'runtime-wake');
4135
+ const isPanInputWake = CAMERA_PAN_INPUT_WAKE_REASONS.has(wakeReason);
4136
+ if (isPanInputWake) {
4137
+ this._lastPanInputAt = typeof performance !== 'undefined' && typeof performance.now === 'function'
4138
+ ? performance.now()
4139
+ : Date.now();
4140
+ this._panSmoothingActive = true;
4141
+ }
4142
+
4108
4143
  if (this._animationLoopRunning !== true || this._animationLoopExecuting === true) {
4109
4144
  return;
4110
4145
  }
4111
4146
 
4112
- const wakeReason = String(reason || 'runtime-wake');
4113
4147
  this._lastAnimationWakeReason = wakeReason;
4148
+ const resumedFromIdleCadence = !!this._animationIdleTimerId;
4149
+ if (resumedFromIdleCadence && isPanInputWake) {
4150
+ this._resetPanLerpDeltaOnNextFrame = true;
4151
+ }
4114
4152
  if (this._animationIdleTimerId) {
4115
4153
  clearTimeout(this._animationIdleTimerId);
4116
4154
  this._animationIdleTimerId = null;
@@ -4880,7 +4918,7 @@ ${summaryLines.map(line => `<div>${escapeNodeFrameDebugHtml(line)}</div>`).join(
4880
4918
 
4881
4919
  this._deferredDprTimer = setTimeout(() => {
4882
4920
  this._deferredDprTimer = null;
4883
- if (this.isZooming || this.isPanning || this.isDraggingNode || this.isDraggingMultipleNodes || this.isResizing || this.isWindowResizing) {
4921
+ if (this.isZooming || this.isPanning || this._panSmoothingActive || this.isDraggingNode || this.isDraggingMultipleNodes || this.isResizing || this.isWindowResizing) {
4884
4922
  this._scheduleDeferredRendererPixelRatioApply(delayMs);
4885
4923
  return;
4886
4924
  }
@@ -5274,6 +5312,7 @@ ${summaryLines.map(line => `<div>${escapeNodeFrameDebugHtml(line)}</div>`).join(
5274
5312
  (typeof performance !== 'undefined' && typeof performance.now === 'function')
5275
5313
  ? performance.now()
5276
5314
  : Date.now();
5315
+ const isPanMotionActive = this.isPanning === true || this._panSmoothingActive === true;
5277
5316
 
5278
5317
  let cullingMarginPx = 3000;
5279
5318
  let maxMarginCells = 6;
@@ -5297,7 +5336,7 @@ ${summaryLines.map(line => `<div>${escapeNodeFrameDebugHtml(line)}</div>`).join(
5297
5336
  const cameraZ = Number(camera.position?.z || 0);
5298
5337
  const marginPolicy = getVisibilityMarginPolicy(
5299
5338
  cameraZ,
5300
- this.isPanning === true,
5339
+ isPanMotionActive,
5301
5340
  isCtrlWheelVerticalScrolling
5302
5341
  );
5303
5342
  cullingMarginPx = marginPolicy.cullingMarginPx;
@@ -5340,7 +5379,7 @@ ${summaryLines.map(line => `<div>${escapeNodeFrameDebugHtml(line)}</div>`).join(
5340
5379
  }
5341
5380
  }
5342
5381
 
5343
- if (this.isPanning === true && cameraZ < 12000) {
5382
+ if (isPanMotionActive && cameraZ < 12000) {
5344
5383
  const panLeadWorld = Math.min(
5345
5384
  rawMarginWorld * (cameraZ < 4500 ? 1.35 : 1.1),
5346
5385
  cellSize * Math.max(1, maxMarginCells)
@@ -5398,7 +5437,7 @@ ${summaryLines.map(line => `<div>${escapeNodeFrameDebugHtml(line)}</div>`).join(
5398
5437
  const previousVisibilityNodeCount = Number(this._lastVisibilityNodeCount || 0);
5399
5438
  const canAttemptIncrementalFullResPan =
5400
5439
  isFullResMode &&
5401
- this.isPanning === true &&
5440
+ isPanMotionActive &&
5402
5441
  this.isZooming !== true &&
5403
5442
  this._fullResMotionFrozen !== true &&
5404
5443
  this.isWindowResizing !== true &&
@@ -5780,7 +5819,8 @@ ${summaryLines.map(line => `<div>${escapeNodeFrameDebugHtml(line)}</div>`).join(
5780
5819
  const frameDelta = frameStart - (this._lastFrameTime || frameStart);
5781
5820
  this._lastFrameTime = frameStart;
5782
5821
  this._lastFrameDeltaMs = Number.isFinite(frameDelta) ? frameDelta : 0;
5783
- if (frameDelta > 0 && Number.isFinite(frameDelta)) {
5822
+ const shouldIgnoreIdleWakeFrameDelta = this._resetPanLerpDeltaOnNextFrame === true;
5823
+ if (frameDelta > 0 && Number.isFinite(frameDelta) && !shouldIgnoreIdleWakeFrameDelta) {
5784
5824
  const previousAvgFrameTime = Number(this._avgFrameTime || frameDelta);
5785
5825
  const nextAvgFrameTime = previousAvgFrameTime + ((frameDelta - previousAvgFrameTime) * 0.15);
5786
5826
  this._avgFrameTime = nextAvgFrameTime;
@@ -5857,20 +5897,54 @@ ${summaryLines.map(line => `<div>${escapeNodeFrameDebugHtml(line)}</div>`).join(
5857
5897
  // ?�▼??[Changed] Time-normalized camera smoothing ?�▼?? // Mouse move events can arrive at an uneven cadence even when rAF is 60fps.
5858
5898
  // Keep zoom instant, but smooth mouse pan enough to hide input sampling jitter.
5859
5899
  const sZoom = 1.0;
5860
- const panLerp60hz = this.isPanning
5900
+ const isRecentPanInput =
5901
+ (frameStart - Number(this._lastPanInputAt || 0)) <= RECENT_PAN_INPUT_WINDOW_MS;
5902
+ const panSnapWorldThreshold = getCameraPanSnapWorldThreshold(this);
5903
+ const cameraTargetX = Number(this.targetX ?? this.camera.position.x);
5904
+ const cameraTargetY = Number(this.targetY ?? this.camera.position.y);
5905
+ const pendingPanDistanceBeforeLerp = Math.hypot(
5906
+ cameraTargetX - Number(this.camera.position.x || 0),
5907
+ cameraTargetY - Number(this.camera.position.y || 0)
5908
+ );
5909
+ let isPanSmoothingActive = this.isZooming !== true && (
5910
+ this.isPanning === true ||
5911
+ ((isRecentPanInput || this._panSmoothingActive === true) &&
5912
+ pendingPanDistanceBeforeLerp > panSnapWorldThreshold)
5913
+ );
5914
+ const panLerp60hz = isPanSmoothingActive
5861
5915
  ? CAMERA_MOUSE_PAN_LERP_60HZ
5862
5916
  : CAMERA_KEYBOARD_PAN_LERP_60HZ;
5863
- const sPan = this.isZooming ? 1.0 : normalizeFrameLerp(panLerp60hz, frameDelta);
5917
+ const panFrameDelta = this._resetPanLerpDeltaOnNextFrame === true
5918
+ ? CAMERA_LERP_BASE_FRAME_MS
5919
+ : frameDelta;
5920
+ this._resetPanLerpDeltaOnNextFrame = false;
5921
+ const sPan = this.isZooming ? 1.0 : normalizeFrameLerp(panLerp60hz, panFrameDelta);
5864
5922
  // ?�▲??[Changed] ?�▲??
5865
5923
  this._tmpOldCamPos.copy(this.camera.position);
5866
- this.camera.position.x += (this.targetX - this.camera.position.x) * sPan;
5867
- this.camera.position.y += (this.targetY - this.camera.position.y) * sPan;
5924
+ this.camera.position.x += (cameraTargetX - this.camera.position.x) * sPan;
5925
+ this.camera.position.y += (cameraTargetY - this.camera.position.y) * sPan;
5868
5926
  this.camera.position.z += (this.targetZ - this.camera.position.z) * sZoom;
5869
5927
 
5928
+ let remainingPanDistance = Math.hypot(
5929
+ cameraTargetX - Number(this.camera.position.x || 0),
5930
+ cameraTargetY - Number(this.camera.position.y || 0)
5931
+ );
5932
+ if (this.isZooming !== true &&
5933
+ this.isPanning !== true &&
5934
+ remainingPanDistance <= panSnapWorldThreshold) {
5935
+ this.camera.position.x = cameraTargetX;
5936
+ this.camera.position.y = cameraTargetY;
5937
+ remainingPanDistance = 0;
5938
+ isPanSmoothingActive = false;
5939
+ }
5940
+ this._panSmoothingActive = isPanSmoothingActive;
5941
+
5870
5942
  // Check delta
5871
5943
  const deltaMove = this.camera.position.distanceToSquared(this._tmpOldCamPos);
5872
5944
  const didCameraPositionChange = deltaMove > 0.0001;
5873
- const isCameraMoving = didCameraPositionChange || hasFreshImmediateCameraFeedbackAtFrameStart;
5945
+ const isCameraMoving = didCameraPositionChange ||
5946
+ hasFreshImmediateCameraFeedbackAtFrameStart ||
5947
+ (isPanSmoothingActive && remainingPanDistance > panSnapWorldThreshold);
5874
5948
  if (isCameraMoving) {
5875
5949
  this._viewStatePersistAfterMotion = true;
5876
5950
  }
@@ -5941,13 +6015,10 @@ ${summaryLines.map(line => `<div>${escapeNodeFrameDebugHtml(line)}</div>`).join(
5941
6015
  const wasViewStateMotionFrozenLastFrame = this._wasViewStateMotionFrozenLastFrame === true;
5942
6016
  const CTRL_WHEEL_SCROLL_ACTIVE_WINDOW_MS = 140;
5943
6017
  const CAMERA_NAVIGATION_OVERLAY_ACTIVE_WINDOW_MS = 220;
5944
- const RECENT_PAN_INPUT_WINDOW_MS = 220;
5945
6018
  const isCtrlWheelVerticalScrolling =
5946
6019
  (frameStart - Number(this._ctrlWheelScrollInputTime || 0)) <= CTRL_WHEEL_SCROLL_ACTIVE_WINDOW_MS;
5947
6020
  const isCameraNavigationOverlayPriority =
5948
6021
  (frameStart - Number(this._cameraNavigationOverlayInputTime || 0)) <= CAMERA_NAVIGATION_OVERLAY_ACTIVE_WINDOW_MS;
5949
- const isRecentPanInput =
5950
- (frameStart - Number(this._lastPanInputAt || 0)) <= RECENT_PAN_INPUT_WINDOW_MS;
5951
6022
  const isCameraManipulatingForOverlay =
5952
6023
  this.isZooming === true ||
5953
6024
  this.isPanning === true ||
@@ -5972,7 +6043,12 @@ ${summaryLines.map(line => `<div>${escapeNodeFrameDebugHtml(line)}</div>`).join(
5972
6043
  this._wasPanningLastFrame = this.isPanning === true;
5973
6044
  this._wasCameraMovingLastFrame = isCameraMoving === true;
5974
6045
  this._wasResizingLastFrame = this.isResizing === true;
5975
- if (zoomEndedThisFrame || panEndedThisFrame || cameraEndedThisFrame || resizeEndedThisFrame) {
6046
+ const shouldFinalizeMotionThisFrame =
6047
+ zoomEndedThisFrame ||
6048
+ cameraEndedThisFrame ||
6049
+ resizeEndedThisFrame ||
6050
+ (panEndedThisFrame && isPanSmoothingActive !== true);
6051
+ if (shouldFinalizeMotionThisFrame) {
5976
6052
  this._lastMotionEndAt = frameStart;
5977
6053
  if (this._cursorDomDeferredForMotion === true) {
5978
6054
  updateCursorDomElement(this);
@@ -6130,7 +6206,7 @@ ${summaryLines.map(line => `<div>${escapeNodeFrameDebugHtml(line)}</div>`).join(
6130
6206
  const idleUpdateDue = false;
6131
6207
  const isLodPanVisibilityRefreshDue =
6132
6208
  !isResidentMidFarLodFrame &&
6133
- this.isPanning === true &&
6209
+ isPanSmoothingActive &&
6134
6210
  this.isZooming !== true &&
6135
6211
  this.useLODRendering === true &&
6136
6212
  this.lodRenderer?.isInLODMode === true &&
@@ -6260,7 +6336,7 @@ ${summaryLines.map(line => `<div>${escapeNodeFrameDebugHtml(line)}</div>`).join(
6260
6336
  Number(this._avgFrameTime || 0) >= FULL_RES_MOTION_FREEZE_FRAME_MS
6261
6337
  );
6262
6338
  const shouldAdaptiveFreezeMotionSafeguard =
6263
- (this.isZooming === true || this.isPanning === true) &&
6339
+ (this.isZooming === true || isPanSmoothingActive) &&
6264
6340
  this.lodRenderer?.isInLODMode === true &&
6265
6341
  isMotionHeavyScene &&
6266
6342
  isMotionFreezeEligible;
@@ -6287,7 +6363,7 @@ ${summaryLines.map(line => `<div>${escapeNodeFrameDebugHtml(line)}</div>`).join(
6287
6363
  this._wasViewStateMotionFrozenLastFrame = shouldFreezeFullResDuringMotion === true;
6288
6364
  const shouldThrottleVisibilityCullingDuringPan =
6289
6365
  isVisibilityCullingEnabled &&
6290
- this.isPanning === true &&
6366
+ isPanSmoothingActive &&
6291
6367
  this.isZooming !== true &&
6292
6368
  !shouldFreezeVisibilityDuringMotion &&
6293
6369
  !isNodeInteracting &&
@@ -6324,7 +6400,7 @@ ${summaryLines.map(line => `<div>${escapeNodeFrameDebugHtml(line)}</div>`).join(
6324
6400
  const shouldThrottleFullResVisibilityCullingDuringPan =
6325
6401
  isFullResMode === true &&
6326
6402
  isVisibilityCullingEnabled &&
6327
- this.isPanning === true &&
6403
+ isPanSmoothingActive &&
6328
6404
  this.isZooming !== true &&
6329
6405
  !shouldFreezeVisibilityDuringMotion &&
6330
6406
  !shouldFreezeFullResDuringMotion &&
@@ -6551,7 +6627,7 @@ ${summaryLines.map(line => `<div>${escapeNodeFrameDebugHtml(line)}</div>`).join(
6551
6627
  const isIncrementalPanVisibilityFrame =
6552
6628
  visibilityChangedThisFrame === true &&
6553
6629
  lastVisibilityProfile?.incrementalPanMode === true &&
6554
- this.isPanning === true &&
6630
+ isPanSmoothingActive &&
6555
6631
  this.isZooming !== true &&
6556
6632
  !isNodeInteracting &&
6557
6633
  !hasNonCameraNavigationForcedUpdate &&
@@ -1279,7 +1279,48 @@ window.MindMapInteractions = (function () {
1279
1279
  }
1280
1280
 
1281
1281
 
1282
- // ▼▼▼ [New] Double-click event handler ▼▼▼
1282
+ function resolveDoubleClickNodeId(module, e) {
1283
+ const domNodeId = getGuardedTargetNodeId(module, e?.target, e);
1284
+ if (domNodeId) return domNodeId;
1285
+ if (!module?.raycaster || !module?.camera || !module?.plane || !e) return null;
1286
+ updatePointer(module, e);
1287
+ module.raycaster.setFromCamera(module.pointer, module.camera);
1288
+ const meshes = [];
1289
+ let residentHit = null;
1290
+ if (module.raycaster.ray.intersectPlane(module.plane, module.intersectPoint)) {
1291
+ meshes.push(...getNearbyMeshes(module, module.intersectPoint));
1292
+ if (meshes.length === 0 && module.nodeObjectsById?.size > 0) {
1293
+ module.nodeObjectsById.forEach((entry) => {
1294
+ const glObject = entry?.glObject;
1295
+ if (glObject && (glObject.isMesh || glObject.children?.length > 0)) {
1296
+ meshes.push(glObject);
1297
+ }
1298
+ });
1299
+ }
1300
+ residentHit = module.lodRenderer?.pickResidentNodeAtWorldPoint?.(module.intersectPoint) || null;
1301
+ }
1302
+ const topHit = getTopIntersectionHit(module, module.raycaster.intersectObjects(meshes, true));
1303
+ const topNodeId = resolveNodeIdFromHitObject(topHit?.object);
1304
+ const topPriority = topHit ? getIntersectionInteractionPriority(module, topHit) : -Infinity;
1305
+ const cssHit = findCssNodeHitAtClientPoint(module, e.clientX, e.clientY);
1306
+ if (cssHit?.nodeId && (!topHit || (cssHit.nodeId !== topNodeId && (cssHit.priority ?? -Infinity) >= topPriority))) {
1307
+ return cssHit.nodeId;
1308
+ }
1309
+ return residentHit?.nodeId && (!topHit || getResidentLodHitPriority(module, residentHit) >= topPriority)
1310
+ ? residentHit.nodeId : topNodeId;
1311
+ }
1312
+
1313
+ function activateDoubleClickedNode(module, nodeId, target) {
1314
+ const nodeEntry = module?.nodeObjectsById?.get?.(nodeId) || null;
1315
+ if (!nodeEntry) return false;
1316
+ if (module.selectedNodeIdJs !== nodeId) {
1317
+ finalizeSingleSelection(module, nodeId, { notifyBlazor: true, bringToFront: true, showMenu: true });
1318
+ }
1319
+ const isLink = !!(target && (target.tagName === 'A' || target.closest?.('a')));
1320
+ handleSelectedNodeActivation(module, nodeId, nodeEntry, { isLink, target });
1321
+ return true;
1322
+ }
1323
+
1283
1324
  async function onDoubleClick(module, e) {
1284
1325
  cancelPendingPrimaryClick(module);
1285
1326
  if (module) {
@@ -1294,13 +1335,22 @@ window.MindMapInteractions = (function () {
1294
1335
  return;
1295
1336
  }
1296
1337
 
1338
+ if (e.target?.closest?.('textarea,input,select,[contenteditable="true"]')) {
1339
+ e.stopPropagation();
1340
+ return;
1341
+ }
1342
+
1297
1343
  e.preventDefault();
1298
1344
  e.stopPropagation();
1299
1345
 
1346
+ const nodeId = resolveDoubleClickNodeId(module, e);
1347
+ if (nodeId && activateDoubleClickedNode(module, nodeId, e.target)) {
1348
+ return;
1349
+ }
1350
+
1300
1351
  focusCanvasForClipboard(module);
1301
- moveCursorToEvent(module, e, 'double-click');
1352
+ moveCursorToEvent(module, e, 'grid double-click');
1302
1353
  }
1303
- // ▲▲▲ [New] ▲▲▲
1304
1354
 
1305
1355
  // ▼▼▼ [New] Helper to clean up an in-progress edit node ▼▼▼
1306
1356
  /**
@@ -3478,7 +3528,6 @@ window.MindMapInteractions = (function () {
3478
3528
 
3479
3529
  if (isDoubleClickPointerEvent(pointerEvent)) {
3480
3530
  cancelPendingPrimaryClick(module);
3481
- moveCursorToEvent(module, pointerEvent, 'overlay double-click');
3482
3531
  return { handled: true, suppressDefault: true };
3483
3532
  }
3484
3533
 
@@ -3540,7 +3589,7 @@ window.MindMapInteractions = (function () {
3540
3589
  }
3541
3590
 
3542
3591
  function beginOverlayNodeDrag(module, event, nodeId) {
3543
- if (!module || !event || event.button !== 0 || !nodeId) {
3592
+ if (!module || !event || Number(event.button) !== 0 || isDoubleClickPointerEvent(event) || !nodeId) {
3544
3593
  return false;
3545
3594
  }
3546
3595
 
@@ -3586,7 +3635,7 @@ window.MindMapInteractions = (function () {
3586
3635
 
3587
3636
  function startNodeDragFromDom(module, event, nodeId, options = {}) {
3588
3637
  const normalizedNodeId = String(nodeId || '').trim();
3589
- if (!module || !event || Number(event.button) !== 0 || !normalizedNodeId) {
3638
+ if (!module || !event || Number(event.button) !== 0 || isDoubleClickPointerEvent(event) || !normalizedNodeId) {
3590
3639
  return false;
3591
3640
  }
3592
3641
 
@@ -4646,70 +4695,12 @@ window.MindMapInteractions = (function () {
4646
4695
  // fallback이 너무 공격적으로 작동하여 노드 주변(배경) 클릭 시에도 노드를 선택하는 문제 발생.
4647
4696
  // 모든 노드는 Mesh를 가지고 있으므로 Raycast로 충분함.
4648
4697
  else if (module.intersectPoint && module.nodeObjectsById.size > 0) {
4649
- const clickX = module.intersectPoint.x;
4650
- const clickY = module.intersectPoint.y;
4651
- let foundNodeId = null;
4652
- let foundEntry = null;
4653
- let highestZ = -Infinity;
4654
-
4655
4698
  // 모든 노드를 순회하며 클릭 좌표가 노드 영역 내에 있는지 확인
4656
- module.nodeObjectsById.forEach((entry, nodeId) => {
4657
- if (!entry || !entry.glObject) return;
4658
-
4659
- const pos = entry.glObject.position;
4660
- const width = entry.model?.width || entry.glObject.userData?.worldWidth || 300;
4661
- const height = entry.model?.height || entry.glObject.userData?.worldHeight || 200;
4662
-
4663
4699
  // Three.js 좌표계: Y가 위로 증가, 노드 position은 좌상단
4664
- const left = pos.x;
4665
- const right = pos.x + width;
4666
- const top = pos.y;
4667
- const bottom = pos.y - height;
4668
-
4669
- if (clickX >= left && clickX <= right && clickY <= top && clickY >= bottom) {
4700
+ if (false) {
4670
4701
  // Z-order로 가장 위에 있는 노드 선택
4671
- const zOrder = entry.glObject.renderOrder || 0;
4672
- if (zOrder > highestZ) {
4673
- highestZ = zOrder;
4674
- foundNodeId = nodeId;
4675
- foundEntry = entry;
4676
- }
4677
- }
4678
- });
4679
-
4680
- if (foundNodeId && foundEntry) {
4681
- // If we found a node by coordinates, check if we hit a handle area?
4682
- // But handles are visible objects, so raycast should have caught them.
4683
- // If we are here, it means raycast failed (no mesh hit), but we are effectively clicking "Through" the empty space of a node?
4684
- // Or maybe the mesh was invisible.
4685
-
4686
- // However, for image resizing, the handles ARE visible now (opacity 0.01).
4687
- // So if we are here, it's likely dragging the body.
4688
-
4689
- console.log(`[onMouseDown] Coordinate-based hit: ${foundNodeId}`);
4690
- e.stopPropagation();
4691
- e.preventDefault();
4692
-
4693
- module.isShiftClick = e.shiftKey;
4694
-
4702
+ }
4695
4703
  // 다중 선택 해제 및 단일 선택 처리
4696
- const isAlreadySelected = (module.selectedNodeIdJs === foundNodeId);
4697
- if (!e.shiftKey && !isAlreadySelected && module.multiSelectedNodeIds?.size > 0) {
4698
- if (MindMapNodes.clearMultiSelection) {
4699
- MindMapNodes.clearMultiSelection(module);
4700
- }
4701
- module.multiSelectedNodeIds?.clear?.();
4702
- if (window.MindMapMultiSelect) {
4703
- window.MindMapMultiSelect.hide();
4704
- }
4705
- }
4706
-
4707
- if (!isAlreadySelected && !module.multiSelectedNodeIds?.has(foundNodeId)) {
4708
- setSingleSelection(module, foundNodeId, true);
4709
- }
4710
-
4711
- startNodeDrag(module, e, foundNodeId, foundEntry, foundEntry.glObject);
4712
- return; // Return to prevent falling through
4713
4704
  } else {
4714
4705
  // Background click
4715
4706
 
@@ -4152,6 +4152,8 @@
4152
4152
  (now - Number(module?._lastMotionEndAt || 0)) < 120;
4153
4153
  return this.isInLODMode !== true && (
4154
4154
  module?.isPanning === true ||
4155
+ module?._cameraMovingThisFrame === true ||
4156
+ module?._panSmoothingActive === true ||
4155
4157
  module?.isZooming === true ||
4156
4158
  module?.isDraggingNode === true ||
4157
4159
  module?.isDraggingMultipleNodes === true ||
@@ -7023,6 +7025,8 @@
7023
7025
 
7024
7026
  _isResidentLodInteractionActive(module = this._module) {
7025
7027
  return module?.isPanning === true ||
7028
+ module?._cameraMovingThisFrame === true ||
7029
+ module?._panSmoothingActive === true ||
7026
7030
  module?.isZooming === true ||
7027
7031
  module?.isDraggingNode === true ||
7028
7032
  module?.isDraggingMultipleNodes === true ||
@@ -726,7 +726,7 @@
726
726
  viewportH: viewportHeight
727
727
  },
728
728
  state: {
729
- isPanning: activeModule.isPanning === true,
729
+ isPanning: activeModule.isPanning === true || activeModule._panSmoothingActive === true,
730
730
  isZooming: activeModule.isZooming === true,
731
731
  isWindowResizing: activeModule.isWindowResizing === true,
732
732
  isResizing: activeModule.isResizing === true,
@@ -579,7 +579,7 @@
579
579
  }
580
580
 
581
581
  const base = '_content/MindExecution.Shared/js/';
582
- const scriptVersion = '20260712-css-reuse-cache-v925';
582
+ const scriptVersion = '20260712-pan-doubleclick-v928';
583
583
  const scriptUrl = (script) => `${base}${script}?v=${scriptVersion}`;
584
584
  console.log(`[Script Loader] Shared JS version: ${scriptVersion}`);
585
585
  const criticalScripts = [
@@ -1,5 +1,5 @@
1
1
  self.assetsManifest = {
2
- "version": "f/kAtLma",
2
+ "version": "awQM6oT9",
3
3
  "assets": [
4
4
  {
5
5
  "hash": "sha256-+CSYMcqLNTsq3VnH11jgYyOCCdxvHzL74CBmo4sCmMU=",
@@ -78,7 +78,7 @@
78
78
  "url": "_content/MindExecution.Shared/js/marked.min.js"
79
79
  },
80
80
  {
81
- "hash": "sha256-VtCWP9VR+lpe3PbV1E2voY0wiwg+YYP3M77qU8JJwTM=",
81
+ "hash": "sha256-DBjG7fXZAAblaqAtuyJcqQeX7OBMP6n+9KWSmTXr8tA=",
82
82
  "url": "_content/MindExecution.Shared/js/mind-map-core.js"
83
83
  },
84
84
  {
@@ -106,7 +106,7 @@
106
106
  "url": "_content/MindExecution.Shared/js/mind-map-glow-shader.js"
107
107
  },
108
108
  {
109
- "hash": "sha256-BRHeAFheJ2u1jOUCpURNhA1j0q4VEW8QHSMPleasr28=",
109
+ "hash": "sha256-nl8vYaz/QSNryxyzuaeEyQewlPRL25ERqb/vuZSxWrY=",
110
110
  "url": "_content/MindExecution.Shared/js/mind-map-interactions.js"
111
111
  },
112
112
  {
@@ -114,11 +114,11 @@
114
114
  "url": "_content/MindExecution.Shared/js/mind-map-lod-plan-worker.js"
115
115
  },
116
116
  {
117
- "hash": "sha256-HL4X4OehLXj2TGrBMLPjEr5kSK3MjOJNF8+RHLA/yRg=",
117
+ "hash": "sha256-lGkrhT2O7MSfdBU0XZGkVtvvOhcr27zlOhTkhUZ3oPo=",
118
118
  "url": "_content/MindExecution.Shared/js/mind-map-lod-renderer.js"
119
119
  },
120
120
  {
121
- "hash": "sha256-cOAX+ZZ70JU+eU1RVGVgVFBCJq40NZ/qZ0zbQl9wTzM=",
121
+ "hash": "sha256-KfVS6Milo4kpU7VFahI25MaUZ3sxGBj+0OdaKruxTWY=",
122
122
  "url": "_content/MindExecution.Shared/js/mind-map-logic-workers.js"
123
123
  },
124
124
  {
@@ -826,7 +826,7 @@
826
826
  "url": "icon-512.png"
827
827
  },
828
828
  {
829
- "hash": "sha256-wFgzyRZGm0LZCRS/TKk/6HOGBvhwd3g+/PXHKVCWhWY=",
829
+ "hash": "sha256-VrQGjuztaCMaHrSZpe+32iwhc6BNIeClZw5/8sHYp28=",
830
830
  "url": "index.html"
831
831
  },
832
832
  {
@@ -1,4 +1,4 @@
1
- /* Manifest version: f/kAtLma */
1
+ /* Manifest version: awQM6oT9 */
2
2
  // Hosted deployments should prefer the network over stale offline caches.
3
3
  // This service worker immediately clears old Blazor offline caches and unregisters itself.
4
4