@naniteninja/profile-comparison-lib 1.0.26 → 1.0.27
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.
|
@@ -17,9 +17,23 @@ var BackendMode;
|
|
|
17
17
|
/** Use a local mock server at localhost:44101. */
|
|
18
18
|
BackendMode[BackendMode["Mock"] = 1] = "Mock";
|
|
19
19
|
})(BackendMode || (BackendMode = {}));
|
|
20
|
+
const PROFILE_COMPARISON_PROD_API_BASE_URL = 'https://api.test.thecyrano.app/api/profile-comparison/api';
|
|
21
|
+
const PROFILE_COMPARISON_LOCAL_API_BASE_URL = 'http://localhost:44101/api';
|
|
22
|
+
function isLocalBrowserRuntime() {
|
|
23
|
+
if (typeof window === 'undefined') {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
const hostname = window.location?.hostname?.toLowerCase() ?? '';
|
|
27
|
+
return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '0.0.0.0';
|
|
28
|
+
}
|
|
29
|
+
function resolveDefaultRealBackendUrl() {
|
|
30
|
+
return isLocalBrowserRuntime()
|
|
31
|
+
? PROFILE_COMPARISON_LOCAL_API_BASE_URL
|
|
32
|
+
: PROFILE_COMPARISON_PROD_API_BASE_URL;
|
|
33
|
+
}
|
|
20
34
|
const BACKEND_MODE_URLS = {
|
|
21
|
-
[BackendMode.Real]:
|
|
22
|
-
[BackendMode.Mock]:
|
|
35
|
+
[BackendMode.Real]: resolveDefaultRealBackendUrl(),
|
|
36
|
+
[BackendMode.Mock]: PROFILE_COMPARISON_LOCAL_API_BASE_URL,
|
|
23
37
|
};
|
|
24
38
|
/**
|
|
25
39
|
* Legacy injection token for the backend base URL.
|
|
@@ -1718,6 +1732,39 @@ class ProfileComparisonLibComponent {
|
|
|
1718
1732
|
static MIN_CROSS_LINKS_PER_NODE = 0;
|
|
1719
1733
|
static MAX_INTRA_LINKS_PER_NODE = 2;
|
|
1720
1734
|
static MAX_RELEVANT_INTERESTS_PER_SIDE = 6;
|
|
1735
|
+
static COLUMN_CORE_SEED_PIXELS = 9;
|
|
1736
|
+
static IDLE_ALPHA_TARGET = 0.012;
|
|
1737
|
+
static CLICK_REHEAT_ALPHA = 0.065;
|
|
1738
|
+
static SEMANTIC_STABILITY_ALPHA_FLOOR = 0.012;
|
|
1739
|
+
static RELATIVE_MEDIAN_ALPHA_FLOOR = 0.01;
|
|
1740
|
+
static RELATIVE_MEDIAN_ATTRACT_STRENGTH = 0.44;
|
|
1741
|
+
static RELATIVE_MEDIAN_REPEL_STRENGTH = 0.58;
|
|
1742
|
+
static RELATIVE_MEDIAN_ATTRACT_RADIUS = 72;
|
|
1743
|
+
static RELATIVE_MEDIAN_REPEL_RADIUS = 194;
|
|
1744
|
+
static STRONG_SIMILARITY_BOOST_EXPONENT = 3.2;
|
|
1745
|
+
static STRONG_SIMILARITY_BOOST_SCALE = 0.5;
|
|
1746
|
+
static PRIORITY_PAIR_MIN_SIMILARITY = 0.62;
|
|
1747
|
+
static PRIORITY_PAIR_ATTRACTION_STRENGTH = 1.02;
|
|
1748
|
+
static PRIORITY_PAIR_CORRIDOR_RADIUS = 42;
|
|
1749
|
+
static PRIORITY_PAIR_CORRIDOR_REPEL_STRENGTH = 0.64;
|
|
1750
|
+
static MUTUAL_TOP_PAIR_MIN_SIMILARITY = 0.58;
|
|
1751
|
+
static MUTUAL_TOP_PAIR_MARGIN = 0.12;
|
|
1752
|
+
static MUTUAL_TOP_PAIR_ATTRACTION_STRENGTH = 1.12;
|
|
1753
|
+
static MUTUAL_TOP_PAIR_BARRIER_RADIUS = 56;
|
|
1754
|
+
static MUTUAL_TOP_PAIR_REPEL_STRENGTH = 1.06;
|
|
1755
|
+
static DETERMINISTIC_SEED_PASSES = 6;
|
|
1756
|
+
static DETERMINISTIC_SEED_BLEND = 0.86;
|
|
1757
|
+
static OBJECTIVE_OPTIMIZER_ITERATIONS = 56;
|
|
1758
|
+
static OBJECTIVE_STEP_START = 0.34;
|
|
1759
|
+
static OBJECTIVE_STEP_END = 0.09;
|
|
1760
|
+
static OBJECTIVE_ATTRACTION_WEIGHT = 0.74;
|
|
1761
|
+
static OBJECTIVE_REPULSION_WEIGHT = 0.5;
|
|
1762
|
+
static OBJECTIVE_ANCHOR_WEIGHT = 0.34;
|
|
1763
|
+
static OBJECTIVE_BOUNDARY_WEIGHT = 0.92;
|
|
1764
|
+
static DISSIMILAR_REPEL_THRESHOLD = 0.52;
|
|
1765
|
+
static DISSIMILAR_REPEL_STRENGTH = 0.34;
|
|
1766
|
+
static DISSIMILAR_REPEL_MAX_DISTANCE = 220;
|
|
1767
|
+
static DISSIMILAR_REPEL_DECAY_DISTANCE = 130;
|
|
1721
1768
|
static CHIP_RADIUS = 18; // half chip height for collision
|
|
1722
1769
|
static CHIP_COLLISION_BORDER_PADDING = 6;
|
|
1723
1770
|
static MAX_LINK_DISTANCE = 120;
|
|
@@ -2731,17 +2778,33 @@ class ProfileComparisonLibComponent {
|
|
|
2731
2778
|
if (matrixMax <= springThreshold)
|
|
2732
2779
|
matrixMax = 1.0;
|
|
2733
2780
|
const simRange = matrixMax - springThreshold;
|
|
2734
|
-
const
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2781
|
+
const columnCoreY = minCenterY + verticalSpan * 0.5;
|
|
2782
|
+
const getMedian = (values) => {
|
|
2783
|
+
if (!values.length)
|
|
2784
|
+
return 0;
|
|
2785
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
2786
|
+
const mid = Math.floor(sorted.length / 2);
|
|
2787
|
+
if (sorted.length % 2 === 1)
|
|
2788
|
+
return sorted[mid];
|
|
2789
|
+
return (sorted[mid - 1] + sorted[mid]) / 2;
|
|
2790
|
+
};
|
|
2791
|
+
const getTopSixMedian = (values) => {
|
|
2792
|
+
if (!values.length)
|
|
2793
|
+
return 0;
|
|
2794
|
+
const top = [...values].sort((a, b) => b - a).slice(0, Math.min(6, values.length));
|
|
2795
|
+
return getMedian(top);
|
|
2796
|
+
};
|
|
2797
|
+
const hashToken = (value) => {
|
|
2798
|
+
let hash = 0;
|
|
2799
|
+
for (let i = 0; i < value.length; i++) {
|
|
2800
|
+
hash = ((hash << 5) - hash + value.charCodeAt(i)) | 0;
|
|
2801
|
+
}
|
|
2802
|
+
return Math.abs(hash);
|
|
2739
2803
|
};
|
|
2740
2804
|
const nodes = [];
|
|
2741
2805
|
const addNodes = (items, group, zoneX) => {
|
|
2742
|
-
const total = items.length;
|
|
2743
2806
|
items.forEach((label, i) => {
|
|
2744
|
-
const targetY =
|
|
2807
|
+
const targetY = columnCoreY;
|
|
2745
2808
|
nodes.push({
|
|
2746
2809
|
id: `${group}-${i}-${label}`,
|
|
2747
2810
|
label,
|
|
@@ -2782,8 +2845,10 @@ class ProfileComparisonLibComponent {
|
|
|
2782
2845
|
: zoneP2 + (zoneShared - zoneP2) * centerPull;
|
|
2783
2846
|
}
|
|
2784
2847
|
});
|
|
2785
|
-
//
|
|
2786
|
-
//
|
|
2848
|
+
// Build a relative attractiveness score (median of top-six similarities)
|
|
2849
|
+
// and seed chips from each column core with only a tiny 9x9 perturbation.
|
|
2850
|
+
// This lets forces "emanate" from a shared origin while preserving
|
|
2851
|
+
// deterministic reset behavior.
|
|
2787
2852
|
const semanticPool = (node) => {
|
|
2788
2853
|
if (node.group === 'p1')
|
|
2789
2854
|
return nodes.filter(n => n.group === 'p2' || n.group === 'shared');
|
|
@@ -2791,35 +2856,50 @@ class ProfileComparisonLibComponent {
|
|
|
2791
2856
|
return nodes.filter(n => n.group === 'p1' || n.group === 'shared');
|
|
2792
2857
|
return nodes.filter(n => n.group === 'p1' || n.group === 'p2');
|
|
2793
2858
|
};
|
|
2794
|
-
const
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2859
|
+
const getSemanticAttractiveness = (node) => {
|
|
2860
|
+
const pool = semanticPool(node);
|
|
2861
|
+
if (!pool.length)
|
|
2862
|
+
return 0;
|
|
2863
|
+
const similarities = pool.map(other => Math.max(0, this.getSimilarity(node.label, other.label)));
|
|
2864
|
+
return getTopSixMedian(similarities);
|
|
2865
|
+
};
|
|
2866
|
+
const microSeedSpan = ProfileComparisonLibComponent.COLUMN_CORE_SEED_PIXELS;
|
|
2867
|
+
const microSeedHalf = microSeedSpan / 2;
|
|
2868
|
+
const deterministicPasses = ProfileComparisonLibComponent.DETERMINISTIC_SEED_PASSES;
|
|
2869
|
+
const deterministicBlend = ProfileComparisonLibComponent.DETERMINISTIC_SEED_BLEND;
|
|
2870
|
+
for (let pass = 0; pass < deterministicPasses; pass++) {
|
|
2871
|
+
['p1', 'shared', 'p2'].forEach(group => {
|
|
2872
|
+
const groupNodes = nodes.filter(node => node.group === group);
|
|
2873
|
+
if (groupNodes.length <= 1)
|
|
2803
2874
|
return;
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2875
|
+
const ranked = groupNodes
|
|
2876
|
+
.map(node => ({
|
|
2877
|
+
node,
|
|
2878
|
+
attractiveness: getSemanticAttractiveness(node),
|
|
2879
|
+
tie: this.normalizeSimilarityToken(node.label),
|
|
2880
|
+
}))
|
|
2881
|
+
.sort((a, b) => (b.attractiveness - a.attractiveness) || a.tie.localeCompare(b.tie));
|
|
2882
|
+
ranked.forEach((entry, index) => {
|
|
2883
|
+
const rankT = ranked.length <= 1 ? 0.5 : (index / Math.max(1, ranked.length - 1));
|
|
2884
|
+
const yOffset = (rankT - 0.5) * microSeedSpan;
|
|
2885
|
+
const desiredY = Math.max(minCenterY, Math.min(maxY, columnCoreY + yOffset));
|
|
2886
|
+
const currentY = entry.node.targetY ?? columnCoreY;
|
|
2887
|
+
entry.node.targetY = Math.max(minCenterY, Math.min(maxY, currentY + (desiredY - currentY) * deterministicBlend));
|
|
2888
|
+
const xSeedRaw = hashToken(`${entry.node.id}:${pass}`) % (microSeedSpan + 1);
|
|
2889
|
+
const xOffset = xSeedRaw - microSeedHalf;
|
|
2890
|
+
const baseTargetX = entry.node.targetX ?? (entry.node.group === 'p1'
|
|
2891
|
+
? zoneP1
|
|
2892
|
+
: (entry.node.group === 'p2' ? zoneP2 : zoneShared));
|
|
2893
|
+
entry.node.x = Math.max(globalMinX, Math.min(globalMaxX, baseTargetX + xOffset));
|
|
2894
|
+
entry.node.y = entry.node.targetY;
|
|
2814
2895
|
});
|
|
2815
|
-
if (totalWeight <= 0)
|
|
2816
|
-
return;
|
|
2817
|
-
const semanticY = weightedY / totalWeight;
|
|
2818
|
-
const currentY = snapshotY.get(node.id) ?? semanticY;
|
|
2819
|
-
const blend = node.group === 'shared' ? 0.84 : 0.78;
|
|
2820
|
-
node.targetY = Math.max(minCenterY, Math.min(maxY, currentY + (semanticY - currentY) * blend));
|
|
2821
2896
|
});
|
|
2822
2897
|
}
|
|
2898
|
+
nodes.forEach(node => {
|
|
2899
|
+
const fallbackX = node.targetX ?? (node.group === 'p1' ? zoneP1 : (node.group === 'p2' ? zoneP2 : zoneShared));
|
|
2900
|
+
node.x = Math.max(globalMinX, Math.min(globalMaxX, node.x ?? fallbackX));
|
|
2901
|
+
node.y = Math.max(minCenterY, Math.min(maxY, node.targetY ?? columnCoreY));
|
|
2902
|
+
});
|
|
2823
2903
|
const buildNodeBounds = (node) => {
|
|
2824
2904
|
let minX = globalMinX;
|
|
2825
2905
|
let maxX = globalMaxX;
|
|
@@ -2999,10 +3079,96 @@ class ProfileComparisonLibComponent {
|
|
|
2999
3079
|
};
|
|
3000
3080
|
})
|
|
3001
3081
|
.filter((record) => !!record);
|
|
3082
|
+
const dissimilarThreshold = ProfileComparisonLibComponent.DISSIMILAR_REPEL_THRESHOLD;
|
|
3083
|
+
const dissimilarPairs = [];
|
|
3084
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
3085
|
+
for (let j = i + 1; j < nodes.length; j++) {
|
|
3086
|
+
const source = nodes[i];
|
|
3087
|
+
const target = nodes[j];
|
|
3088
|
+
const similarity = Math.max(0, this.getSimilarity(source.label, target.label));
|
|
3089
|
+
if (similarity >= dissimilarThreshold)
|
|
3090
|
+
continue;
|
|
3091
|
+
const laneFactor = source.group === target.group
|
|
3092
|
+
? 1
|
|
3093
|
+
: (source.group === 'shared' || target.group === 'shared' ? 0.62 : 0.44);
|
|
3094
|
+
const repulsion = laneFactor * (dissimilarThreshold - similarity) / Math.max(0.001, dissimilarThreshold);
|
|
3095
|
+
dissimilarPairs.push({
|
|
3096
|
+
source,
|
|
3097
|
+
target,
|
|
3098
|
+
repulsion,
|
|
3099
|
+
});
|
|
3100
|
+
}
|
|
3101
|
+
}
|
|
3102
|
+
const relativeMedianByNode = new Map();
|
|
3103
|
+
const relativeMedianPairs = [];
|
|
3104
|
+
['p1', 'shared', 'p2'].forEach(group => {
|
|
3105
|
+
const groupNodes = nodes.filter(node => node.group === group);
|
|
3106
|
+
if (groupNodes.length <= 1)
|
|
3107
|
+
return;
|
|
3108
|
+
groupNodes.forEach(node => {
|
|
3109
|
+
const similarities = groupNodes
|
|
3110
|
+
.filter(other => other.id !== node.id)
|
|
3111
|
+
.map(other => Math.max(0, this.getSimilarity(node.label, other.label)));
|
|
3112
|
+
relativeMedianByNode.set(node.id, getTopSixMedian(similarities));
|
|
3113
|
+
});
|
|
3114
|
+
for (let i = 0; i < groupNodes.length; i++) {
|
|
3115
|
+
for (let j = i + 1; j < groupNodes.length; j++) {
|
|
3116
|
+
const source = groupNodes[i];
|
|
3117
|
+
const target = groupNodes[j];
|
|
3118
|
+
const similarity = Math.max(0, this.getSimilarity(source.label, target.label));
|
|
3119
|
+
const sourceMedian = relativeMedianByNode.get(source.id) ?? 0;
|
|
3120
|
+
const targetMedian = relativeMedianByNode.get(target.id) ?? 0;
|
|
3121
|
+
const median = (sourceMedian + targetMedian) / 2;
|
|
3122
|
+
relativeMedianPairs.push({ source, target, similarity, median });
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
});
|
|
3002
3126
|
const maxLinkSimilarity = Math.max(0.001, ...linkRecords.map(record => Math.max(0, record.similarity)));
|
|
3003
3127
|
const MIN_PAIR_DISTANCE = 34;
|
|
3004
3128
|
const MAX_PAIR_DISTANCE = 88;
|
|
3129
|
+
const semanticAlphaFloor = ProfileComparisonLibComponent.SEMANTIC_STABILITY_ALPHA_FLOOR;
|
|
3130
|
+
const relativeMedianColumnForce = (alpha) => {
|
|
3131
|
+
const effectiveAlpha = Math.max(alpha, ProfileComparisonLibComponent.RELATIVE_MEDIAN_ALPHA_FLOOR);
|
|
3132
|
+
const attractStrength = ProfileComparisonLibComponent.RELATIVE_MEDIAN_ATTRACT_STRENGTH;
|
|
3133
|
+
const repelStrength = ProfileComparisonLibComponent.RELATIVE_MEDIAN_REPEL_STRENGTH;
|
|
3134
|
+
const attractRadius = ProfileComparisonLibComponent.RELATIVE_MEDIAN_ATTRACT_RADIUS;
|
|
3135
|
+
const repelRadius = ProfileComparisonLibComponent.RELATIVE_MEDIAN_REPEL_RADIUS;
|
|
3136
|
+
const deltaDeadband = 0.025;
|
|
3137
|
+
relativeMedianPairs.forEach(pair => {
|
|
3138
|
+
const dx = (pair.target.x ?? 0) - (pair.source.x ?? 0);
|
|
3139
|
+
const dy = (pair.target.y ?? 0) - (pair.source.y ?? 0);
|
|
3140
|
+
const dist = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
3141
|
+
const delta = pair.similarity - pair.median;
|
|
3142
|
+
if (Math.abs(delta) <= deltaDeadband)
|
|
3143
|
+
return;
|
|
3144
|
+
if (delta >= 0) {
|
|
3145
|
+
const closenessBias = Math.max(0.08, Math.min(1, delta + 0.08));
|
|
3146
|
+
const targetDistance = Math.max(24, attractRadius - closenessBias * 46);
|
|
3147
|
+
if (dist <= targetDistance + 2)
|
|
3148
|
+
return;
|
|
3149
|
+
const pull = attractStrength * effectiveAlpha * closenessBias * ((dist - targetDistance) / dist);
|
|
3150
|
+
pair.source.vx = (pair.source.vx ?? 0) + dx * pull;
|
|
3151
|
+
pair.source.vy = (pair.source.vy ?? 0) + dy * pull;
|
|
3152
|
+
pair.target.vx = (pair.target.vx ?? 0) - dx * pull;
|
|
3153
|
+
pair.target.vy = (pair.target.vy ?? 0) - dy * pull;
|
|
3154
|
+
return;
|
|
3155
|
+
}
|
|
3156
|
+
if (dist >= repelRadius || dist <= 28)
|
|
3157
|
+
return;
|
|
3158
|
+
const underMedian = Math.min(1, Math.abs(delta) / Math.max(0.001, pair.median + 0.05));
|
|
3159
|
+
const push = repelStrength * effectiveAlpha * underMedian * (1 - (dist / repelRadius));
|
|
3160
|
+
if (push <= 0)
|
|
3161
|
+
return;
|
|
3162
|
+
const unitX = dx / dist;
|
|
3163
|
+
const unitY = dy / dist;
|
|
3164
|
+
pair.source.vx = (pair.source.vx ?? 0) - unitX * push;
|
|
3165
|
+
pair.source.vy = (pair.source.vy ?? 0) - unitY * push;
|
|
3166
|
+
pair.target.vx = (pair.target.vx ?? 0) + unitX * push;
|
|
3167
|
+
pair.target.vy = (pair.target.vy ?? 0) + unitY * push;
|
|
3168
|
+
});
|
|
3169
|
+
};
|
|
3005
3170
|
const similarityAttractionForce = (alpha) => {
|
|
3171
|
+
const effectiveAlpha = Math.max(alpha, semanticAlphaFloor);
|
|
3006
3172
|
linkRecords.forEach(record => {
|
|
3007
3173
|
const dx = (record.target.x ?? 0) - (record.source.x ?? 0);
|
|
3008
3174
|
const dy = (record.target.y ?? 0) - (record.source.y ?? 0);
|
|
@@ -3012,14 +3178,348 @@ class ProfileComparisonLibComponent {
|
|
|
3012
3178
|
if (dist <= targetDistance)
|
|
3013
3179
|
return;
|
|
3014
3180
|
const pullGap = dist - targetDistance;
|
|
3015
|
-
const pullBase = record.kind === 'cross' ? 0.
|
|
3016
|
-
const
|
|
3181
|
+
const pullBase = record.kind === 'cross' ? 0.86 : 0.78;
|
|
3182
|
+
const nonlinearBoost = 1
|
|
3183
|
+
+ Math.pow(normalizedSimilarity, ProfileComparisonLibComponent.STRONG_SIMILARITY_BOOST_EXPONENT)
|
|
3184
|
+
* ProfileComparisonLibComponent.STRONG_SIMILARITY_BOOST_SCALE;
|
|
3185
|
+
const pullFactor = pullBase * nonlinearBoost * effectiveAlpha * (pullGap / dist);
|
|
3017
3186
|
record.source.vx = (record.source.vx ?? 0) + dx * pullFactor;
|
|
3018
3187
|
record.source.vy = (record.source.vy ?? 0) + dy * pullFactor;
|
|
3019
3188
|
record.target.vx = (record.target.vx ?? 0) - dx * pullFactor;
|
|
3020
3189
|
record.target.vy = (record.target.vy ?? 0) - dy * pullFactor;
|
|
3021
3190
|
});
|
|
3022
3191
|
};
|
|
3192
|
+
const dissimilarRepulsionForce = (alpha) => {
|
|
3193
|
+
const effectiveAlpha = Math.max(alpha, semanticAlphaFloor * 0.9);
|
|
3194
|
+
const maxDistance = ProfileComparisonLibComponent.DISSIMILAR_REPEL_MAX_DISTANCE;
|
|
3195
|
+
const decayDistance = ProfileComparisonLibComponent.DISSIMILAR_REPEL_DECAY_DISTANCE;
|
|
3196
|
+
const baseStrength = ProfileComparisonLibComponent.DISSIMILAR_REPEL_STRENGTH;
|
|
3197
|
+
dissimilarPairs.forEach(record => {
|
|
3198
|
+
const dx = (record.target.x ?? 0) - (record.source.x ?? 0);
|
|
3199
|
+
const dy = (record.target.y ?? 0) - (record.source.y ?? 0);
|
|
3200
|
+
const rawDistance = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
3201
|
+
if (rawDistance >= maxDistance)
|
|
3202
|
+
return;
|
|
3203
|
+
const distance = Math.max(8, rawDistance);
|
|
3204
|
+
const closeness = 1 - Math.min(1, distance / maxDistance);
|
|
3205
|
+
const decay = Math.exp(-Math.pow(distance / Math.max(1, decayDistance), 2));
|
|
3206
|
+
const push = baseStrength * effectiveAlpha * record.repulsion * closeness * decay;
|
|
3207
|
+
if (push <= 0)
|
|
3208
|
+
return;
|
|
3209
|
+
const unitX = dx / distance;
|
|
3210
|
+
const unitY = dy / distance;
|
|
3211
|
+
record.source.vx = (record.source.vx ?? 0) - unitX * push;
|
|
3212
|
+
record.source.vy = (record.source.vy ?? 0) - unitY * push;
|
|
3213
|
+
record.target.vx = (record.target.vx ?? 0) + unitX * push;
|
|
3214
|
+
record.target.vy = (record.target.vy ?? 0) + unitY * push;
|
|
3215
|
+
});
|
|
3216
|
+
};
|
|
3217
|
+
const priorityMinSimilarity = ProfileComparisonLibComponent.PRIORITY_PAIR_MIN_SIMILARITY;
|
|
3218
|
+
const priorityPairCandidates = [...crossCandidates, ...intraCandidates]
|
|
3219
|
+
.filter(link => link.similarity >= priorityMinSimilarity)
|
|
3220
|
+
.sort((a, b) => (b.similarity - a.similarity) || (b.strength - a.strength));
|
|
3221
|
+
const priorityPairKeys = new Set();
|
|
3222
|
+
const endpointClaims = new Map();
|
|
3223
|
+
const maxPairsPerNode = 2;
|
|
3224
|
+
const getClaimCount = (id) => endpointClaims.get(id) ?? 0;
|
|
3225
|
+
const priorityPairs = priorityPairCandidates
|
|
3226
|
+
.filter(candidate => {
|
|
3227
|
+
const sourceId = this.getLinkNodeId(candidate.source);
|
|
3228
|
+
const targetId = this.getLinkNodeId(candidate.target);
|
|
3229
|
+
const key = this.getLinkPairKey(sourceId, targetId);
|
|
3230
|
+
if (priorityPairKeys.has(key))
|
|
3231
|
+
return false;
|
|
3232
|
+
if (getClaimCount(sourceId) >= maxPairsPerNode || getClaimCount(targetId) >= maxPairsPerNode)
|
|
3233
|
+
return false;
|
|
3234
|
+
priorityPairKeys.add(key);
|
|
3235
|
+
endpointClaims.set(sourceId, getClaimCount(sourceId) + 1);
|
|
3236
|
+
endpointClaims.set(targetId, getClaimCount(targetId) + 1);
|
|
3237
|
+
return true;
|
|
3238
|
+
})
|
|
3239
|
+
.map(link => {
|
|
3240
|
+
const source = typeof link.source === 'object' ? link.source : nodeById.get(String(link.source));
|
|
3241
|
+
const target = typeof link.target === 'object' ? link.target : nodeById.get(String(link.target));
|
|
3242
|
+
if (!source || !target)
|
|
3243
|
+
return null;
|
|
3244
|
+
return {
|
|
3245
|
+
source,
|
|
3246
|
+
target,
|
|
3247
|
+
similarity: Math.max(priorityMinSimilarity, Number(link.similarity ?? 0)),
|
|
3248
|
+
};
|
|
3249
|
+
})
|
|
3250
|
+
.filter((pair) => !!pair);
|
|
3251
|
+
const maxPrioritySimilarity = Math.max(priorityMinSimilarity, ...priorityPairs.map(pair => pair.similarity));
|
|
3252
|
+
const priorityPairForce = (alpha) => {
|
|
3253
|
+
const effectiveAlpha = Math.max(alpha, semanticAlphaFloor);
|
|
3254
|
+
const pairStrength = ProfileComparisonLibComponent.PRIORITY_PAIR_ATTRACTION_STRENGTH;
|
|
3255
|
+
const corridorRadius = ProfileComparisonLibComponent.PRIORITY_PAIR_CORRIDOR_RADIUS;
|
|
3256
|
+
const corridorRepel = ProfileComparisonLibComponent.PRIORITY_PAIR_CORRIDOR_REPEL_STRENGTH;
|
|
3257
|
+
priorityPairs.forEach(pair => {
|
|
3258
|
+
const ax = pair.source.x ?? 0;
|
|
3259
|
+
const ay = pair.source.y ?? 0;
|
|
3260
|
+
const bx = pair.target.x ?? 0;
|
|
3261
|
+
const by = pair.target.y ?? 0;
|
|
3262
|
+
const dx = bx - ax;
|
|
3263
|
+
const dy = by - ay;
|
|
3264
|
+
const dist = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
3265
|
+
const similarityNorm = Math.max(0, Math.min(1, pair.similarity / Math.max(0.001, maxPrioritySimilarity)));
|
|
3266
|
+
const targetDistance = 26 + (1 - similarityNorm) * 20;
|
|
3267
|
+
if (dist > targetDistance) {
|
|
3268
|
+
const pull = pairStrength * effectiveAlpha * ((dist - targetDistance) / dist);
|
|
3269
|
+
pair.source.vx = (pair.source.vx ?? 0) + dx * pull;
|
|
3270
|
+
pair.source.vy = (pair.source.vy ?? 0) + dy * pull;
|
|
3271
|
+
pair.target.vx = (pair.target.vx ?? 0) - dx * pull;
|
|
3272
|
+
pair.target.vy = (pair.target.vy ?? 0) - dy * pull;
|
|
3273
|
+
}
|
|
3274
|
+
const segmentLenSq = dx * dx + dy * dy;
|
|
3275
|
+
if (segmentLenSq <= 1)
|
|
3276
|
+
return;
|
|
3277
|
+
nodes.forEach(candidate => {
|
|
3278
|
+
if (candidate.id === pair.source.id || candidate.id === pair.target.id)
|
|
3279
|
+
return;
|
|
3280
|
+
const candidateSimilarity = Math.max(this.getSimilarity(candidate.label, pair.source.label), this.getSimilarity(candidate.label, pair.target.label));
|
|
3281
|
+
if (candidateSimilarity >= pair.similarity * 0.74)
|
|
3282
|
+
return;
|
|
3283
|
+
const px = candidate.x ?? 0;
|
|
3284
|
+
const py = candidate.y ?? 0;
|
|
3285
|
+
const tRaw = ((px - ax) * dx + (py - ay) * dy) / segmentLenSq;
|
|
3286
|
+
const t = Math.max(0, Math.min(1, tRaw));
|
|
3287
|
+
if (t <= 0.12 || t >= 0.88)
|
|
3288
|
+
return;
|
|
3289
|
+
const closestX = ax + dx * t;
|
|
3290
|
+
const closestY = ay + dy * t;
|
|
3291
|
+
const offX = px - closestX;
|
|
3292
|
+
const offY = py - closestY;
|
|
3293
|
+
const offDist = Math.sqrt(offX * offX + offY * offY) || 1;
|
|
3294
|
+
if (offDist >= corridorRadius)
|
|
3295
|
+
return;
|
|
3296
|
+
const proximity = 1 - (offDist / corridorRadius);
|
|
3297
|
+
const mismatch = 1 - Math.max(0, Math.min(1, candidateSimilarity / Math.max(0.001, pair.similarity)));
|
|
3298
|
+
const push = corridorRepel * effectiveAlpha * proximity * mismatch;
|
|
3299
|
+
if (push <= 0)
|
|
3300
|
+
return;
|
|
3301
|
+
const unitOffX = offX / offDist;
|
|
3302
|
+
const unitOffY = offY / offDist;
|
|
3303
|
+
candidate.vx = (candidate.vx ?? 0) + unitOffX * push;
|
|
3304
|
+
candidate.vy = (candidate.vy ?? 0) + unitOffY * push;
|
|
3305
|
+
// Counter-force keeps the pair stable while opening a semantic corridor.
|
|
3306
|
+
const damp = push * 0.24;
|
|
3307
|
+
pair.source.vx = (pair.source.vx ?? 0) - unitOffX * damp;
|
|
3308
|
+
pair.source.vy = (pair.source.vy ?? 0) - unitOffY * damp;
|
|
3309
|
+
pair.target.vx = (pair.target.vx ?? 0) - unitOffX * damp;
|
|
3310
|
+
pair.target.vy = (pair.target.vy ?? 0) - unitOffY * damp;
|
|
3311
|
+
});
|
|
3312
|
+
});
|
|
3313
|
+
};
|
|
3314
|
+
const bestByNode = new Map();
|
|
3315
|
+
nodes.forEach(node => {
|
|
3316
|
+
let bestNode = null;
|
|
3317
|
+
let best = -1;
|
|
3318
|
+
let second = -1;
|
|
3319
|
+
nodes.forEach(other => {
|
|
3320
|
+
if (other.id === node.id)
|
|
3321
|
+
return;
|
|
3322
|
+
const similarity = Math.max(0, this.getSimilarity(node.label, other.label));
|
|
3323
|
+
if (similarity > best) {
|
|
3324
|
+
second = best;
|
|
3325
|
+
best = similarity;
|
|
3326
|
+
bestNode = other;
|
|
3327
|
+
}
|
|
3328
|
+
else if (similarity > second) {
|
|
3329
|
+
second = similarity;
|
|
3330
|
+
}
|
|
3331
|
+
});
|
|
3332
|
+
bestByNode.set(node.id, { node: bestNode, best: Math.max(0, best), second: Math.max(0, second) });
|
|
3333
|
+
});
|
|
3334
|
+
const mutualPairMinSimilarity = ProfileComparisonLibComponent.MUTUAL_TOP_PAIR_MIN_SIMILARITY;
|
|
3335
|
+
const mutualPairMargin = ProfileComparisonLibComponent.MUTUAL_TOP_PAIR_MARGIN;
|
|
3336
|
+
const mutualPairKeys = new Set();
|
|
3337
|
+
const mutualTopPairs = nodes
|
|
3338
|
+
.map(node => {
|
|
3339
|
+
const best = bestByNode.get(node.id);
|
|
3340
|
+
if (!best?.node)
|
|
3341
|
+
return null;
|
|
3342
|
+
if (best.best < mutualPairMinSimilarity)
|
|
3343
|
+
return null;
|
|
3344
|
+
if (best.best - best.second < mutualPairMargin)
|
|
3345
|
+
return null;
|
|
3346
|
+
const reverse = bestByNode.get(best.node.id);
|
|
3347
|
+
if (!reverse?.node || reverse.node.id !== node.id)
|
|
3348
|
+
return null;
|
|
3349
|
+
if (reverse.best < mutualPairMinSimilarity || reverse.best - reverse.second < mutualPairMargin)
|
|
3350
|
+
return null;
|
|
3351
|
+
const key = this.getLinkPairKey(node.id, best.node.id);
|
|
3352
|
+
if (mutualPairKeys.has(key))
|
|
3353
|
+
return null;
|
|
3354
|
+
mutualPairKeys.add(key);
|
|
3355
|
+
return {
|
|
3356
|
+
source: node,
|
|
3357
|
+
target: best.node,
|
|
3358
|
+
similarity: Math.min(best.best, reverse.best),
|
|
3359
|
+
};
|
|
3360
|
+
})
|
|
3361
|
+
.filter((pair) => !!pair);
|
|
3362
|
+
const mutualTopPairForce = (alpha) => {
|
|
3363
|
+
const effectiveAlpha = Math.max(alpha, semanticAlphaFloor * 1.15);
|
|
3364
|
+
const attractionStrength = ProfileComparisonLibComponent.MUTUAL_TOP_PAIR_ATTRACTION_STRENGTH;
|
|
3365
|
+
const baseBarrierRadius = ProfileComparisonLibComponent.MUTUAL_TOP_PAIR_BARRIER_RADIUS;
|
|
3366
|
+
const repelStrength = ProfileComparisonLibComponent.MUTUAL_TOP_PAIR_REPEL_STRENGTH;
|
|
3367
|
+
mutualTopPairs.forEach(pair => {
|
|
3368
|
+
const ax = pair.source.x ?? 0;
|
|
3369
|
+
const ay = pair.source.y ?? 0;
|
|
3370
|
+
const bx = pair.target.x ?? 0;
|
|
3371
|
+
const by = pair.target.y ?? 0;
|
|
3372
|
+
const dx = bx - ax;
|
|
3373
|
+
const dy = by - ay;
|
|
3374
|
+
const dist = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
3375
|
+
const norm = Math.max(0, Math.min(1, pair.similarity));
|
|
3376
|
+
const targetDistance = 24 + (1 - norm) * 26;
|
|
3377
|
+
if (dist > targetDistance) {
|
|
3378
|
+
const pull = attractionStrength * effectiveAlpha * ((dist - targetDistance) / dist);
|
|
3379
|
+
pair.source.vx = (pair.source.vx ?? 0) + dx * pull;
|
|
3380
|
+
pair.source.vy = (pair.source.vy ?? 0) + dy * pull;
|
|
3381
|
+
pair.target.vx = (pair.target.vx ?? 0) - dx * pull;
|
|
3382
|
+
pair.target.vy = (pair.target.vy ?? 0) - dy * pull;
|
|
3383
|
+
}
|
|
3384
|
+
const segmentLenSq = dx * dx + dy * dy;
|
|
3385
|
+
if (segmentLenSq <= 1)
|
|
3386
|
+
return;
|
|
3387
|
+
const barrierRadius = baseBarrierRadius + (1 - norm) * 10;
|
|
3388
|
+
nodes.forEach(candidate => {
|
|
3389
|
+
if (candidate.id === pair.source.id || candidate.id === pair.target.id)
|
|
3390
|
+
return;
|
|
3391
|
+
const candidateSimilarity = Math.max(this.getSimilarity(candidate.label, pair.source.label), this.getSimilarity(candidate.label, pair.target.label));
|
|
3392
|
+
if (candidateSimilarity >= pair.similarity * 0.76)
|
|
3393
|
+
return;
|
|
3394
|
+
const px = candidate.x ?? 0;
|
|
3395
|
+
const py = candidate.y ?? 0;
|
|
3396
|
+
const tRaw = ((px - ax) * dx + (py - ay) * dy) / segmentLenSq;
|
|
3397
|
+
const t = Math.max(0, Math.min(1, tRaw));
|
|
3398
|
+
if (t <= 0.08 || t >= 0.92)
|
|
3399
|
+
return;
|
|
3400
|
+
const closestX = ax + dx * t;
|
|
3401
|
+
const closestY = ay + dy * t;
|
|
3402
|
+
const offX = px - closestX;
|
|
3403
|
+
const offY = py - closestY;
|
|
3404
|
+
const offDist = Math.sqrt(offX * offX + offY * offY) || 1;
|
|
3405
|
+
if (offDist >= barrierRadius)
|
|
3406
|
+
return;
|
|
3407
|
+
const proximity = 1 - (offDist / barrierRadius);
|
|
3408
|
+
const mismatch = 1 - Math.max(0, Math.min(1, candidateSimilarity / Math.max(0.001, pair.similarity)));
|
|
3409
|
+
const push = repelStrength * effectiveAlpha * proximity * mismatch;
|
|
3410
|
+
if (push <= 0)
|
|
3411
|
+
return;
|
|
3412
|
+
const unitOffX = offX / offDist;
|
|
3413
|
+
const unitOffY = offY / offDist;
|
|
3414
|
+
candidate.vx = (candidate.vx ?? 0) + unitOffX * push;
|
|
3415
|
+
candidate.vy = (candidate.vy ?? 0) + unitOffY * push;
|
|
3416
|
+
const damp = push * 0.26;
|
|
3417
|
+
pair.source.vx = (pair.source.vx ?? 0) - unitOffX * damp;
|
|
3418
|
+
pair.source.vy = (pair.source.vy ?? 0) - unitOffY * damp;
|
|
3419
|
+
pair.target.vx = (pair.target.vx ?? 0) - unitOffX * damp;
|
|
3420
|
+
pair.target.vy = (pair.target.vy ?? 0) - unitOffY * damp;
|
|
3421
|
+
});
|
|
3422
|
+
});
|
|
3423
|
+
};
|
|
3424
|
+
const runObjectiveOptimizer = () => {
|
|
3425
|
+
if (!nodes.length)
|
|
3426
|
+
return;
|
|
3427
|
+
const attractionFloor = springThreshold;
|
|
3428
|
+
const dissimilarFloor = dissimilarThreshold;
|
|
3429
|
+
const pairRecords = [];
|
|
3430
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
3431
|
+
for (let j = i + 1; j < nodes.length; j++) {
|
|
3432
|
+
const similarity = Math.max(0, this.getSimilarity(nodes[i].label, nodes[j].label));
|
|
3433
|
+
if (similarity <= dissimilarFloor || similarity >= attractionFloor) {
|
|
3434
|
+
pairRecords.push({ a: nodes[i], b: nodes[j], similarity });
|
|
3435
|
+
}
|
|
3436
|
+
}
|
|
3437
|
+
}
|
|
3438
|
+
const iterations = ProfileComparisonLibComponent.OBJECTIVE_OPTIMIZER_ITERATIONS;
|
|
3439
|
+
const stepStart = ProfileComparisonLibComponent.OBJECTIVE_STEP_START;
|
|
3440
|
+
const stepEnd = ProfileComparisonLibComponent.OBJECTIVE_STEP_END;
|
|
3441
|
+
const attractionWeight = ProfileComparisonLibComponent.OBJECTIVE_ATTRACTION_WEIGHT;
|
|
3442
|
+
const repulsionWeight = ProfileComparisonLibComponent.OBJECTIVE_REPULSION_WEIGHT;
|
|
3443
|
+
const anchorWeight = ProfileComparisonLibComponent.OBJECTIVE_ANCHOR_WEIGHT;
|
|
3444
|
+
const boundaryWeight = ProfileComparisonLibComponent.OBJECTIVE_BOUNDARY_WEIGHT;
|
|
3445
|
+
for (let iteration = 0; iteration < iterations; iteration++) {
|
|
3446
|
+
const progress = iteration / Math.max(1, iterations - 1);
|
|
3447
|
+
const step = stepStart + (stepEnd - stepStart) * progress;
|
|
3448
|
+
const gradient = new Map();
|
|
3449
|
+
nodes.forEach(node => gradient.set(node.id, { x: 0, y: 0 }));
|
|
3450
|
+
pairRecords.forEach(record => {
|
|
3451
|
+
const ax = record.a.x ?? record.a.targetX ?? 0;
|
|
3452
|
+
const ay = record.a.y ?? record.a.targetY ?? 0;
|
|
3453
|
+
const bx = record.b.x ?? record.b.targetX ?? 0;
|
|
3454
|
+
const by = record.b.y ?? record.b.targetY ?? 0;
|
|
3455
|
+
const dx = bx - ax;
|
|
3456
|
+
const dy = by - ay;
|
|
3457
|
+
const dist = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
3458
|
+
const unitX = dx / dist;
|
|
3459
|
+
const unitY = dy / dist;
|
|
3460
|
+
if (record.similarity >= attractionFloor) {
|
|
3461
|
+
const normalized = Math.max(0, Math.min(1, (record.similarity - attractionFloor) / Math.max(0.001, 1 - attractionFloor)));
|
|
3462
|
+
const targetDistance = 24 + (1 - normalized) * 66;
|
|
3463
|
+
if (dist > targetDistance) {
|
|
3464
|
+
const pull = attractionWeight * Math.pow(normalized, 1.4) * ((dist - targetDistance) / dist);
|
|
3465
|
+
const gradA = gradient.get(record.a.id);
|
|
3466
|
+
const gradB = gradient.get(record.b.id);
|
|
3467
|
+
if (gradA && gradB) {
|
|
3468
|
+
gradA.x += dx * pull;
|
|
3469
|
+
gradA.y += dy * pull;
|
|
3470
|
+
gradB.x -= dx * pull;
|
|
3471
|
+
gradB.y -= dy * pull;
|
|
3472
|
+
}
|
|
3473
|
+
}
|
|
3474
|
+
return;
|
|
3475
|
+
}
|
|
3476
|
+
if (record.similarity > dissimilarFloor)
|
|
3477
|
+
return;
|
|
3478
|
+
const normalized = (dissimilarFloor - record.similarity) / Math.max(0.001, dissimilarFloor);
|
|
3479
|
+
const maxDistance = 210;
|
|
3480
|
+
if (dist >= maxDistance)
|
|
3481
|
+
return;
|
|
3482
|
+
const push = repulsionWeight * normalized * (1 - (dist / maxDistance));
|
|
3483
|
+
const gradA = gradient.get(record.a.id);
|
|
3484
|
+
const gradB = gradient.get(record.b.id);
|
|
3485
|
+
if (gradA && gradB) {
|
|
3486
|
+
gradA.x -= unitX * push;
|
|
3487
|
+
gradA.y -= unitY * push;
|
|
3488
|
+
gradB.x += unitX * push;
|
|
3489
|
+
gradB.y += unitY * push;
|
|
3490
|
+
}
|
|
3491
|
+
});
|
|
3492
|
+
nodes.forEach(node => {
|
|
3493
|
+
const grad = gradient.get(node.id);
|
|
3494
|
+
if (!grad)
|
|
3495
|
+
return;
|
|
3496
|
+
const currentX = node.x ?? node.targetX ?? zoneShared;
|
|
3497
|
+
const currentY = node.y ?? node.targetY ?? minCenterY;
|
|
3498
|
+
const targetX = node.targetX ?? currentX;
|
|
3499
|
+
const targetY = node.targetY ?? currentY;
|
|
3500
|
+
grad.x += (targetX - currentX) * anchorWeight;
|
|
3501
|
+
grad.y += (targetY - currentY) * anchorWeight;
|
|
3502
|
+
const bounds = nodeBounds.get(node.id) ?? buildNodeBounds(node);
|
|
3503
|
+
if (currentX < bounds.minX)
|
|
3504
|
+
grad.x += (bounds.minX - currentX) * boundaryWeight;
|
|
3505
|
+
if (currentX > bounds.maxX)
|
|
3506
|
+
grad.x -= (currentX - bounds.maxX) * boundaryWeight;
|
|
3507
|
+
if (currentY < bounds.minY)
|
|
3508
|
+
grad.y += (bounds.minY - currentY) * boundaryWeight;
|
|
3509
|
+
if (currentY > bounds.maxY)
|
|
3510
|
+
grad.y -= (currentY - bounds.maxY) * boundaryWeight;
|
|
3511
|
+
});
|
|
3512
|
+
nodes.forEach(node => {
|
|
3513
|
+
const grad = gradient.get(node.id);
|
|
3514
|
+
if (!grad)
|
|
3515
|
+
return;
|
|
3516
|
+
node.x = (node.x ?? node.targetX ?? zoneShared) + grad.x * step;
|
|
3517
|
+
node.y = (node.y ?? node.targetY ?? minCenterY) + grad.y * step;
|
|
3518
|
+
clampNodeToBounds(node);
|
|
3519
|
+
});
|
|
3520
|
+
}
|
|
3521
|
+
};
|
|
3522
|
+
runObjectiveOptimizer();
|
|
3023
3523
|
this.ngZone.runOutsideAngular(() => {
|
|
3024
3524
|
const sim = forceSimulation(nodes)
|
|
3025
3525
|
.alphaDecay(0.014)
|
|
@@ -3038,7 +3538,11 @@ class ProfileComparisonLibComponent {
|
|
|
3038
3538
|
.strength(d => d.group === 'shared' ? 1.15 : 0.85))
|
|
3039
3539
|
.force('yTargetSeed', forceY(d => d.targetY ?? minCenterY).strength(0.1))
|
|
3040
3540
|
.force('crossAlign', crossAlignForce)
|
|
3041
|
-
.force('
|
|
3541
|
+
.force('relativeMedianColumn', relativeMedianColumnForce)
|
|
3542
|
+
.force('similarityAttraction', similarityAttractionForce)
|
|
3543
|
+
.force('dissimilarRepulsion', dissimilarRepulsionForce)
|
|
3544
|
+
.force('priorityPair', priorityPairForce)
|
|
3545
|
+
.force('mutualTopPair', mutualTopPairForce);
|
|
3042
3546
|
for (let i = 0; i < 420 && sim.alpha() > 0.0035; i++) {
|
|
3043
3547
|
sim.tick();
|
|
3044
3548
|
nodes.forEach(n => {
|
|
@@ -3054,8 +3558,9 @@ class ProfileComparisonLibComponent {
|
|
|
3054
3558
|
sim.force('yTargetSeed', null);
|
|
3055
3559
|
sim.force('crossAlign', null);
|
|
3056
3560
|
sim.force('xTarget', forceX(d => d.originalFx ?? d.x ?? 0)
|
|
3057
|
-
.strength(d => d.group === 'shared' ? 0.
|
|
3058
|
-
sim.force('yTarget', forceY(d => d.originalFy ?? d.y ?? 0).strength(0.
|
|
3561
|
+
.strength(d => d.group === 'shared' ? 0.085 : 0.03));
|
|
3562
|
+
sim.force('yTarget', forceY(d => d.originalFy ?? d.y ?? 0).strength(0.03));
|
|
3563
|
+
sim.alphaTarget(ProfileComparisonLibComponent.IDLE_ALPHA_TARGET);
|
|
3059
3564
|
this.simulation = sim;
|
|
3060
3565
|
this.ngZone.run(() => {
|
|
3061
3566
|
this.forceNodes = [...nodes];
|
|
@@ -3493,10 +3998,14 @@ class ProfileComparisonLibComponent {
|
|
|
3493
3998
|
return;
|
|
3494
3999
|
this.simulation.force('attraction', null);
|
|
3495
4000
|
if (anchorNode) {
|
|
3496
|
-
|
|
4001
|
+
const minClickAlpha = ProfileComparisonLibComponent.CLICK_REHEAT_ALPHA;
|
|
4002
|
+
if (this.simulation.alpha() < minClickAlpha) {
|
|
4003
|
+
this.simulation.alpha(minClickAlpha);
|
|
4004
|
+
}
|
|
4005
|
+
this.simulation.alphaTarget(ProfileComparisonLibComponent.IDLE_ALPHA_TARGET).restart();
|
|
3497
4006
|
}
|
|
3498
4007
|
else if (!this.isDragging) {
|
|
3499
|
-
this.simulation.alphaTarget(
|
|
4008
|
+
this.simulation.alphaTarget(ProfileComparisonLibComponent.IDLE_ALPHA_TARGET);
|
|
3500
4009
|
}
|
|
3501
4010
|
}
|
|
3502
4011
|
isNodeConnected(node) {
|
|
@@ -3595,10 +4104,10 @@ class ProfileComparisonLibComponent {
|
|
|
3595
4104
|
collideForce.strength(1.0);
|
|
3596
4105
|
const xTarget = this.simulation.force('xTarget');
|
|
3597
4106
|
if (xTarget)
|
|
3598
|
-
xTarget.strength((d) => d.group === 'shared' ? 0.
|
|
4107
|
+
xTarget.strength((d) => d.group === 'shared' ? 0.1 : 0.045);
|
|
3599
4108
|
const yTarget = this.simulation.force('yTarget');
|
|
3600
4109
|
if (yTarget)
|
|
3601
|
-
yTarget.strength(0.
|
|
4110
|
+
yTarget.strength(0.042);
|
|
3602
4111
|
}
|
|
3603
4112
|
// Pull connected chips toward the dragged node
|
|
3604
4113
|
this.updateAttractionForce(node);
|
|
@@ -3657,14 +4166,14 @@ class ProfileComparisonLibComponent {
|
|
|
3657
4166
|
collideForce.strength(1.0);
|
|
3658
4167
|
const xTarget = this.simulation.force('xTarget');
|
|
3659
4168
|
if (xTarget)
|
|
3660
|
-
xTarget.strength((d) => d.group === 'shared' ? 0.
|
|
4169
|
+
xTarget.strength((d) => d.group === 'shared' ? 0.1 : 0.045);
|
|
3661
4170
|
const yTarget = this.simulation.force('yTarget');
|
|
3662
4171
|
if (yTarget)
|
|
3663
|
-
yTarget.strength(0.
|
|
4172
|
+
yTarget.strength(0.042);
|
|
3664
4173
|
}
|
|
3665
4174
|
// Cool down simulation targets
|
|
3666
|
-
this.simulation.alphaTarget(
|
|
3667
|
-
this.simulation.alpha(0.
|
|
4175
|
+
this.simulation.alphaTarget(ProfileComparisonLibComponent.IDLE_ALPHA_TARGET);
|
|
4176
|
+
this.simulation.alpha(0.2).restart();
|
|
3668
4177
|
this.draggedNode = null;
|
|
3669
4178
|
this.isDragging = false;
|
|
3670
4179
|
}
|