* Enable/disable pseudo-3D rotation mode. * Assigns Z coordinates based on community membership (golden angle distribution) * and applies perspective projection + auto-rotation on the Pixi ticker.
(
enabled: boolean,
communityAssignments?: Record<string, number>,
)
| 2737 | * and applies perspective projection + auto-rotation on the Pixi ticker. |
| 2738 | */ |
| 2739 | set3DMode( |
| 2740 | enabled: boolean, |
| 2741 | communityAssignments?: Record<string, number>, |
| 2742 | ): void { |
| 2743 | // Guard against re-entry with the same mode (Fix #36). React in |
| 2744 | // dev StrictMode and the `communityData.assignments` dep on the |
| 2745 | // wiring effect both cause this to fire even when nothing about |
| 2746 | // the mode itself changed. Without the guard each rerun would |
| 2747 | // reset `communityVisibilityFrozen`, triggering a fresh community |
| 2748 | // cull — and the user would see a new label set on every Louvain |
| 2749 | // rerun mid-rotation. |
| 2750 | if (this.mode3d === enabled && enabled) { |
| 2751 | // Same mode + 3D: just refresh community depth assignments if |
| 2752 | // provided (still useful when Louvain produces new IDs), but |
| 2753 | // don't unfreeze the wayfinders. |
| 2754 | if (communityAssignments) { |
| 2755 | this.assignCommunityDepths(communityAssignments); |
| 2756 | } |
| 2757 | return; |
| 2758 | } |
| 2759 | this.mode3d = enabled; |
| 2760 | this.mode3dFrameCounter = 0; |
| 2761 | // Re-cull on the next frame — 2D ↔ 3D changes the projection so |
| 2762 | // the prior visible set may no longer be optimal. |
| 2763 | this.communityVisibilityFrozen = false; |
| 2764 | if (!enabled) { |
| 2765 | // Restore 2D positions from stored (x, y) |
| 2766 | for (const node of this.nodeArray) { |
| 2767 | if (!node.visible) continue; |
| 2768 | node.sprite.position.set(node.x, node.y); |
| 2769 | node.sprite.alpha = 0.9; |
| 2770 | } |
| 2771 | this.applyCounterScale(); |
| 2772 | this.redrawAllEdges(); |
| 2773 | // Rebuild quadtree with 2D positions so hit detection works correctly |
| 2774 | this.rebuildQuadtree(); |
| 2775 | return; |
| 2776 | } |
| 2777 | |
| 2778 | // Compute initial bounding radius from current positions. |
| 2779 | // Reset first so recomputeMode3DExtents() picks the real extent |
| 2780 | // rather than a stale (possibly larger) previous radius. |
| 2781 | this.mode3dRadius = 0; |
| 2782 | this.recomputeMode3DExtents(); |
| 2783 | |
| 2784 | // Assign Z depth per node: community-based + jitter |
| 2785 | if (communityAssignments) { |
| 2786 | this.assignCommunityDepths(communityAssignments); |
| 2787 | } else { |
| 2788 | this.nodeDepthT.clear(); |
| 2789 | } |
| 2790 | |
| 2791 | this.mode3dAngle = 0; |
| 2792 | this.mode3dAutoRotate = true; |
| 2793 | |
| 2794 | // Run one update3D pass to set projected positions, then cull labels |
| 2795 | this.update3D(); |
| 2796 | this.lastLabelCull = 0; |
no test coverage detected