@andespindola/brainlink 0.1.0-beta.111 → 0.1.0-beta.113
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/README.md +1 -0
- package/dist/application/frontend/client-js.js +177 -56
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -595,6 +595,7 @@ The graph UI shows:
|
|
|
595
595
|
- realtime refresh while `--watch` is enabled
|
|
596
596
|
- graph controls for zoom in, zoom out, fit visible nodes and reset-to-fit-all
|
|
597
597
|
- wheel zoom (including `cmd+scroll` and `ctrl+scroll`) anchored to cursor position for faster navigation in large graphs
|
|
598
|
+
- continuous target-scale interpolation for wheel/button zoom to avoid abrupt LOD jumps while keeping cursor-anchored focus
|
|
598
599
|
- zoom-out floor for large and massive graphs, plus reset macro floor tied to hub-neighbor distance and first-level cluster spacing, with a tighter initial camera so the first graph appears as a closer cell instead of a distant mesh
|
|
599
600
|
- keyboard shortcuts: `+` zoom in, `-` zoom out, `0` reset fit
|
|
600
601
|
- double-click on canvas zooms in at cursor position
|
|
@@ -13,8 +13,8 @@ const macroGalaxyZoomThreshold = 0.012
|
|
|
13
13
|
const macroGalaxyEnterHysteresis = 0.86
|
|
14
14
|
const macroGalaxyExitHysteresis = 1.24
|
|
15
15
|
const galaxyDiscoveryEnabled = false
|
|
16
|
-
const massiveAutoFitMacroScale = 0.
|
|
17
|
-
const defaultMacroScale = 0.
|
|
16
|
+
const massiveAutoFitMacroScale = 0.018
|
|
17
|
+
const defaultMacroScale = 0.018
|
|
18
18
|
const clusterCellPixelSize = 64
|
|
19
19
|
const minNodePixelRadius = 2.3
|
|
20
20
|
const viewportPaddingPx = 280
|
|
@@ -28,32 +28,38 @@ const ecosystemHubEdgeLimit = 120
|
|
|
28
28
|
const ecosystemSiblingEdgeLimit = 180
|
|
29
29
|
const ecosystemClusterScaleThreshold = 0.78
|
|
30
30
|
const massiveEcosystemClusterScaleThreshold = 4.2
|
|
31
|
+
const ecosystemClusterEnterHysteresis = 0.94
|
|
32
|
+
const ecosystemClusterExitHysteresis = 1.1
|
|
31
33
|
const ecosystemSubgraphScaleThreshold = 0.18
|
|
32
34
|
const ecosystemMicroScaleThreshold = 0.08
|
|
33
35
|
const ecosystemFocusedParentLimit = 2
|
|
34
36
|
const ecosystemDepthNear = 80
|
|
35
37
|
const ecosystemDepthFar = 2600
|
|
36
|
-
const ecosystemDepthPerspective =
|
|
37
|
-
const ecosystemDepthTiltY = 0.
|
|
38
|
-
const ecosystemDepthYaw = 0.
|
|
39
|
-
const ecosystemDepthPitch = 0.
|
|
40
|
-
const ecosystemDepthRadialGain = 0.
|
|
38
|
+
const ecosystemDepthPerspective = 560
|
|
39
|
+
const ecosystemDepthTiltY = 0.3
|
|
40
|
+
const ecosystemDepthYaw = 0.3
|
|
41
|
+
const ecosystemDepthPitch = 0.24
|
|
42
|
+
const ecosystemDepthRadialGain = 0.13
|
|
41
43
|
const ecosystemDepthOrbitalMaxOffset = 160
|
|
42
|
-
const ecosystemDepthMinScale = 0.
|
|
43
|
-
const ecosystemDepthOpacityFloor = 0.
|
|
44
|
+
const ecosystemDepthMinScale = 0.2
|
|
45
|
+
const ecosystemDepthOpacityFloor = 0.16
|
|
44
46
|
const graphDepthNear = 40
|
|
45
|
-
const graphDepthFar =
|
|
46
|
-
const graphDepthPerspective =
|
|
47
|
-
const graphDepthYaw = 0.
|
|
48
|
-
const graphDepthPitch = 0.
|
|
49
|
-
const graphDepthRadialGain = 0.
|
|
50
|
-
const graphDepthMinScale = 0.
|
|
51
|
-
const graphDepthOpacityFloor = 0.
|
|
52
|
-
const graphDepthEdgeOpacityFloor = 0.
|
|
53
|
-
const graphDepthProjectionNodeThreshold =
|
|
54
|
-
const graphDepthProjectionNodeCap =
|
|
55
|
-
const graphDepthProjectionMinScale = 0.
|
|
56
|
-
const graphDepthProjectionMaxScale =
|
|
47
|
+
const graphDepthFar = 1320
|
|
48
|
+
const graphDepthPerspective = 430
|
|
49
|
+
const graphDepthYaw = 0.42
|
|
50
|
+
const graphDepthPitch = 0.3
|
|
51
|
+
const graphDepthRadialGain = 0.24
|
|
52
|
+
const graphDepthMinScale = 0.34
|
|
53
|
+
const graphDepthOpacityFloor = 0.22
|
|
54
|
+
const graphDepthEdgeOpacityFloor = 0.12
|
|
55
|
+
const graphDepthProjectionNodeThreshold = 40
|
|
56
|
+
const graphDepthProjectionNodeCap = 2600
|
|
57
|
+
const graphDepthProjectionMinScale = 0.03
|
|
58
|
+
const graphDepthProjectionMaxScale = 1.7
|
|
59
|
+
const graphDepthProjectionEnterMinScale = graphDepthProjectionMinScale * 1.08
|
|
60
|
+
const graphDepthProjectionExitMinScale = graphDepthProjectionMinScale * 0.88
|
|
61
|
+
const graphDepthProjectionEnterMaxScale = graphDepthProjectionMaxScale * 0.92
|
|
62
|
+
const graphDepthProjectionExitMaxScale = graphDepthProjectionMaxScale * 1.16
|
|
57
63
|
const zoomRecoveryGuardMs = 4200
|
|
58
64
|
const zoomCapTargetViewportShare = 0.72
|
|
59
65
|
const meshEdgeScaleThreshold = 0.09
|
|
@@ -65,6 +71,12 @@ const dragSettleRounds = 3
|
|
|
65
71
|
const wheelZoomExponent = 0.0009
|
|
66
72
|
const wheelZoomExponentCap = 0.035
|
|
67
73
|
const wheelZoomModifierBoost = 1.08
|
|
74
|
+
const wheelZoomInputFloorCap = 0.976
|
|
75
|
+
const wheelZoomInputCeilCap = 1.024
|
|
76
|
+
const zoomAnimationSlowLerp = 0.18
|
|
77
|
+
const zoomAnimationFastLerp = 0.36
|
|
78
|
+
const zoomAnimationScaleSnap = 0.00008
|
|
79
|
+
const zoomAnimationPositionSnap = 0.14
|
|
68
80
|
const physicsDragFrameIntervalMs = 16
|
|
69
81
|
const physicsIdleFrameIntervalMs = 78
|
|
70
82
|
const physicsLargeGraphIdleFrameIntervalMs = 108
|
|
@@ -123,7 +135,18 @@ const state = {
|
|
|
123
135
|
lastHoverHitAt: 0,
|
|
124
136
|
lastManualZoomAt: 0,
|
|
125
137
|
lastZoomFocus: { x: 0, y: 0, at: 0 },
|
|
126
|
-
macroViewActive: false
|
|
138
|
+
macroViewActive: false,
|
|
139
|
+
ecosystemViewActive: false,
|
|
140
|
+
depthProjectionActive: false,
|
|
141
|
+
zoomTransition: {
|
|
142
|
+
active: false,
|
|
143
|
+
source: 'generic',
|
|
144
|
+
screenX: 0,
|
|
145
|
+
screenY: 0,
|
|
146
|
+
worldX: 0,
|
|
147
|
+
worldY: 0,
|
|
148
|
+
targetScale: 1
|
|
149
|
+
}
|
|
127
150
|
}
|
|
128
151
|
|
|
129
152
|
const byId = id => document.getElementById(id)
|
|
@@ -1593,10 +1616,20 @@ const cursorWorldPoint = () => {
|
|
|
1593
1616
|
|
|
1594
1617
|
const visibilityScaleBucket = (scale) => {
|
|
1595
1618
|
const safeScale = Math.max(zoomRange.min, scale)
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1619
|
+
return Math.round(safeScale * 180_000)
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
const shouldRenderEcosystemClusterView = (nodeCount, scale) => {
|
|
1623
|
+
const baseThreshold = nodeCount > massiveGraphNodeThreshold
|
|
1624
|
+
? massiveEcosystemClusterScaleThreshold
|
|
1625
|
+
: ecosystemClusterScaleThreshold
|
|
1626
|
+
const enterThreshold = baseThreshold * ecosystemClusterEnterHysteresis
|
|
1627
|
+
const exitThreshold = baseThreshold * ecosystemClusterExitHysteresis
|
|
1628
|
+
const shouldRender = state.ecosystemViewActive
|
|
1629
|
+
? scale <= exitThreshold
|
|
1630
|
+
: scale <= enterThreshold
|
|
1631
|
+
state.ecosystemViewActive = shouldRender
|
|
1632
|
+
return shouldRender
|
|
1600
1633
|
}
|
|
1601
1634
|
|
|
1602
1635
|
const shouldRenderMacroGalaxyView = () => {
|
|
@@ -2462,8 +2495,8 @@ const currentZoomMax = () => {
|
|
|
2462
2495
|
}
|
|
2463
2496
|
|
|
2464
2497
|
const zoomFloorByNodeCount = (nodeCount) => {
|
|
2465
|
-
if (nodeCount > massiveGraphNodeThreshold) return 0.
|
|
2466
|
-
if (nodeCount > largeGraphNodeThreshold) return 0.
|
|
2498
|
+
if (nodeCount > massiveGraphNodeThreshold) return 0.018
|
|
2499
|
+
if (nodeCount > largeGraphNodeThreshold) return 0.0032
|
|
2467
2500
|
if (nodeCount > ecosystemActivationNodeThreshold) return 0.001
|
|
2468
2501
|
return zoomRange.min
|
|
2469
2502
|
}
|
|
@@ -2483,6 +2516,52 @@ const clampTransformCoordinate = value => {
|
|
|
2483
2516
|
return value
|
|
2484
2517
|
}
|
|
2485
2518
|
|
|
2519
|
+
const clearZoomTransition = () => {
|
|
2520
|
+
state.zoomTransition.active = false
|
|
2521
|
+
state.zoomTransition.targetScale = state.transform.scale
|
|
2522
|
+
}
|
|
2523
|
+
|
|
2524
|
+
const zoomTransitionLerp = (delta, source) => {
|
|
2525
|
+
const normalizedDelta = Math.max(0, Math.min(1, delta / 32))
|
|
2526
|
+
const base = zoomAnimationSlowLerp + (zoomAnimationFastLerp - zoomAnimationSlowLerp) * normalizedDelta
|
|
2527
|
+
const sourceBoost = source === 'wheel' ? 1 : 1.25
|
|
2528
|
+
return Math.max(0.08, Math.min(0.45, base * sourceBoost))
|
|
2529
|
+
}
|
|
2530
|
+
|
|
2531
|
+
const applyZoomTransition = (delta) => {
|
|
2532
|
+
if (!state.zoomTransition.active) {
|
|
2533
|
+
return
|
|
2534
|
+
}
|
|
2535
|
+
|
|
2536
|
+
const targetScale = clampScale(state.zoomTransition.targetScale)
|
|
2537
|
+
const scaleDelta = targetScale - state.transform.scale
|
|
2538
|
+
const targetX = clampTransformCoordinate(state.zoomTransition.screenX - state.zoomTransition.worldX * targetScale)
|
|
2539
|
+
const targetY = clampTransformCoordinate(state.zoomTransition.screenY - state.zoomTransition.worldY * targetScale)
|
|
2540
|
+
const scaleSettled = Math.abs(scaleDelta) <= zoomAnimationScaleSnap
|
|
2541
|
+
|
|
2542
|
+
if (scaleSettled) {
|
|
2543
|
+
state.transform.scale = targetScale
|
|
2544
|
+
state.transform.x = targetX
|
|
2545
|
+
state.transform.y = targetY
|
|
2546
|
+
clearZoomTransition()
|
|
2547
|
+
return
|
|
2548
|
+
}
|
|
2549
|
+
|
|
2550
|
+
const lerp = zoomTransitionLerp(delta, state.zoomTransition.source)
|
|
2551
|
+
const nextScale = clampScale(state.transform.scale + scaleDelta * lerp)
|
|
2552
|
+
state.transform.scale = nextScale
|
|
2553
|
+
state.transform.x = clampTransformCoordinate(state.zoomTransition.screenX - state.zoomTransition.worldX * nextScale)
|
|
2554
|
+
state.transform.y = clampTransformCoordinate(state.zoomTransition.screenY - state.zoomTransition.worldY * nextScale)
|
|
2555
|
+
const settledX = Math.abs(targetX - state.transform.x) <= zoomAnimationPositionSnap
|
|
2556
|
+
const settledY = Math.abs(targetY - state.transform.y) <= zoomAnimationPositionSnap
|
|
2557
|
+
if (Math.abs(targetScale - nextScale) <= zoomAnimationScaleSnap && settledX && settledY) {
|
|
2558
|
+
state.transform.scale = targetScale
|
|
2559
|
+
state.transform.x = targetX
|
|
2560
|
+
state.transform.y = targetY
|
|
2561
|
+
clearZoomTransition()
|
|
2562
|
+
}
|
|
2563
|
+
}
|
|
2564
|
+
|
|
2486
2565
|
const graphBounds = nodes => {
|
|
2487
2566
|
if (nodes.length === 0) return null
|
|
2488
2567
|
let minX = Number.POSITIVE_INFINITY
|
|
@@ -2515,8 +2594,8 @@ const fitScaleBiasByNodeCount = nodeCount => {
|
|
|
2515
2594
|
if (nodeCount <= 180) return 1
|
|
2516
2595
|
if (nodeCount <= 600) return 0.94
|
|
2517
2596
|
if (nodeCount <= 2000) return 0.82
|
|
2518
|
-
if (nodeCount <= 6000) return 0.
|
|
2519
|
-
return 0.
|
|
2597
|
+
if (nodeCount <= 6000) return 0.74
|
|
2598
|
+
return 0.72
|
|
2520
2599
|
}
|
|
2521
2600
|
|
|
2522
2601
|
const autoFitScaleRangeByNodeCount = nodeCount => {
|
|
@@ -2526,8 +2605,8 @@ const autoFitScaleRangeByNodeCount = nodeCount => {
|
|
|
2526
2605
|
if (nodeCount <= 180) return { min: 0.18, max: 0.92 }
|
|
2527
2606
|
if (nodeCount <= 600) return { min: 0.12, max: 0.72 }
|
|
2528
2607
|
if (nodeCount <= 2000) return { min: 0.08, max: 0.52 }
|
|
2529
|
-
if (nodeCount <= 6000) return { min: 0.
|
|
2530
|
-
return { min: 0.
|
|
2608
|
+
if (nodeCount <= 6000) return { min: 0.08, max: 0.38 }
|
|
2609
|
+
return { min: 0.0085, max: 0.36 }
|
|
2531
2610
|
}
|
|
2532
2611
|
|
|
2533
2612
|
const macroFaceToFaceScale = (nodeCount, hubDistance) => {
|
|
@@ -2537,7 +2616,7 @@ const macroFaceToFaceScale = (nodeCount, hubDistance) => {
|
|
|
2537
2616
|
|
|
2538
2617
|
const rect = canvas.getBoundingClientRect()
|
|
2539
2618
|
const viewportReference = Math.max(320, Math.min(rect.width, rect.height))
|
|
2540
|
-
const share = nodeCount > massiveGraphNodeThreshold ? 0.
|
|
2619
|
+
const share = nodeCount > massiveGraphNodeThreshold ? 0.2 : 0.17
|
|
2541
2620
|
const targetPx = Math.max(24, viewportReference * share)
|
|
2542
2621
|
return targetPx / hubDistance
|
|
2543
2622
|
}
|
|
@@ -2576,7 +2655,7 @@ const macroEcosystemFaceScale = (nodeCount) => {
|
|
|
2576
2655
|
|
|
2577
2656
|
const rect = canvas.getBoundingClientRect()
|
|
2578
2657
|
const viewportReference = Math.max(320, Math.min(rect.width, rect.height))
|
|
2579
|
-
const targetShare = nodeCount > massiveGraphNodeThreshold ? 0.
|
|
2658
|
+
const targetShare = nodeCount > massiveGraphNodeThreshold ? 0.28 : 0.24
|
|
2580
2659
|
const targetPx = Math.max(30, viewportReference * targetShare)
|
|
2581
2660
|
return targetPx / nearestDistance
|
|
2582
2661
|
}
|
|
@@ -2589,6 +2668,7 @@ const fitView = (options = { useFiltered: true, macro: false, preferHubCenter: t
|
|
|
2589
2668
|
|
|
2590
2669
|
if (!bounds) {
|
|
2591
2670
|
state.transform = { x: width / 2, y: height / 2, scale: 1 }
|
|
2671
|
+
clearZoomTransition()
|
|
2592
2672
|
state.offscreenFrameCount = 0
|
|
2593
2673
|
state.recoveringViewport = false
|
|
2594
2674
|
markRenderDirty()
|
|
@@ -2638,6 +2718,7 @@ const fitView = (options = { useFiltered: true, macro: false, preferHubCenter: t
|
|
|
2638
2718
|
y: clampTransformCoordinate(height / 2 - centerY * resolvedScale),
|
|
2639
2719
|
scale: clampScale(resolvedScale)
|
|
2640
2720
|
}
|
|
2721
|
+
clearZoomTransition()
|
|
2641
2722
|
state.offscreenFrameCount = 0
|
|
2642
2723
|
state.recoveringViewport = false
|
|
2643
2724
|
markRenderDirty()
|
|
@@ -2662,6 +2743,7 @@ const focusPrimaryHub = () => {
|
|
|
2662
2743
|
y: clampTransformCoordinate(height / 2 - hub.y * targetScale),
|
|
2663
2744
|
scale: targetScale
|
|
2664
2745
|
}
|
|
2746
|
+
clearZoomTransition()
|
|
2665
2747
|
state.offscreenFrameCount = 0
|
|
2666
2748
|
markRenderDirty()
|
|
2667
2749
|
}
|
|
@@ -3068,13 +3150,25 @@ const clusterOpacity = cluster =>
|
|
|
3068
3150
|
const clusterDepth = cluster => Number.isFinite(cluster.depth) ? cluster.depth : ecosystemDepthNear
|
|
3069
3151
|
const clusterDepthScale = cluster => Number.isFinite(cluster.depthScale) ? cluster.depthScale : 1
|
|
3070
3152
|
|
|
3071
|
-
const shouldProjectRenderNodesInDepth = () =>
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3153
|
+
const shouldProjectRenderNodesInDepth = () => {
|
|
3154
|
+
const withinNodeCountWindow =
|
|
3155
|
+
state.renderClusters.length === 0 &&
|
|
3156
|
+
state.renderNodes.length >= graphDepthProjectionNodeThreshold &&
|
|
3157
|
+
state.renderNodes.length <= graphDepthProjectionNodeCap &&
|
|
3158
|
+
!state.pointer.down
|
|
3159
|
+
|
|
3160
|
+
if (!withinNodeCountWindow) {
|
|
3161
|
+
state.depthProjectionActive = false
|
|
3162
|
+
return false
|
|
3163
|
+
}
|
|
3164
|
+
|
|
3165
|
+
const scale = state.transform.scale
|
|
3166
|
+
const shouldProject = state.depthProjectionActive
|
|
3167
|
+
? scale >= graphDepthProjectionExitMinScale && scale <= graphDepthProjectionExitMaxScale
|
|
3168
|
+
: scale >= graphDepthProjectionEnterMinScale && scale <= graphDepthProjectionEnterMaxScale
|
|
3169
|
+
state.depthProjectionActive = shouldProject
|
|
3170
|
+
return shouldProject
|
|
3171
|
+
}
|
|
3078
3172
|
|
|
3079
3173
|
const nodeProjectionAnchor = () => {
|
|
3080
3174
|
const hub = state.primaryHub
|
|
@@ -3312,12 +3406,9 @@ const computeRenderVisibility = () => {
|
|
|
3312
3406
|
return
|
|
3313
3407
|
}
|
|
3314
3408
|
|
|
3315
|
-
const ecosystemScaleThreshold = state.visibleNodes.length > massiveGraphNodeThreshold
|
|
3316
|
-
? massiveEcosystemClusterScaleThreshold
|
|
3317
|
-
: ecosystemClusterScaleThreshold
|
|
3318
3409
|
if (
|
|
3319
3410
|
state.ecosystemExpansionLevels.length > 0 &&
|
|
3320
|
-
state.transform.scale
|
|
3411
|
+
shouldRenderEcosystemClusterView(state.visibleNodes.length, state.transform.scale) &&
|
|
3321
3412
|
state.ecosystemClusters.length > 0
|
|
3322
3413
|
) {
|
|
3323
3414
|
const clusters = selectHierarchicalEcosystemClusters(viewport)
|
|
@@ -3331,6 +3422,7 @@ const computeRenderVisibility = () => {
|
|
|
3331
3422
|
state.renderEdges = []
|
|
3332
3423
|
return
|
|
3333
3424
|
}
|
|
3425
|
+
state.ecosystemViewActive = false
|
|
3334
3426
|
|
|
3335
3427
|
if (state.visibleNodes.length <= 2000) {
|
|
3336
3428
|
state.renderNodes = state.visibleNodes
|
|
@@ -3509,7 +3601,8 @@ const render = now => {
|
|
|
3509
3601
|
const isInteracting =
|
|
3510
3602
|
state.pointer.down ||
|
|
3511
3603
|
state.renderVisibilityDirty ||
|
|
3512
|
-
state.recoveringViewport
|
|
3604
|
+
state.recoveringViewport ||
|
|
3605
|
+
state.zoomTransition.active
|
|
3513
3606
|
const minFrameIntervalMs = isInteracting ? 16 : backgroundFrameIntervalMs
|
|
3514
3607
|
if (delta < minFrameIntervalMs) {
|
|
3515
3608
|
requestAnimationFrame(render)
|
|
@@ -3523,6 +3616,7 @@ const render = now => {
|
|
|
3523
3616
|
if (!hasValidTransform()) {
|
|
3524
3617
|
resetView()
|
|
3525
3618
|
}
|
|
3619
|
+
applyZoomTransition(delta)
|
|
3526
3620
|
ctx.clearRect(0, 0, width, height)
|
|
3527
3621
|
webGlRenderer?.clear(width, height)
|
|
3528
3622
|
if (state.nodes.length === 0) {
|
|
@@ -3752,12 +3846,23 @@ const selectNodeById = id => {
|
|
|
3752
3846
|
|
|
3753
3847
|
const zoomAtPoint = (screenX, screenY, factor, source = 'generic') => {
|
|
3754
3848
|
state.lastManualZoomAt = performance.now()
|
|
3755
|
-
const
|
|
3756
|
-
|
|
3757
|
-
|
|
3849
|
+
const baseScale = state.zoomTransition.active
|
|
3850
|
+
? state.zoomTransition.targetScale
|
|
3851
|
+
: state.transform.scale
|
|
3852
|
+
const boundedFactor = source === 'wheel'
|
|
3853
|
+
? Math.max(wheelZoomInputFloorCap, Math.min(wheelZoomInputCeilCap, factor))
|
|
3854
|
+
: factor
|
|
3855
|
+
const nextScale = clampScale(baseScale * boundedFactor)
|
|
3856
|
+
if (nextScale === baseScale && !state.zoomTransition.active) {
|
|
3758
3857
|
return
|
|
3759
3858
|
}
|
|
3760
|
-
const worldPointAtCursor =
|
|
3859
|
+
const worldPointAtCursor =
|
|
3860
|
+
state.zoomTransition.active &&
|
|
3861
|
+
state.zoomTransition.source === source &&
|
|
3862
|
+
state.zoomTransition.screenX === screenX &&
|
|
3863
|
+
state.zoomTransition.screenY === screenY
|
|
3864
|
+
? { x: state.zoomTransition.worldX, y: state.zoomTransition.worldY }
|
|
3865
|
+
: screenToWorldPoint(screenX, screenY)
|
|
3761
3866
|
const worldX = worldPointAtCursor.x
|
|
3762
3867
|
const worldY = worldPointAtCursor.y
|
|
3763
3868
|
state.lastZoomFocus = {
|
|
@@ -3765,9 +3870,15 @@ const zoomAtPoint = (screenX, screenY, factor, source = 'generic') => {
|
|
|
3765
3870
|
y: worldY,
|
|
3766
3871
|
at: performance.now()
|
|
3767
3872
|
}
|
|
3768
|
-
state.
|
|
3769
|
-
|
|
3770
|
-
|
|
3873
|
+
state.zoomTransition = {
|
|
3874
|
+
active: true,
|
|
3875
|
+
source,
|
|
3876
|
+
screenX,
|
|
3877
|
+
screenY,
|
|
3878
|
+
worldX,
|
|
3879
|
+
worldY,
|
|
3880
|
+
targetScale: nextScale
|
|
3881
|
+
}
|
|
3771
3882
|
state.offscreenFrameCount = 0
|
|
3772
3883
|
markRenderDirty()
|
|
3773
3884
|
}
|
|
@@ -3786,13 +3897,22 @@ const wheelZoomFactor = event => {
|
|
|
3786
3897
|
state.transform.scale <= massiveEcosystemClusterScaleThreshold
|
|
3787
3898
|
const sensitivityMultiplier = isMassiveEcosystemZoom ? 0.48 : 1
|
|
3788
3899
|
const capMultiplier = isMassiveEcosystemZoom ? 0.34 : 1
|
|
3789
|
-
const
|
|
3790
|
-
const
|
|
3900
|
+
const isZoomOut = normalizedDelta > 0
|
|
3901
|
+
const currentScale = state.transform.scale
|
|
3902
|
+
const zoomOutDamping = isZoomOut
|
|
3903
|
+
? (currentScale <= 0.03 ? 0.38 : currentScale <= 0.08 ? 0.52 : 0.68)
|
|
3904
|
+
: 1
|
|
3905
|
+
const sensitivity = wheelZoomExponent * (isModifierZoom ? wheelZoomModifierBoost : 1) * sensitivityMultiplier * zoomOutDamping
|
|
3906
|
+
const exponentCap = wheelZoomExponentCap * capMultiplier * (isZoomOut ? 0.74 : 1)
|
|
3791
3907
|
const exponent = Math.max(
|
|
3792
3908
|
-exponentCap,
|
|
3793
3909
|
Math.min(exponentCap, -normalizedDelta * sensitivity)
|
|
3794
3910
|
)
|
|
3795
|
-
|
|
3911
|
+
const factor = Math.exp(exponent)
|
|
3912
|
+
if (factor > 1) {
|
|
3913
|
+
return Math.min(factor, wheelZoomInputCeilCap)
|
|
3914
|
+
}
|
|
3915
|
+
return Math.max(factor, wheelZoomInputFloorCap)
|
|
3796
3916
|
}
|
|
3797
3917
|
|
|
3798
3918
|
const handleWheelZoom = event => {
|
|
@@ -3876,6 +3996,7 @@ const bindEvents = () => {
|
|
|
3876
3996
|
zoomAtPoint(cursorX, cursorY, 1.055)
|
|
3877
3997
|
})
|
|
3878
3998
|
canvas.addEventListener('pointerdown', event => {
|
|
3999
|
+
clearZoomTransition()
|
|
3879
4000
|
const point = worldPoint(event)
|
|
3880
4001
|
const node = hitNode(point)
|
|
3881
4002
|
state.pointer = { x: event.clientX, y: event.clientY, down: true, dragNode: node, moved: false }
|
package/package.json
CHANGED