@naniteninja/profile-comparison-lib 1.0.24 → 1.0.26
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.
|
@@ -2428,6 +2428,11 @@ class ProfileComparisonLibComponent {
|
|
|
2428
2428
|
const idB = this.getLinkNodeId(b);
|
|
2429
2429
|
return idA < idB ? `${idA}|${idB}` : `${idB}|${idA}`;
|
|
2430
2430
|
}
|
|
2431
|
+
getRuntimeLinkStrength(link) {
|
|
2432
|
+
return link.kind === 'cross'
|
|
2433
|
+
? link.strength * 1.7
|
|
2434
|
+
: link.strength * 1.12;
|
|
2435
|
+
}
|
|
2431
2436
|
selectLinksWithCaps(candidates, maxPerNode, minPerNode = 0) {
|
|
2432
2437
|
if (!candidates.length || maxPerNode <= 0)
|
|
2433
2438
|
return [];
|
|
@@ -2660,8 +2665,6 @@ class ProfileComparisonLibComponent {
|
|
|
2660
2665
|
const horizontalPadding = this.chipPaddingXPx * 2;
|
|
2661
2666
|
const getHalfWidth = (label) => (label.length * approxCharWidth + horizontalPadding) / 2;
|
|
2662
2667
|
const EDGE_PADDING = 20;
|
|
2663
|
-
const getNodeMinX = (label) => getHalfWidth(label) + EDGE_PADDING;
|
|
2664
|
-
const getNodeMaxX = (label) => containerWidth - getHalfWidth(label) - EDGE_PADDING;
|
|
2665
2668
|
const maxHalfP1 = p1Items.length ? Math.max(...p1Items.map(getHalfWidth)) : chipHalfH;
|
|
2666
2669
|
const maxHalfShared = shared.length ? Math.max(...shared.map(getHalfWidth)) : chipHalfH;
|
|
2667
2670
|
const maxHalfP2 = p2Items.length ? Math.max(...p2Items.map(getHalfWidth)) : chipHalfH;
|
|
@@ -2681,6 +2684,37 @@ class ProfileComparisonLibComponent {
|
|
|
2681
2684
|
const minCenterY = startY + chipHalfH;
|
|
2682
2685
|
const verticalSpan = Math.max(0, maxY - minCenterY);
|
|
2683
2686
|
const centerGuard = Math.max(12, Math.min(26, (zoneP2 - zoneP1) * 0.12));
|
|
2687
|
+
const maxHalfAny = Math.max(maxHalfP1, maxHalfShared, maxHalfP2, chipHalfH);
|
|
2688
|
+
const globalMinX = maxHalfAny + EDGE_PADDING;
|
|
2689
|
+
const globalMaxX = containerWidth - maxHalfAny - EDGE_PADDING;
|
|
2690
|
+
const dividerLeft = (zoneP1 + zoneShared) / 2;
|
|
2691
|
+
const dividerRight = (zoneShared + zoneP2) / 2;
|
|
2692
|
+
const laneGap = Math.max(4, Math.min(16, centerGuard * 0.45));
|
|
2693
|
+
const sharedRawMin = Math.max(globalMinX, dividerLeft + laneGap);
|
|
2694
|
+
const sharedRawMax = Math.min(globalMaxX, dividerRight - laneGap);
|
|
2695
|
+
const sharedRawWidth = Math.max(0, sharedRawMax - sharedRawMin);
|
|
2696
|
+
const sharedTighten = Math.max(12, Math.min(34, sharedRawWidth * 0.22));
|
|
2697
|
+
const sharedMinWidth = Math.max(44, Math.min(84, maxHalfShared * 1.05));
|
|
2698
|
+
let p1BoundsMinX = globalMinX;
|
|
2699
|
+
let p1BoundsMaxX = Math.min(globalMaxX, dividerLeft - laneGap);
|
|
2700
|
+
let sharedBoundsMinX = Math.max(globalMinX, sharedRawMin + sharedTighten);
|
|
2701
|
+
let sharedBoundsMaxX = Math.min(globalMaxX, sharedRawMax - sharedTighten);
|
|
2702
|
+
let p2BoundsMinX = Math.max(globalMinX, dividerRight + laneGap);
|
|
2703
|
+
let p2BoundsMaxX = globalMaxX;
|
|
2704
|
+
if (p1BoundsMaxX < p1BoundsMinX)
|
|
2705
|
+
p1BoundsMaxX = p1BoundsMinX;
|
|
2706
|
+
if (p2BoundsMinX > p2BoundsMaxX)
|
|
2707
|
+
p2BoundsMinX = p2BoundsMaxX;
|
|
2708
|
+
if (sharedBoundsMaxX - sharedBoundsMinX < sharedMinWidth) {
|
|
2709
|
+
const centerX = Math.max(globalMinX, Math.min(globalMaxX, zoneShared));
|
|
2710
|
+
const fallbackHalfWidth = sharedMinWidth / 2;
|
|
2711
|
+
sharedBoundsMinX = Math.max(globalMinX, centerX - fallbackHalfWidth);
|
|
2712
|
+
sharedBoundsMaxX = Math.min(globalMaxX, centerX + fallbackHalfWidth);
|
|
2713
|
+
if (sharedBoundsMaxX < sharedBoundsMinX) {
|
|
2714
|
+
sharedBoundsMinX = centerX;
|
|
2715
|
+
sharedBoundsMaxX = centerX;
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2684
2718
|
// --- Scan matrix for max similarity to normalize spring distances ---
|
|
2685
2719
|
let matrixMax = 0;
|
|
2686
2720
|
if (this.matrixData?.rows) {
|
|
@@ -2758,7 +2792,7 @@ class ProfileComparisonLibComponent {
|
|
|
2758
2792
|
return nodes.filter(n => n.group === 'p1' || n.group === 'p2');
|
|
2759
2793
|
};
|
|
2760
2794
|
const ySimilarityFloor = Math.max(0.18, springThreshold - 0.06);
|
|
2761
|
-
for (let pass = 0; pass <
|
|
2795
|
+
for (let pass = 0; pass < 6; pass++) {
|
|
2762
2796
|
const snapshotY = new Map();
|
|
2763
2797
|
nodes.forEach(node => {
|
|
2764
2798
|
snapshotY.set(node.id, node.targetY ?? (minCenterY + verticalSpan * 0.5));
|
|
@@ -2782,57 +2816,27 @@ class ProfileComparisonLibComponent {
|
|
|
2782
2816
|
return;
|
|
2783
2817
|
const semanticY = weightedY / totalWeight;
|
|
2784
2818
|
const currentY = snapshotY.get(node.id) ?? semanticY;
|
|
2785
|
-
const blend = node.group === 'shared' ? 0.
|
|
2819
|
+
const blend = node.group === 'shared' ? 0.84 : 0.78;
|
|
2786
2820
|
node.targetY = Math.max(minCenterY, Math.min(maxY, currentY + (semanticY - currentY) * blend));
|
|
2787
2821
|
});
|
|
2788
2822
|
}
|
|
2789
|
-
const groupCounts = {
|
|
2790
|
-
p1: p1Items.length,
|
|
2791
|
-
shared: shared.length,
|
|
2792
|
-
p2: p2Items.length,
|
|
2793
|
-
};
|
|
2794
|
-
const fullVerticalSpan = Math.max(rowGap, maxY - minCenterY);
|
|
2795
2823
|
const buildNodeBounds = (node) => {
|
|
2796
|
-
const halfW = getHalfWidth(node.label);
|
|
2797
|
-
const globalMinX = getNodeMinX(node.label);
|
|
2798
|
-
const globalMaxX = getNodeMaxX(node.label);
|
|
2799
|
-
const leftLimit = zoneShared - halfW - centerGuard;
|
|
2800
|
-
const rightLimit = zoneShared + halfW + centerGuard;
|
|
2801
2824
|
let minX = globalMinX;
|
|
2802
2825
|
let maxX = globalMaxX;
|
|
2803
2826
|
if (node.group === 'p1') {
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
maxX = minX;
|
|
2827
|
+
minX = p1BoundsMinX;
|
|
2828
|
+
maxX = p1BoundsMaxX;
|
|
2807
2829
|
}
|
|
2808
2830
|
else if (node.group === 'p2') {
|
|
2809
|
-
minX =
|
|
2810
|
-
|
|
2811
|
-
minX = maxX;
|
|
2831
|
+
minX = p2BoundsMinX;
|
|
2832
|
+
maxX = p2BoundsMaxX;
|
|
2812
2833
|
}
|
|
2813
2834
|
else {
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
minX = centerX;
|
|
2817
|
-
maxX = centerX;
|
|
2818
|
-
}
|
|
2819
|
-
const baseY = node.targetY ?? (minCenterY + fullVerticalSpan * 0.5);
|
|
2820
|
-
const groupCount = groupCounts[node.group];
|
|
2821
|
-
let ySlack = rowGap * 2.2;
|
|
2822
|
-
if (node.group === 'shared') {
|
|
2823
|
-
ySlack = Math.max(rowGap * 4.2, fullVerticalSpan * 0.58);
|
|
2824
|
-
}
|
|
2825
|
-
else {
|
|
2826
|
-
const laneSpan = groupCount > 1 ? fullVerticalSpan / (groupCount - 1) : fullVerticalSpan;
|
|
2827
|
-
ySlack = Math.min(fullVerticalSpan, Math.max(rowGap * 2.4, laneSpan * 0.95));
|
|
2828
|
-
}
|
|
2829
|
-
let minY = Math.max(minCenterY, baseY - ySlack);
|
|
2830
|
-
let maxYForNode = Math.min(maxY, baseY + ySlack);
|
|
2831
|
-
if (minY > maxYForNode) {
|
|
2832
|
-
minY = minCenterY;
|
|
2833
|
-
maxYForNode = maxY;
|
|
2835
|
+
minX = sharedBoundsMinX;
|
|
2836
|
+
maxX = sharedBoundsMaxX;
|
|
2834
2837
|
}
|
|
2835
|
-
|
|
2838
|
+
// Y should be column-wide so any chip can travel vertically within its lane.
|
|
2839
|
+
return { minX, maxX, minY: minCenterY, maxY };
|
|
2836
2840
|
};
|
|
2837
2841
|
const nodeBounds = new Map();
|
|
2838
2842
|
nodes.forEach(node => nodeBounds.set(node.id, buildNodeBounds(node)));
|
|
@@ -2844,8 +2848,8 @@ class ProfileComparisonLibComponent {
|
|
|
2844
2848
|
};
|
|
2845
2849
|
const crossCandidates = [];
|
|
2846
2850
|
const intraCandidates = [];
|
|
2847
|
-
const MIN_DIST =
|
|
2848
|
-
const SPAN =
|
|
2851
|
+
const MIN_DIST = 34;
|
|
2852
|
+
const SPAN = 72;
|
|
2849
2853
|
for (let i = 0; i < nodes.length; i++) {
|
|
2850
2854
|
for (let j = i + 1; j < nodes.length; j++) {
|
|
2851
2855
|
if (!this.canLinkNodes(nodes[i], nodes[j]))
|
|
@@ -2879,9 +2883,9 @@ class ProfileComparisonLibComponent {
|
|
|
2879
2883
|
kind: 'intra',
|
|
2880
2884
|
source: groupNodes[i],
|
|
2881
2885
|
target: groupNodes[j],
|
|
2882
|
-
strength: 0.
|
|
2886
|
+
strength: 0.12 + normSim * 0.24,
|
|
2883
2887
|
similarity,
|
|
2884
|
-
distance:
|
|
2888
|
+
distance: 34 + (1 - normSim) * 46,
|
|
2885
2889
|
});
|
|
2886
2890
|
}
|
|
2887
2891
|
}
|
|
@@ -2974,20 +2978,56 @@ class ProfileComparisonLibComponent {
|
|
|
2974
2978
|
return;
|
|
2975
2979
|
const targetY = weightedY / totalWeight;
|
|
2976
2980
|
const dy = targetY - (node.y ?? targetY);
|
|
2981
|
+
const sparseNeighbors = neighbors.length <= 2;
|
|
2977
2982
|
const alignStrength = node.group === 'shared'
|
|
2978
|
-
? (
|
|
2979
|
-
: 0.
|
|
2983
|
+
? (sparseNeighbors ? 0.95 : 0.8)
|
|
2984
|
+
: (sparseNeighbors ? 0.82 : 0.68);
|
|
2980
2985
|
node.vy = (node.vy ?? 0) + dy * alignStrength * alpha;
|
|
2981
2986
|
});
|
|
2982
2987
|
};
|
|
2988
|
+
const linkRecords = links
|
|
2989
|
+
.map(link => {
|
|
2990
|
+
const source = typeof link.source === 'object' ? link.source : nodeById.get(String(link.source));
|
|
2991
|
+
const target = typeof link.target === 'object' ? link.target : nodeById.get(String(link.target));
|
|
2992
|
+
if (!source || !target)
|
|
2993
|
+
return null;
|
|
2994
|
+
return {
|
|
2995
|
+
source,
|
|
2996
|
+
target,
|
|
2997
|
+
similarity: Number(link.similarity ?? link.strength ?? 0.5),
|
|
2998
|
+
kind: link.kind,
|
|
2999
|
+
};
|
|
3000
|
+
})
|
|
3001
|
+
.filter((record) => !!record);
|
|
3002
|
+
const maxLinkSimilarity = Math.max(0.001, ...linkRecords.map(record => Math.max(0, record.similarity)));
|
|
3003
|
+
const MIN_PAIR_DISTANCE = 34;
|
|
3004
|
+
const MAX_PAIR_DISTANCE = 88;
|
|
3005
|
+
const similarityAttractionForce = (alpha) => {
|
|
3006
|
+
linkRecords.forEach(record => {
|
|
3007
|
+
const dx = (record.target.x ?? 0) - (record.source.x ?? 0);
|
|
3008
|
+
const dy = (record.target.y ?? 0) - (record.source.y ?? 0);
|
|
3009
|
+
const dist = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
3010
|
+
const normalizedSimilarity = Math.max(0, Math.min(1, record.similarity / maxLinkSimilarity));
|
|
3011
|
+
const targetDistance = MIN_PAIR_DISTANCE + (MAX_PAIR_DISTANCE - MIN_PAIR_DISTANCE) * (1 - normalizedSimilarity);
|
|
3012
|
+
if (dist <= targetDistance)
|
|
3013
|
+
return;
|
|
3014
|
+
const pullGap = dist - targetDistance;
|
|
3015
|
+
const pullBase = record.kind === 'cross' ? 0.9 : 0.82;
|
|
3016
|
+
const pullFactor = pullBase * alpha * (pullGap / dist);
|
|
3017
|
+
record.source.vx = (record.source.vx ?? 0) + dx * pullFactor;
|
|
3018
|
+
record.source.vy = (record.source.vy ?? 0) + dy * pullFactor;
|
|
3019
|
+
record.target.vx = (record.target.vx ?? 0) - dx * pullFactor;
|
|
3020
|
+
record.target.vy = (record.target.vy ?? 0) - dy * pullFactor;
|
|
3021
|
+
});
|
|
3022
|
+
};
|
|
2983
3023
|
this.ngZone.runOutsideAngular(() => {
|
|
2984
3024
|
const sim = forceSimulation(nodes)
|
|
2985
|
-
.alphaDecay(0.
|
|
3025
|
+
.alphaDecay(0.014)
|
|
2986
3026
|
.stop()
|
|
2987
3027
|
.force('link', forceLink(links)
|
|
2988
3028
|
.id(d => d.id)
|
|
2989
3029
|
.distance(d => d.distance)
|
|
2990
|
-
.strength(d =>
|
|
3030
|
+
.strength(d => this.getRuntimeLinkStrength(d)))
|
|
2991
3031
|
.force('collide', forceCollide((d) => {
|
|
2992
3032
|
const baseRadius = getHalfWidth(d.label) * 0.55 + ProfileComparisonLibComponent.CHIP_COLLISION_BORDER_PADDING;
|
|
2993
3033
|
return Math.min(58, Math.max(ProfileComparisonLibComponent.CHIP_RADIUS + 8, baseRadius));
|
|
@@ -2996,29 +3036,15 @@ class ProfileComparisonLibComponent {
|
|
|
2996
3036
|
.iterations(2))
|
|
2997
3037
|
.force('xBound', forceX(d => d.targetX ?? zoneShared)
|
|
2998
3038
|
.strength(d => d.group === 'shared' ? 1.15 : 0.85))
|
|
2999
|
-
.force('yTargetSeed', forceY(d => d.targetY ?? minCenterY).strength(0.
|
|
3000
|
-
.force('crossAlign', crossAlignForce)
|
|
3039
|
+
.force('yTargetSeed', forceY(d => d.targetY ?? minCenterY).strength(0.1))
|
|
3040
|
+
.force('crossAlign', crossAlignForce)
|
|
3041
|
+
.force('similarityAttraction', similarityAttractionForce);
|
|
3001
3042
|
for (let i = 0; i < 420 && sim.alpha() > 0.0035; i++) {
|
|
3002
3043
|
sim.tick();
|
|
3003
3044
|
nodes.forEach(n => {
|
|
3004
3045
|
clampNodeToBounds(n);
|
|
3005
3046
|
});
|
|
3006
3047
|
}
|
|
3007
|
-
const enforceGroupStack = (group) => {
|
|
3008
|
-
const groupNodes = nodes
|
|
3009
|
-
.filter(n => n.group === group)
|
|
3010
|
-
.sort((a, b) => (a.y ?? minCenterY) - (b.y ?? minCenterY));
|
|
3011
|
-
for (let i = 1; i < groupNodes.length; i++) {
|
|
3012
|
-
const prevY = groupNodes[i - 1].y ?? minCenterY;
|
|
3013
|
-
const minAllowed = prevY + rowGap * 0.92;
|
|
3014
|
-
if ((groupNodes[i].y ?? minCenterY) < minAllowed) {
|
|
3015
|
-
groupNodes[i].y = minAllowed;
|
|
3016
|
-
}
|
|
3017
|
-
}
|
|
3018
|
-
};
|
|
3019
|
-
enforceGroupStack('p1');
|
|
3020
|
-
enforceGroupStack('shared');
|
|
3021
|
-
enforceGroupStack('p2');
|
|
3022
3048
|
nodes.forEach(n => {
|
|
3023
3049
|
clampNodeToBounds(n);
|
|
3024
3050
|
n.originalFx = n.x;
|
|
@@ -3028,8 +3054,8 @@ class ProfileComparisonLibComponent {
|
|
|
3028
3054
|
sim.force('yTargetSeed', null);
|
|
3029
3055
|
sim.force('crossAlign', null);
|
|
3030
3056
|
sim.force('xTarget', forceX(d => d.originalFx ?? d.x ?? 0)
|
|
3031
|
-
.strength(d => d.group === 'shared' ? 0.
|
|
3032
|
-
sim.force('yTarget', forceY(d => d.originalFy ?? d.y ?? 0).strength(0.
|
|
3057
|
+
.strength(d => d.group === 'shared' ? 0.12 : 0.045));
|
|
3058
|
+
sim.force('yTarget', forceY(d => d.originalFy ?? d.y ?? 0).strength(0.045));
|
|
3033
3059
|
this.simulation = sim;
|
|
3034
3060
|
this.ngZone.run(() => {
|
|
3035
3061
|
this.forceNodes = [...nodes];
|
|
@@ -3459,85 +3485,19 @@ class ProfileComparisonLibComponent {
|
|
|
3459
3485
|
}
|
|
3460
3486
|
}
|
|
3461
3487
|
/**
|
|
3462
|
-
*
|
|
3463
|
-
*
|
|
3464
|
-
* highest-sim → MIN_ORBIT (touches anchor), lowest-sim → MAX_ORBIT.
|
|
3465
|
-
*
|
|
3466
|
-
* While active, the xTarget/yTarget restoring forces are muted so they don't
|
|
3467
|
-
* fight the attraction. They are restored the moment attraction is cleared.
|
|
3488
|
+
* Anchor interaction should only affect highlighting/energy.
|
|
3489
|
+
* Similarity attraction is always-on via the base simulation forces.
|
|
3468
3490
|
*/
|
|
3469
3491
|
updateAttractionForce(anchorNode) {
|
|
3470
3492
|
if (!this.simulation)
|
|
3471
3493
|
return;
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
xT.strength(0.1);
|
|
3478
|
-
const yT = this.simulation.force('yTarget');
|
|
3479
|
-
if (yT)
|
|
3480
|
-
yT.strength(0.1);
|
|
3481
|
-
const col = this.simulation.force('collide');
|
|
3482
|
-
if (col)
|
|
3483
|
-
col.strength(1.0);
|
|
3494
|
+
this.simulation.force('attraction', null);
|
|
3495
|
+
if (anchorNode) {
|
|
3496
|
+
this.simulation.alpha(0.24).restart();
|
|
3497
|
+
}
|
|
3498
|
+
else if (!this.isDragging) {
|
|
3484
3499
|
this.simulation.alphaTarget(0);
|
|
3485
|
-
this.simulation.alpha(0.6).restart();
|
|
3486
|
-
return;
|
|
3487
3500
|
}
|
|
3488
|
-
// Collect connected node IDs and their similarity values
|
|
3489
|
-
const linkSim = new Map();
|
|
3490
|
-
this.forceLinks.forEach(link => {
|
|
3491
|
-
const srcId = typeof link.source === 'object' ? link.source.id : link.source;
|
|
3492
|
-
const tgtId = typeof link.target === 'object' ? link.target.id : link.target;
|
|
3493
|
-
const sim = Number(link.similarity ?? link.strength ?? 0.5);
|
|
3494
|
-
if (srcId === anchorNode.id)
|
|
3495
|
-
linkSim.set(String(tgtId), sim);
|
|
3496
|
-
if (tgtId === anchorNode.id)
|
|
3497
|
-
linkSim.set(String(srcId), sim);
|
|
3498
|
-
});
|
|
3499
|
-
const simValues = Array.from(linkSim.values());
|
|
3500
|
-
const maxSim = simValues.length ? Math.max(...simValues) : 1;
|
|
3501
|
-
// Closest chip (highest sim) touches the anchor (36px ≈ one chip height).
|
|
3502
|
-
// Others are spread tightly between 36–80px.
|
|
3503
|
-
const MIN_ORBIT = 36; // px — highest-sim chip: touching
|
|
3504
|
-
const MAX_ORBIT = 80; // px — lowest-sim chip: still very close
|
|
3505
|
-
const PULL = 0.5; // strong pull — overcomes any residual forces
|
|
3506
|
-
// Mute the forces that would fight the pull while attraction is active
|
|
3507
|
-
const xT = this.simulation.force('xTarget');
|
|
3508
|
-
if (xT)
|
|
3509
|
-
xT.strength(0);
|
|
3510
|
-
const yT = this.simulation.force('yTarget');
|
|
3511
|
-
if (yT)
|
|
3512
|
-
yT.strength(0);
|
|
3513
|
-
const col = this.simulation.force('collide');
|
|
3514
|
-
if (col)
|
|
3515
|
-
col.strength(0.35); // keep slight chip separation while still allowing orbiting
|
|
3516
|
-
const attractionForce = (alpha) => {
|
|
3517
|
-
const ax = anchorNode.x ?? 0;
|
|
3518
|
-
const ay = anchorNode.y ?? 0;
|
|
3519
|
-
this.forceNodes.forEach(n => {
|
|
3520
|
-
if (n.id === anchorNode.id)
|
|
3521
|
-
return;
|
|
3522
|
-
const sim = linkSim.get(n.id);
|
|
3523
|
-
if (sim === undefined)
|
|
3524
|
-
return; // not connected — leave untouched
|
|
3525
|
-
const normalizedSim = maxSim > 0 ? sim / maxSim : 0;
|
|
3526
|
-
const orbitDist = MIN_ORBIT + (MAX_ORBIT - MIN_ORBIT) * (1 - normalizedSim);
|
|
3527
|
-
const dx = ax - (n.x ?? 0);
|
|
3528
|
-
const dy = ay - (n.y ?? 0);
|
|
3529
|
-
const dist = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
3530
|
-
if (dist > orbitDist) {
|
|
3531
|
-
// Pull toward orbit boundary — proportional to gap remaining
|
|
3532
|
-
const factor = PULL * alpha * (dist - orbitDist) / dist;
|
|
3533
|
-
n.vx = (n.vx ?? 0) + dx * factor;
|
|
3534
|
-
n.vy = (n.vy ?? 0) + dy * factor;
|
|
3535
|
-
}
|
|
3536
|
-
});
|
|
3537
|
-
};
|
|
3538
|
-
this.simulation.force('attraction', attractionForce);
|
|
3539
|
-
// alphaTarget 0.3 keeps alpha high so attraction stays strong indefinitely
|
|
3540
|
-
this.simulation.alphaTarget(0.3).restart();
|
|
3541
3501
|
}
|
|
3542
3502
|
isNodeConnected(node) {
|
|
3543
3503
|
if (!this.activeNode)
|
|
@@ -3629,21 +3589,21 @@ class ProfileComparisonLibComponent {
|
|
|
3629
3589
|
if (this.simulation) {
|
|
3630
3590
|
const linkForce = this.simulation.force('link');
|
|
3631
3591
|
if (linkForce)
|
|
3632
|
-
linkForce.strength((d) => d
|
|
3592
|
+
linkForce.strength((d) => this.getRuntimeLinkStrength(d));
|
|
3633
3593
|
const collideForce = this.simulation.force('collide');
|
|
3634
3594
|
if (collideForce)
|
|
3635
3595
|
collideForce.strength(1.0);
|
|
3636
3596
|
const xTarget = this.simulation.force('xTarget');
|
|
3637
3597
|
if (xTarget)
|
|
3638
|
-
xTarget.strength(0.
|
|
3598
|
+
xTarget.strength((d) => d.group === 'shared' ? 0.12 : 0.05);
|
|
3639
3599
|
const yTarget = this.simulation.force('yTarget');
|
|
3640
3600
|
if (yTarget)
|
|
3641
|
-
yTarget.strength(0.
|
|
3601
|
+
yTarget.strength(0.05);
|
|
3642
3602
|
}
|
|
3643
3603
|
// Pull connected chips toward the dragged node
|
|
3644
3604
|
this.updateAttractionForce(node);
|
|
3645
3605
|
// Wake up the simulation target to keep it highly active during drag
|
|
3646
|
-
this.simulation.alphaTarget(0.
|
|
3606
|
+
this.simulation.alphaTarget(0.28).restart();
|
|
3647
3607
|
}
|
|
3648
3608
|
onDocumentDragMove(event) {
|
|
3649
3609
|
if (!this.isDragging || !this.draggedNode)
|
|
@@ -3675,12 +3635,8 @@ class ProfileComparisonLibComponent {
|
|
|
3675
3635
|
const maxY = bounds?.maxY ?? fallbackMaxY;
|
|
3676
3636
|
const clampedX = Math.max(minX, Math.min(maxX, relX));
|
|
3677
3637
|
const clampedY = Math.max(minY, Math.min(maxY, relY));
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
const xElasticity = this.draggedNode.group === 'shared' ? 0.62 : 0.72;
|
|
3681
|
-
const yElasticity = this.draggedNode.group === 'shared' ? 0.58 : 0.68;
|
|
3682
|
-
this.draggedNode.fx = anchorX + (clampedX - anchorX) * xElasticity;
|
|
3683
|
-
this.draggedNode.fy = anchorY + (clampedY - anchorY) * yElasticity;
|
|
3638
|
+
this.draggedNode.fx = clampedX;
|
|
3639
|
+
this.draggedNode.fy = clampedY;
|
|
3684
3640
|
// Keep active force simulation running
|
|
3685
3641
|
this.simulation.alpha(0.3).restart();
|
|
3686
3642
|
}
|
|
@@ -3690,27 +3646,25 @@ class ProfileComparisonLibComponent {
|
|
|
3690
3646
|
// Release the node. Clear fx and fy so it is no longer pinned to the mouse
|
|
3691
3647
|
this.draggedNode.fx = undefined;
|
|
3692
3648
|
this.draggedNode.fy = undefined;
|
|
3693
|
-
//
|
|
3649
|
+
// Keep baseline similarity attraction and restoring forces active after drag release.
|
|
3694
3650
|
this.simulation.force('attraction', null);
|
|
3695
|
-
// Set other forces to 0 so all chips glide directly to their starting coordinates
|
|
3696
3651
|
if (this.simulation) {
|
|
3697
3652
|
const linkForce = this.simulation.force('link');
|
|
3698
3653
|
if (linkForce)
|
|
3699
|
-
linkForce.strength(
|
|
3654
|
+
linkForce.strength((d) => this.getRuntimeLinkStrength(d));
|
|
3700
3655
|
const collideForce = this.simulation.force('collide');
|
|
3701
3656
|
if (collideForce)
|
|
3702
|
-
collideForce.strength(0
|
|
3703
|
-
// Pull nodes smoothly and slowly back to their exact starting points
|
|
3657
|
+
collideForce.strength(1.0);
|
|
3704
3658
|
const xTarget = this.simulation.force('xTarget');
|
|
3705
3659
|
if (xTarget)
|
|
3706
|
-
xTarget.strength(0.
|
|
3660
|
+
xTarget.strength((d) => d.group === 'shared' ? 0.12 : 0.045);
|
|
3707
3661
|
const yTarget = this.simulation.force('yTarget');
|
|
3708
3662
|
if (yTarget)
|
|
3709
|
-
yTarget.strength(0.
|
|
3663
|
+
yTarget.strength(0.045);
|
|
3710
3664
|
}
|
|
3711
3665
|
// Cool down simulation targets
|
|
3712
3666
|
this.simulation.alphaTarget(0);
|
|
3713
|
-
this.simulation.alpha(0.
|
|
3667
|
+
this.simulation.alpha(0.4).restart();
|
|
3714
3668
|
this.draggedNode = null;
|
|
3715
3669
|
this.isDragging = false;
|
|
3716
3670
|
}
|