polly-graph 0.2.4 → 0.2.5
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/dist/index.cjs +2291 -894
- package/dist/index.d.cts +267 -126
- package/dist/index.d.ts +267 -126
- package/dist/index.js +2291 -894
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -517,6 +517,15 @@ var DEFAULT_NODE_INTERACTION_STYLE = {
|
|
|
517
517
|
// Hover strokeWidth from KG component
|
|
518
518
|
// Selection uses radius: 24 and strokeWidth: 4 (handled separately in interaction config)
|
|
519
519
|
};
|
|
520
|
+
var DEFAULT_NODE_HIGHLIGHT_STYLE = {
|
|
521
|
+
fill: "#fbbf24",
|
|
522
|
+
// Amber highlight color
|
|
523
|
+
stroke: "#f59e0b",
|
|
524
|
+
// Darker amber border
|
|
525
|
+
strokeWidth: 2,
|
|
526
|
+
// Highlighted border
|
|
527
|
+
opacity: 1
|
|
528
|
+
};
|
|
520
529
|
var DEFAULT_LINK_LABEL_STYLE2 = {
|
|
521
530
|
enabled: true,
|
|
522
531
|
visibility: "always",
|
|
@@ -573,8 +582,8 @@ var StyleResolver = class {
|
|
|
573
582
|
/**
|
|
574
583
|
* Generate cache key for node styles (Step 4 optimization)
|
|
575
584
|
*/
|
|
576
|
-
createNodeCacheKey(node, isHovered, isSelected) {
|
|
577
|
-
return `${node.id}_${!!node.style}_${isHovered}_${isSelected}_${this.interactionConfigHash}`;
|
|
585
|
+
createNodeCacheKey(node, isHovered, isSelected, isHighlighted = false) {
|
|
586
|
+
return `${node.id}_${!!node.style}_${isHovered}_${isSelected}_${isHighlighted}_${this.interactionConfigHash}`;
|
|
578
587
|
}
|
|
579
588
|
/**
|
|
580
589
|
* Generate cache key for link styles (Step 4 optimization)
|
|
@@ -589,13 +598,13 @@ var StyleResolver = class {
|
|
|
589
598
|
* Resolve node style using V1-compatible approach with caching (Step 4 optimization)
|
|
590
599
|
*/
|
|
591
600
|
resolveNodeStyle(params) {
|
|
592
|
-
const { node, isHovered = false, isSelected = false } = params;
|
|
593
|
-
const cacheKey = this.createNodeCacheKey(node, isHovered, isSelected);
|
|
601
|
+
const { node, isHovered = false, isSelected = false, isHighlighted = false } = params;
|
|
602
|
+
const cacheKey = this.createNodeCacheKey(node, isHovered, isSelected, isHighlighted);
|
|
594
603
|
const cached = this.nodeStyleCache.get(cacheKey);
|
|
595
604
|
if (cached) {
|
|
596
605
|
return cached;
|
|
597
606
|
}
|
|
598
|
-
if (!isHovered && !isSelected) {
|
|
607
|
+
if (!isHovered && !isSelected && !isHighlighted) {
|
|
599
608
|
const result2 = node.style ? { ...DEFAULT_NODE_STYLE, ...node.style } : DEFAULT_NODE_STYLE;
|
|
600
609
|
this.nodeStyleCache.set(cacheKey, result2);
|
|
601
610
|
return result2;
|
|
@@ -610,6 +619,12 @@ var StyleResolver = class {
|
|
|
610
619
|
this.interactionConfig?.selection?.nodeStyle
|
|
611
620
|
);
|
|
612
621
|
result = { ...result, ...interactionStyle };
|
|
622
|
+
} else if (isHighlighted) {
|
|
623
|
+
const highlightStyle = this.mergeNodeStyleSmart(
|
|
624
|
+
DEFAULT_NODE_HIGHLIGHT_STYLE,
|
|
625
|
+
this.interactionConfig?.highlight?.nodeStyle
|
|
626
|
+
);
|
|
627
|
+
result = { ...result, ...highlightStyle };
|
|
613
628
|
} else if (isHovered) {
|
|
614
629
|
const interactionStyle = this.mergeNodeStyleSmart(
|
|
615
630
|
DEFAULT_NODE_INTERACTION_STYLE,
|
|
@@ -796,89 +811,6 @@ function calculateCanvasDimensions(width, height) {
|
|
|
796
811
|
};
|
|
797
812
|
}
|
|
798
813
|
|
|
799
|
-
// src/v2/utils/z-index-manager.ts
|
|
800
|
-
var ZIndexManager = class {
|
|
801
|
-
/**
|
|
802
|
-
* Separate entities into background and foreground layers based on interaction state
|
|
803
|
-
*/
|
|
804
|
-
static separateIntoLayers(entities, isHighlighted) {
|
|
805
|
-
try {
|
|
806
|
-
const background = [];
|
|
807
|
-
const foreground = [];
|
|
808
|
-
for (const entity of entities) {
|
|
809
|
-
if (isHighlighted(entity)) {
|
|
810
|
-
foreground.push(entity);
|
|
811
|
-
} else {
|
|
812
|
-
background.push(entity);
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
return { background, foreground };
|
|
816
|
-
} catch (error) {
|
|
817
|
-
ErrorHandler.logError(error, {
|
|
818
|
-
entityCount: entities.length
|
|
819
|
-
});
|
|
820
|
-
return { background: entities, foreground: [] };
|
|
821
|
-
}
|
|
822
|
-
}
|
|
823
|
-
/**
|
|
824
|
-
* Get connected entities for a node (for bringing associated elements to front)
|
|
825
|
-
*/
|
|
826
|
-
static getConnectedEntities(node, allLinks) {
|
|
827
|
-
try {
|
|
828
|
-
const connectedLinks = [];
|
|
829
|
-
const linkIds = /* @__PURE__ */ new Set();
|
|
830
|
-
for (const link of allLinks) {
|
|
831
|
-
const sourceId = typeof link.source === "string" ? link.source : link.source.id;
|
|
832
|
-
const targetId = typeof link.target === "string" ? link.target : link.target.id;
|
|
833
|
-
if (sourceId === node.id || targetId === node.id) {
|
|
834
|
-
connectedLinks.push(link);
|
|
835
|
-
linkIds.add(`${sourceId}->${targetId}`);
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
return { connectedLinks, linkIds };
|
|
839
|
-
} catch (error) {
|
|
840
|
-
ErrorHandler.logError(error, {
|
|
841
|
-
nodeId: node.id
|
|
842
|
-
});
|
|
843
|
-
return { connectedLinks: [], linkIds: /* @__PURE__ */ new Set() };
|
|
844
|
-
}
|
|
845
|
-
}
|
|
846
|
-
/**
|
|
847
|
-
* Create highlight checker for nodes (node is highlighted if hovered or selected)
|
|
848
|
-
*/
|
|
849
|
-
static createNodeHighlightChecker(hoveredNodeId, selectedNodeId) {
|
|
850
|
-
return (node) => {
|
|
851
|
-
return node.id === hoveredNodeId || node.id === selectedNodeId;
|
|
852
|
-
};
|
|
853
|
-
}
|
|
854
|
-
/**
|
|
855
|
-
* Create highlight checker for links (link is highlighted if hovered, selected, or connected to highlighted node)
|
|
856
|
-
*/
|
|
857
|
-
static createLinkHighlightChecker(hoveredNodeId, selectedNodeId, hoveredLinkId, selectedLinkId) {
|
|
858
|
-
return (link) => {
|
|
859
|
-
const sourceId = typeof link.source === "string" ? link.source : link.source.id;
|
|
860
|
-
const targetId = typeof link.target === "string" ? link.target : link.target.id;
|
|
861
|
-
const linkId = `${sourceId}->${targetId}`;
|
|
862
|
-
if (linkId === hoveredLinkId || linkId === selectedLinkId) {
|
|
863
|
-
return true;
|
|
864
|
-
}
|
|
865
|
-
if (hoveredNodeId && (sourceId === hoveredNodeId || targetId === hoveredNodeId)) {
|
|
866
|
-
return true;
|
|
867
|
-
}
|
|
868
|
-
if (selectedNodeId && (sourceId === selectedNodeId || targetId === selectedNodeId)) {
|
|
869
|
-
return true;
|
|
870
|
-
}
|
|
871
|
-
return false;
|
|
872
|
-
};
|
|
873
|
-
}
|
|
874
|
-
/**
|
|
875
|
-
* Create highlight checker for labels (same logic as their parent entities)
|
|
876
|
-
*/
|
|
877
|
-
static createLabelHighlightChecker(entityHighlightChecker) {
|
|
878
|
-
return entityHighlightChecker;
|
|
879
|
-
}
|
|
880
|
-
};
|
|
881
|
-
|
|
882
814
|
// src/v2/utils/timer-manager.ts
|
|
883
815
|
var TimerManager = class {
|
|
884
816
|
activeTimers = /* @__PURE__ */ new Map();
|
|
@@ -2054,6 +1986,266 @@ function manyBody_default() {
|
|
|
2054
1986
|
return force;
|
|
2055
1987
|
}
|
|
2056
1988
|
|
|
1989
|
+
// src/v2/core/state-manager.ts
|
|
1990
|
+
var StateManager = class {
|
|
1991
|
+
// Core entity maps for O(1) lookups
|
|
1992
|
+
nodeMap = /* @__PURE__ */ new Map();
|
|
1993
|
+
linkMap = /* @__PURE__ */ new Map();
|
|
1994
|
+
// Using linkId as key
|
|
1995
|
+
// State cache maps for performance
|
|
1996
|
+
nodeStateCache = /* @__PURE__ */ new Map();
|
|
1997
|
+
linkStateCache = /* @__PURE__ */ new Map();
|
|
1998
|
+
// Link ID to Link mapping for efficient lookups
|
|
1999
|
+
linkIdToLinkMap = /* @__PURE__ */ new Map();
|
|
2000
|
+
// Highlight state tracking
|
|
2001
|
+
highlightedNodes = /* @__PURE__ */ new Set();
|
|
2002
|
+
/**
|
|
2003
|
+
* Initialize with graph data
|
|
2004
|
+
*/
|
|
2005
|
+
initialize(state) {
|
|
2006
|
+
try {
|
|
2007
|
+
this.buildNodeMap(state.nodes);
|
|
2008
|
+
this.buildLinkMaps(state.links);
|
|
2009
|
+
this.clearStateCache();
|
|
2010
|
+
} catch (error) {
|
|
2011
|
+
ErrorHandler.logError(error);
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
/**
|
|
2015
|
+
* Update with new graph data
|
|
2016
|
+
*/
|
|
2017
|
+
updateState(state) {
|
|
2018
|
+
try {
|
|
2019
|
+
this.nodeMap.clear();
|
|
2020
|
+
this.linkMap.clear();
|
|
2021
|
+
this.linkIdToLinkMap.clear();
|
|
2022
|
+
this.buildNodeMap(state.nodes);
|
|
2023
|
+
this.buildLinkMaps(state.links);
|
|
2024
|
+
this.clearStateCache();
|
|
2025
|
+
} catch (error) {
|
|
2026
|
+
ErrorHandler.logError(error);
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
/**
|
|
2030
|
+
* Build node map for O(1) node lookups
|
|
2031
|
+
*/
|
|
2032
|
+
buildNodeMap(nodes) {
|
|
2033
|
+
for (const node of nodes) {
|
|
2034
|
+
this.nodeMap.set(node.id, node);
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
/**
|
|
2038
|
+
* Build link maps for O(1) link lookups
|
|
2039
|
+
*/
|
|
2040
|
+
buildLinkMaps(links) {
|
|
2041
|
+
for (const link of links) {
|
|
2042
|
+
const linkId = this.getLinkId(link);
|
|
2043
|
+
this.linkMap.set(linkId, link);
|
|
2044
|
+
this.linkIdToLinkMap.set(linkId, link);
|
|
2045
|
+
if (typeof link.source === "string") {
|
|
2046
|
+
const sourceNode = this.nodeMap.get(link.source);
|
|
2047
|
+
if (sourceNode) {
|
|
2048
|
+
link.source = sourceNode;
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
if (typeof link.target === "string") {
|
|
2052
|
+
const targetNode = this.nodeMap.get(link.target);
|
|
2053
|
+
if (targetNode) {
|
|
2054
|
+
link.target = targetNode;
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
/**
|
|
2060
|
+
* Get node by ID (O(1) lookup)
|
|
2061
|
+
*/
|
|
2062
|
+
getNode(nodeId) {
|
|
2063
|
+
return this.nodeMap.get(nodeId);
|
|
2064
|
+
}
|
|
2065
|
+
/**
|
|
2066
|
+
* Get link by link ID (O(1) lookup)
|
|
2067
|
+
*/
|
|
2068
|
+
getLink(linkId) {
|
|
2069
|
+
return this.linkMap.get(linkId);
|
|
2070
|
+
}
|
|
2071
|
+
/**
|
|
2072
|
+
* Get link by source/target IDs (O(1) lookup)
|
|
2073
|
+
*/
|
|
2074
|
+
getLinkByNodes(sourceId, targetId) {
|
|
2075
|
+
const linkId = `${sourceId}->${targetId}`;
|
|
2076
|
+
return this.linkMap.get(linkId);
|
|
2077
|
+
}
|
|
2078
|
+
/**
|
|
2079
|
+
* Get all nodes (returns reference to avoid copying)
|
|
2080
|
+
*/
|
|
2081
|
+
getAllNodes() {
|
|
2082
|
+
return Array.from(this.nodeMap.values());
|
|
2083
|
+
}
|
|
2084
|
+
/**
|
|
2085
|
+
* Get all links (returns reference to avoid copying)
|
|
2086
|
+
*/
|
|
2087
|
+
getAllLinks() {
|
|
2088
|
+
return Array.from(this.linkMap.values());
|
|
2089
|
+
}
|
|
2090
|
+
/**
|
|
2091
|
+
* Get node map (for renderers that need direct map access)
|
|
2092
|
+
*/
|
|
2093
|
+
getNodeMap() {
|
|
2094
|
+
return this.nodeMap;
|
|
2095
|
+
}
|
|
2096
|
+
/**
|
|
2097
|
+
* Get link ID to link map (for renderers that need direct map access)
|
|
2098
|
+
*/
|
|
2099
|
+
getLinkIdToLinkMap() {
|
|
2100
|
+
return this.linkIdToLinkMap;
|
|
2101
|
+
}
|
|
2102
|
+
/**
|
|
2103
|
+
* Cache node state for performance (avoid repeated hover/selection checks)
|
|
2104
|
+
*/
|
|
2105
|
+
cacheNodeState(nodeId, state) {
|
|
2106
|
+
this.nodeStateCache.set(nodeId, state);
|
|
2107
|
+
}
|
|
2108
|
+
/**
|
|
2109
|
+
* Get cached node state
|
|
2110
|
+
*/
|
|
2111
|
+
getCachedNodeState(nodeId) {
|
|
2112
|
+
return this.nodeStateCache.get(nodeId);
|
|
2113
|
+
}
|
|
2114
|
+
/**
|
|
2115
|
+
* Cache link state for performance (avoid repeated hover/selection checks)
|
|
2116
|
+
*/
|
|
2117
|
+
cacheLinkState(linkId, state) {
|
|
2118
|
+
this.linkStateCache.set(linkId, state);
|
|
2119
|
+
}
|
|
2120
|
+
/**
|
|
2121
|
+
* Get cached link state
|
|
2122
|
+
*/
|
|
2123
|
+
getCachedLinkState(linkId) {
|
|
2124
|
+
return this.linkStateCache.get(linkId);
|
|
2125
|
+
}
|
|
2126
|
+
/**
|
|
2127
|
+
* Clear state caches (call when hover/selection state changes)
|
|
2128
|
+
*/
|
|
2129
|
+
clearStateCache() {
|
|
2130
|
+
this.nodeStateCache.clear();
|
|
2131
|
+
this.linkStateCache.clear();
|
|
2132
|
+
}
|
|
2133
|
+
/**
|
|
2134
|
+
* Clear only node state cache
|
|
2135
|
+
*/
|
|
2136
|
+
clearNodeStateCache() {
|
|
2137
|
+
this.nodeStateCache.clear();
|
|
2138
|
+
}
|
|
2139
|
+
/**
|
|
2140
|
+
* Clear only link state cache
|
|
2141
|
+
*/
|
|
2142
|
+
clearLinkStateCache() {
|
|
2143
|
+
this.linkStateCache.clear();
|
|
2144
|
+
}
|
|
2145
|
+
/**
|
|
2146
|
+
* Generate consistent link ID
|
|
2147
|
+
*/
|
|
2148
|
+
getLinkId(link) {
|
|
2149
|
+
const sourceId = typeof link.source === "string" ? link.source : link.source.id;
|
|
2150
|
+
const targetId = typeof link.target === "string" ? link.target : link.target.id;
|
|
2151
|
+
return `${sourceId}->${targetId}`;
|
|
2152
|
+
}
|
|
2153
|
+
/**
|
|
2154
|
+
* Calculate link midpoint (common utility)
|
|
2155
|
+
*/
|
|
2156
|
+
getLinkMidpoint(link) {
|
|
2157
|
+
const sourceNode = typeof link.source === "string" ? this.nodeMap.get(link.source) : link.source;
|
|
2158
|
+
const targetNode = typeof link.target === "string" ? this.nodeMap.get(link.target) : link.target;
|
|
2159
|
+
if (!sourceNode || !targetNode || sourceNode.x === void 0 || sourceNode.y === void 0 || targetNode.x === void 0 || targetNode.y === void 0) {
|
|
2160
|
+
return null;
|
|
2161
|
+
}
|
|
2162
|
+
return {
|
|
2163
|
+
x: (sourceNode.x + targetNode.x) / 2,
|
|
2164
|
+
y: (sourceNode.y + targetNode.y) / 2
|
|
2165
|
+
};
|
|
2166
|
+
}
|
|
2167
|
+
/**
|
|
2168
|
+
* Highlight a node by ID
|
|
2169
|
+
*/
|
|
2170
|
+
highlightNode(nodeId) {
|
|
2171
|
+
if (this.nodeMap.has(nodeId)) {
|
|
2172
|
+
this.highlightedNodes.add(nodeId);
|
|
2173
|
+
this.clearSingleNodeStateCache(nodeId);
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
/**
|
|
2177
|
+
* Highlight multiple nodes by IDs
|
|
2178
|
+
*/
|
|
2179
|
+
highlightNodes(nodeIds) {
|
|
2180
|
+
for (const nodeId of nodeIds) {
|
|
2181
|
+
if (this.nodeMap.has(nodeId)) {
|
|
2182
|
+
this.highlightedNodes.add(nodeId);
|
|
2183
|
+
this.clearSingleNodeStateCache(nodeId);
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
/**
|
|
2188
|
+
* Remove highlight from a node
|
|
2189
|
+
*/
|
|
2190
|
+
unhighlightNode(nodeId) {
|
|
2191
|
+
if (this.highlightedNodes.has(nodeId)) {
|
|
2192
|
+
this.highlightedNodes.delete(nodeId);
|
|
2193
|
+
this.clearSingleNodeStateCache(nodeId);
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
/**
|
|
2197
|
+
* Clear all node highlights
|
|
2198
|
+
*/
|
|
2199
|
+
clearHighlights() {
|
|
2200
|
+
const highlightedIds = Array.from(this.highlightedNodes);
|
|
2201
|
+
this.highlightedNodes.clear();
|
|
2202
|
+
for (const nodeId of highlightedIds) {
|
|
2203
|
+
this.clearSingleNodeStateCache(nodeId);
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
/**
|
|
2207
|
+
* Check if a node is highlighted
|
|
2208
|
+
*/
|
|
2209
|
+
isNodeHighlighted(nodeId) {
|
|
2210
|
+
return this.highlightedNodes.has(nodeId);
|
|
2211
|
+
}
|
|
2212
|
+
/**
|
|
2213
|
+
* Get all highlighted node IDs
|
|
2214
|
+
*/
|
|
2215
|
+
getHighlightedNodes() {
|
|
2216
|
+
return new Set(this.highlightedNodes);
|
|
2217
|
+
}
|
|
2218
|
+
/**
|
|
2219
|
+
* Clear state cache for a specific node
|
|
2220
|
+
*/
|
|
2221
|
+
clearSingleNodeStateCache(nodeId) {
|
|
2222
|
+
this.nodeStateCache.delete(nodeId);
|
|
2223
|
+
}
|
|
2224
|
+
/**
|
|
2225
|
+
* Get statistics about cached state
|
|
2226
|
+
*/
|
|
2227
|
+
getStats() {
|
|
2228
|
+
return {
|
|
2229
|
+
nodeCount: this.nodeMap.size,
|
|
2230
|
+
linkCount: this.linkMap.size,
|
|
2231
|
+
cachedNodeStates: this.nodeStateCache.size,
|
|
2232
|
+
cachedLinkStates: this.linkStateCache.size,
|
|
2233
|
+
highlightedNodes: this.highlightedNodes.size
|
|
2234
|
+
};
|
|
2235
|
+
}
|
|
2236
|
+
/**
|
|
2237
|
+
* Destroy and clean up all maps
|
|
2238
|
+
*/
|
|
2239
|
+
destroy() {
|
|
2240
|
+
this.nodeMap.clear();
|
|
2241
|
+
this.linkMap.clear();
|
|
2242
|
+
this.linkIdToLinkMap.clear();
|
|
2243
|
+
this.nodeStateCache.clear();
|
|
2244
|
+
this.linkStateCache.clear();
|
|
2245
|
+
this.highlightedNodes.clear();
|
|
2246
|
+
}
|
|
2247
|
+
};
|
|
2248
|
+
|
|
2057
2249
|
// src/v2/core/physics-manager.ts
|
|
2058
2250
|
var PhysicsManager = class {
|
|
2059
2251
|
simulation;
|
|
@@ -2063,7 +2255,9 @@ var PhysicsManager = class {
|
|
|
2063
2255
|
hasInitialAutoFitCompleted = false;
|
|
2064
2256
|
timerManager;
|
|
2065
2257
|
isVisibilityListenerAttached = false;
|
|
2066
|
-
|
|
2258
|
+
stateManager = new StateManager();
|
|
2259
|
+
isWarmingUp = false;
|
|
2260
|
+
warmupSteps = 0;
|
|
2067
2261
|
constructor(timerManager) {
|
|
2068
2262
|
this.timerManager = timerManager;
|
|
2069
2263
|
}
|
|
@@ -2094,19 +2288,19 @@ var PhysicsManager = class {
|
|
|
2094
2288
|
if (this.simulation) {
|
|
2095
2289
|
this.simulation.stop();
|
|
2096
2290
|
}
|
|
2097
|
-
this.
|
|
2098
|
-
const linkDistance = nodeCount > 1e4 ?
|
|
2099
|
-
const
|
|
2100
|
-
const
|
|
2101
|
-
const
|
|
2102
|
-
const
|
|
2103
|
-
const
|
|
2291
|
+
this.stateManager.initialize({ nodes: config.nodes, links: config.links });
|
|
2292
|
+
const linkDistance = nodeCount > 1e4 ? 400 : nodeCount > 6e3 ? 350 : nodeCount > 2e3 ? 300 : 250;
|
|
2293
|
+
const linkStrength = nodeCount > 1e4 ? 0.08 : nodeCount > 6e3 ? 0.12 : nodeCount > 2e3 ? 0.2 : 0.4;
|
|
2294
|
+
const chargeStrength = nodeCount > 1e4 ? -800 : nodeCount > 6e3 ? -700 : nodeCount > 2e3 ? -600 : -500;
|
|
2295
|
+
const collisionRadius = nodeCount > 1e4 ? 5 : nodeCount > 6e3 ? 4 : 3;
|
|
2296
|
+
const collisionIterations = nodeCount > 1e4 ? 2 : nodeCount > 6e3 ? 2 : 2;
|
|
2297
|
+
const centerStrength = nodeCount > 6e3 ? 0.05 : 0.3;
|
|
2104
2298
|
this.simulation = simulation_default(config.nodes).force(
|
|
2105
2299
|
"link",
|
|
2106
2300
|
link_default(config.links).id((d) => d.id).distance(linkDistance).strength(linkStrength).iterations(1)
|
|
2107
2301
|
).force(
|
|
2108
2302
|
"charge",
|
|
2109
|
-
manyBody_default().strength(chargeStrength).theta(nodeCount >
|
|
2303
|
+
manyBody_default().strength(chargeStrength).theta(nodeCount > 6e3 ? 0.8 : 0.9).distanceMax(nodeCount > 1e4 ? 800 : nodeCount > 6e3 ? 700 : 1e3)
|
|
2110
2304
|
).force(
|
|
2111
2305
|
"collision",
|
|
2112
2306
|
collide_default().radius((node) => (node.style?.radius ?? 20) + collisionRadius).strength(1).iterations(collisionIterations)
|
|
@@ -2114,12 +2308,15 @@ var PhysicsManager = class {
|
|
|
2114
2308
|
"center",
|
|
2115
2309
|
center_default(0, 0).strength(centerStrength)
|
|
2116
2310
|
).velocityDecay(
|
|
2117
|
-
nodeCount >
|
|
2311
|
+
nodeCount > 1e4 ? 0.4 : nodeCount > 6e3 ? 0.5 : adaptiveVelocityDecay
|
|
2118
2312
|
).alphaDecay(
|
|
2119
|
-
nodeCount >
|
|
2313
|
+
nodeCount > 1e4 ? 0.01 : nodeCount > 6e3 ? 0.02 : adaptiveAlphaDecay
|
|
2120
2314
|
).alphaMin(
|
|
2121
|
-
nodeCount >
|
|
2122
|
-
).on("tick", config.onTick).on("end", () => this.handleSimulationEnd());
|
|
2315
|
+
nodeCount > 1e4 ? 0.01 : nodeCount > 6e3 ? 0.02 : 1e-3
|
|
2316
|
+
).on("tick", () => this.handleTick(config.onTick)).on("end", () => this.handleSimulationEnd());
|
|
2317
|
+
if (nodeCount > 1e3) {
|
|
2318
|
+
this.startSmoothWarmup();
|
|
2319
|
+
}
|
|
2123
2320
|
if (config.cooldownTime) {
|
|
2124
2321
|
this.setupCooldownTimer(config.cooldownTime);
|
|
2125
2322
|
}
|
|
@@ -2132,12 +2329,64 @@ var PhysicsManager = class {
|
|
|
2132
2329
|
throw error;
|
|
2133
2330
|
}
|
|
2134
2331
|
}
|
|
2332
|
+
/**
|
|
2333
|
+
* Handle tick with smooth warmup progression
|
|
2334
|
+
*/
|
|
2335
|
+
handleTick(originalOnTick) {
|
|
2336
|
+
if (this.isWarmingUp) {
|
|
2337
|
+
this.progressWarmup();
|
|
2338
|
+
}
|
|
2339
|
+
originalOnTick();
|
|
2340
|
+
}
|
|
2341
|
+
/**
|
|
2342
|
+
* Start smooth warmup process for large graphs
|
|
2343
|
+
*/
|
|
2344
|
+
startSmoothWarmup() {
|
|
2345
|
+
if (!this.simulation || !this.config) return;
|
|
2346
|
+
this.isWarmingUp = true;
|
|
2347
|
+
this.warmupSteps = 0;
|
|
2348
|
+
this.applyWarmupForces(0.2);
|
|
2349
|
+
}
|
|
2350
|
+
/**
|
|
2351
|
+
* Progress warmup over time for smoother initial animation
|
|
2352
|
+
*/
|
|
2353
|
+
progressWarmup() {
|
|
2354
|
+
if (!this.simulation || !this.config) return;
|
|
2355
|
+
this.warmupSteps++;
|
|
2356
|
+
const maxWarmupSteps = 60;
|
|
2357
|
+
const progress = Math.min(this.warmupSteps / maxWarmupSteps, 1);
|
|
2358
|
+
if (progress >= 1) {
|
|
2359
|
+
this.isWarmingUp = false;
|
|
2360
|
+
this.applyWarmupForces(1);
|
|
2361
|
+
} else {
|
|
2362
|
+
const forceMultiplier = 0.2 + progress * 0.8;
|
|
2363
|
+
this.applyWarmupForces(forceMultiplier);
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
/**
|
|
2367
|
+
* Apply scaled forces during warmup
|
|
2368
|
+
*/
|
|
2369
|
+
applyWarmupForces(multiplier) {
|
|
2370
|
+
if (!this.simulation || !this.config) return;
|
|
2371
|
+
const nodeCount = this.config.nodes.length;
|
|
2372
|
+
const linkStrength = (nodeCount > 1e4 ? 0.08 : nodeCount > 6e3 ? 0.12 : nodeCount > 2e3 ? 0.2 : 0.4) * multiplier;
|
|
2373
|
+
const chargeStrength = (nodeCount > 1e4 ? -800 : nodeCount > 6e3 ? -700 : nodeCount > 2e3 ? -600 : -500) * multiplier;
|
|
2374
|
+
const linkForce = this.simulation.force("link");
|
|
2375
|
+
const chargeForce = this.simulation.force("charge");
|
|
2376
|
+
if (linkForce) {
|
|
2377
|
+
linkForce.strength(linkStrength);
|
|
2378
|
+
}
|
|
2379
|
+
if (chargeForce) {
|
|
2380
|
+
chargeForce.strength(chargeStrength);
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2135
2383
|
/**
|
|
2136
2384
|
* Handle simulation end
|
|
2137
2385
|
*/
|
|
2138
2386
|
handleSimulationEnd() {
|
|
2139
2387
|
try {
|
|
2140
2388
|
this.simulationEndTime = performance.now();
|
|
2389
|
+
this.isWarmingUp = false;
|
|
2141
2390
|
if (this.config?.onEnd) {
|
|
2142
2391
|
this.config.onEnd();
|
|
2143
2392
|
}
|
|
@@ -2172,34 +2421,6 @@ var PhysicsManager = class {
|
|
|
2172
2421
|
ErrorHandler.logError(error, { cooldownTime });
|
|
2173
2422
|
}
|
|
2174
2423
|
}
|
|
2175
|
-
/**
|
|
2176
|
-
* Build node index for O(1) lookups (Step 3 optimization)
|
|
2177
|
-
*/
|
|
2178
|
-
buildNodeIndex() {
|
|
2179
|
-
if (!this.config) return;
|
|
2180
|
-
try {
|
|
2181
|
-
this.nodeMap.clear();
|
|
2182
|
-
for (const node of this.config.nodes) {
|
|
2183
|
-
this.nodeMap.set(node.id, node);
|
|
2184
|
-
}
|
|
2185
|
-
for (const link of this.config.links) {
|
|
2186
|
-
if (typeof link.source === "string") {
|
|
2187
|
-
const sourceNode = this.nodeMap.get(link.source);
|
|
2188
|
-
if (sourceNode) {
|
|
2189
|
-
link.source = sourceNode;
|
|
2190
|
-
}
|
|
2191
|
-
}
|
|
2192
|
-
if (typeof link.target === "string") {
|
|
2193
|
-
const targetNode = this.nodeMap.get(link.target);
|
|
2194
|
-
if (targetNode) {
|
|
2195
|
-
link.target = targetNode;
|
|
2196
|
-
}
|
|
2197
|
-
}
|
|
2198
|
-
}
|
|
2199
|
-
} catch (error) {
|
|
2200
|
-
ErrorHandler.logError(error);
|
|
2201
|
-
}
|
|
2202
|
-
}
|
|
2203
2424
|
/**
|
|
2204
2425
|
* Get simulation instance
|
|
2205
2426
|
*/
|
|
@@ -2217,7 +2438,7 @@ var PhysicsManager = class {
|
|
|
2217
2438
|
return;
|
|
2218
2439
|
}
|
|
2219
2440
|
const nodeCount = this.config.nodes.length;
|
|
2220
|
-
const effectiveAlpha = alphaTarget ?? (nodeCount > 1e4 ?
|
|
2441
|
+
const effectiveAlpha = alphaTarget ?? (nodeCount > 1e4 ? 0.03 : nodeCount > 5e3 ? 0.05 : nodeCount > 2e3 ? 0.08 : 0.15);
|
|
2221
2442
|
try {
|
|
2222
2443
|
this.simulation.alphaTarget(effectiveAlpha).restart();
|
|
2223
2444
|
if (this.config.cooldownTime) {
|
|
@@ -2294,8 +2515,8 @@ var PhysicsManager = class {
|
|
|
2294
2515
|
const linkForce = this.simulation.force("link");
|
|
2295
2516
|
if (linkForce) {
|
|
2296
2517
|
linkForce.distance((link) => {
|
|
2297
|
-
const sourceNode = typeof link.source === "string" ? this.
|
|
2298
|
-
const targetNode = typeof link.target === "string" ? this.
|
|
2518
|
+
const sourceNode = typeof link.source === "string" ? this.stateManager.getNode(link.source) : link.source;
|
|
2519
|
+
const targetNode = typeof link.target === "string" ? this.stateManager.getNode(link.target) : link.target;
|
|
2299
2520
|
const sourceRadius = sourceNode?.style?.radius ?? 20;
|
|
2300
2521
|
const targetRadius = targetNode?.style?.radius ?? 20;
|
|
2301
2522
|
const arrowLength = this.getLinkArrowLength(link);
|
|
@@ -2313,11 +2534,13 @@ var PhysicsManager = class {
|
|
|
2313
2534
|
* Calculate base distance based on graph size and node count
|
|
2314
2535
|
*/
|
|
2315
2536
|
calculateBaseDistance() {
|
|
2316
|
-
if (!this.config) return
|
|
2537
|
+
if (!this.config) return 150;
|
|
2317
2538
|
const nodeCount = this.config.nodes.length;
|
|
2318
2539
|
const graphArea = Math.max(this.config.width * this.config.height, 1);
|
|
2319
2540
|
const nodeAreaRatio = nodeCount / (graphArea / 1e4);
|
|
2320
|
-
|
|
2541
|
+
const baseDistance = nodeCount > 1e4 ? 300 : nodeCount > 6e3 ? 250 : nodeCount > 2e3 ? 200 : 150;
|
|
2542
|
+
const areaAdjustment = Math.min(100, nodeAreaRatio * 30);
|
|
2543
|
+
return Math.max(baseDistance, baseDistance + areaAdjustment);
|
|
2321
2544
|
}
|
|
2322
2545
|
/**
|
|
2323
2546
|
* Get arrow length from link style or default
|
|
@@ -2329,15 +2552,38 @@ var PhysicsManager = class {
|
|
|
2329
2552
|
return linkStyle?.arrow?.size ?? 8;
|
|
2330
2553
|
}
|
|
2331
2554
|
/**
|
|
2332
|
-
* Initialize node positions
|
|
2555
|
+
* Initialize node positions with improved distribution for smoother startup
|
|
2333
2556
|
*/
|
|
2334
2557
|
initializePositions() {
|
|
2335
2558
|
if (!this.config) return;
|
|
2336
2559
|
try {
|
|
2337
|
-
|
|
2560
|
+
const nodeCount = this.config.nodes.length;
|
|
2561
|
+
const centerX = this.config.width / 2;
|
|
2562
|
+
const centerY = this.config.height / 2;
|
|
2563
|
+
const radius = Math.min(this.config.width, this.config.height) * 0.3;
|
|
2564
|
+
for (let i = 0; i < this.config.nodes.length; i++) {
|
|
2565
|
+
const node = this.config.nodes[i];
|
|
2566
|
+
if (!node) continue;
|
|
2338
2567
|
if (node.x == null || node.y == null) {
|
|
2339
|
-
|
|
2340
|
-
|
|
2568
|
+
if (nodeCount === 1) {
|
|
2569
|
+
node.x = centerX;
|
|
2570
|
+
node.y = centerY;
|
|
2571
|
+
} else if (nodeCount <= 10) {
|
|
2572
|
+
const angle = i / nodeCount * 2 * Math.PI;
|
|
2573
|
+
node.x = centerX + Math.cos(angle) * radius * 0.5;
|
|
2574
|
+
node.y = centerY + Math.sin(angle) * radius * 0.5;
|
|
2575
|
+
} else if (nodeCount <= 100) {
|
|
2576
|
+
const angle = i * 0.5;
|
|
2577
|
+
const spiralRadius = i / nodeCount * radius;
|
|
2578
|
+
node.x = centerX + Math.cos(angle) * spiralRadius;
|
|
2579
|
+
node.y = centerY + Math.sin(angle) * spiralRadius;
|
|
2580
|
+
} else {
|
|
2581
|
+
const clusterRadius = radius * 0.6;
|
|
2582
|
+
const angle = Math.random() * 2 * Math.PI;
|
|
2583
|
+
const distance = Math.random() * clusterRadius;
|
|
2584
|
+
node.x = centerX + Math.cos(angle) * distance;
|
|
2585
|
+
node.y = centerY + Math.sin(angle) * distance;
|
|
2586
|
+
}
|
|
2341
2587
|
}
|
|
2342
2588
|
}
|
|
2343
2589
|
} catch (error) {
|
|
@@ -2397,7 +2643,7 @@ var PhysicsManager = class {
|
|
|
2397
2643
|
return;
|
|
2398
2644
|
}
|
|
2399
2645
|
const nodeCount = this.config?.nodes.length ?? 0;
|
|
2400
|
-
const alpha = nodeCount > 1e4 ? 0.
|
|
2646
|
+
const alpha = nodeCount > 1e4 ? 0.05 : nodeCount > 5e3 ? 0.08 : nodeCount > 2e3 ? 0.12 : 0.3;
|
|
2401
2647
|
this.simulation.alpha(alpha).alphaTarget(0).restart();
|
|
2402
2648
|
if (this.config?.cooldownTime) {
|
|
2403
2649
|
this.setupCooldownTimer(this.config.cooldownTime);
|
|
@@ -2408,6 +2654,9 @@ var PhysicsManager = class {
|
|
|
2408
2654
|
this.pause();
|
|
2409
2655
|
} else {
|
|
2410
2656
|
this.resume();
|
|
2657
|
+
if (this.hasInitialAutoFitCompleted && this.simulation && this.simulation.alpha() > 0) {
|
|
2658
|
+
this.hasInitialAutoFitCompleted = false;
|
|
2659
|
+
}
|
|
2411
2660
|
}
|
|
2412
2661
|
};
|
|
2413
2662
|
/**
|
|
@@ -2425,10 +2674,12 @@ var PhysicsManager = class {
|
|
|
2425
2674
|
this.simulation = void 0;
|
|
2426
2675
|
}
|
|
2427
2676
|
this.config = void 0;
|
|
2428
|
-
this.
|
|
2677
|
+
this.stateManager.destroy();
|
|
2429
2678
|
this.simulationStartTime = void 0;
|
|
2430
2679
|
this.simulationEndTime = void 0;
|
|
2431
2680
|
this.hasInitialAutoFitCompleted = false;
|
|
2681
|
+
this.isWarmingUp = false;
|
|
2682
|
+
this.warmupSteps = 0;
|
|
2432
2683
|
} catch (error) {
|
|
2433
2684
|
ErrorHandler.logError(error);
|
|
2434
2685
|
}
|
|
@@ -5214,6 +5465,10 @@ var DragManager = class {
|
|
|
5214
5465
|
};
|
|
5215
5466
|
DRAG_CLICK_TOLERANCE_PX = 3;
|
|
5216
5467
|
// Force-graph constant
|
|
5468
|
+
// RAF throttling for smooth drag performance
|
|
5469
|
+
dragRenderPending = false;
|
|
5470
|
+
lastDragRenderTime = 0;
|
|
5471
|
+
pendingAnimationFrame;
|
|
5217
5472
|
/**
|
|
5218
5473
|
* Initialize drag behavior
|
|
5219
5474
|
*/
|
|
@@ -5263,6 +5518,10 @@ var DragManager = class {
|
|
|
5263
5518
|
obj.fy = obj.y;
|
|
5264
5519
|
}
|
|
5265
5520
|
this.config.canvas.classList.add("grabbable");
|
|
5521
|
+
if (this.config.renderer) {
|
|
5522
|
+
this.config.renderer.setDraggedNode(obj);
|
|
5523
|
+
this.config.renderer.setDragState(true);
|
|
5524
|
+
}
|
|
5266
5525
|
} catch (error) {
|
|
5267
5526
|
ErrorHandler.logError(error, {
|
|
5268
5527
|
nodeId: obj?.id,
|
|
@@ -5295,7 +5554,7 @@ var DragManager = class {
|
|
|
5295
5554
|
this.state.isDragging = true;
|
|
5296
5555
|
this.state.isPointerDragging = true;
|
|
5297
5556
|
obj.__dragged = true;
|
|
5298
|
-
this.
|
|
5557
|
+
this.throttledDragRender();
|
|
5299
5558
|
} catch (error) {
|
|
5300
5559
|
ErrorHandler.logError(error, {
|
|
5301
5560
|
nodeId: obj?.id,
|
|
@@ -5303,6 +5562,24 @@ var DragManager = class {
|
|
|
5303
5562
|
});
|
|
5304
5563
|
}
|
|
5305
5564
|
}
|
|
5565
|
+
/**
|
|
5566
|
+
* RAF-throttled render during drag (like ZoomManager pattern)
|
|
5567
|
+
*/
|
|
5568
|
+
throttledDragRender() {
|
|
5569
|
+
if (!this.config) return;
|
|
5570
|
+
const now2 = performance.now();
|
|
5571
|
+
if (now2 - this.lastDragRenderTime < 16) return;
|
|
5572
|
+
if (this.dragRenderPending) return;
|
|
5573
|
+
this.dragRenderPending = true;
|
|
5574
|
+
this.pendingAnimationFrame = requestAnimationFrame(() => {
|
|
5575
|
+
this.pendingAnimationFrame = void 0;
|
|
5576
|
+
this.dragRenderPending = false;
|
|
5577
|
+
this.lastDragRenderTime = performance.now();
|
|
5578
|
+
if (this.config && this.state.isPointerDragging) {
|
|
5579
|
+
this.config.onRender();
|
|
5580
|
+
}
|
|
5581
|
+
});
|
|
5582
|
+
}
|
|
5306
5583
|
/**
|
|
5307
5584
|
* Handle drag end
|
|
5308
5585
|
*/
|
|
@@ -5327,6 +5604,9 @@ var DragManager = class {
|
|
|
5327
5604
|
this.config.canvas.classList.remove("grabbable");
|
|
5328
5605
|
this.state.isDragging = false;
|
|
5329
5606
|
this.state.isPointerDragging = false;
|
|
5607
|
+
if (this.config.renderer) {
|
|
5608
|
+
this.config.renderer.setDragState(false);
|
|
5609
|
+
}
|
|
5330
5610
|
if (obj.__dragged) {
|
|
5331
5611
|
delete obj.__dragged;
|
|
5332
5612
|
this.config.onRender();
|
|
@@ -5361,6 +5641,10 @@ var DragManager = class {
|
|
|
5361
5641
|
*/
|
|
5362
5642
|
destroy() {
|
|
5363
5643
|
try {
|
|
5644
|
+
if (this.pendingAnimationFrame !== void 0) {
|
|
5645
|
+
cancelAnimationFrame(this.pendingAnimationFrame);
|
|
5646
|
+
this.pendingAnimationFrame = void 0;
|
|
5647
|
+
}
|
|
5364
5648
|
if (this.config?.canvas) {
|
|
5365
5649
|
select_default2(this.config.canvas).on(".drag", null);
|
|
5366
5650
|
this.config.canvas.classList.remove("grabbable");
|
|
@@ -5380,6 +5664,10 @@ var DragManager = class {
|
|
|
5380
5664
|
var ZoomManager = class {
|
|
5381
5665
|
config;
|
|
5382
5666
|
zoomBehavior;
|
|
5667
|
+
isZooming = false;
|
|
5668
|
+
zoomRenderPending = false;
|
|
5669
|
+
isProgrammaticZoom = false;
|
|
5670
|
+
// Flag for programmatic zoom operations
|
|
5383
5671
|
/**
|
|
5384
5672
|
* Initialize zoom behavior
|
|
5385
5673
|
*/
|
|
@@ -5427,49 +5715,69 @@ var ZoomManager = class {
|
|
|
5427
5715
|
* Handle zoom start (when panning begins)
|
|
5428
5716
|
*/
|
|
5429
5717
|
handleZoomStart(event) {
|
|
5718
|
+
this.isZooming = true;
|
|
5430
5719
|
if (!this.config) return;
|
|
5431
5720
|
try {
|
|
5432
5721
|
event.sourceEvent?.stopPropagation();
|
|
5433
5722
|
event.sourceEvent?.preventDefault();
|
|
5434
|
-
this.config.
|
|
5723
|
+
if (this.config.renderer && !this.isProgrammaticZoom) {
|
|
5724
|
+
this.config.renderer.setZoomState(true);
|
|
5725
|
+
}
|
|
5726
|
+
if (!this.isProgrammaticZoom) {
|
|
5727
|
+
this.config.canvas.style.cursor = "grabbing";
|
|
5728
|
+
}
|
|
5435
5729
|
} catch (error) {
|
|
5436
5730
|
ErrorHandler.logError(error);
|
|
5437
5731
|
}
|
|
5438
5732
|
}
|
|
5439
5733
|
/**
|
|
5440
|
-
* Handle zoom events
|
|
5734
|
+
* Handle zoom events with RAF throttling for smooth performance
|
|
5441
5735
|
*/
|
|
5442
5736
|
handleZoom(event) {
|
|
5443
5737
|
if (!this.config) return;
|
|
5444
|
-
|
|
5445
|
-
|
|
5446
|
-
|
|
5447
|
-
|
|
5448
|
-
this.config.canvasManager.clear();
|
|
5449
|
-
this.config.canvasManager.applyTransform(transform2);
|
|
5450
|
-
this.config.onRender();
|
|
5451
|
-
} catch (error) {
|
|
5452
|
-
ErrorHandler.logError(error, {
|
|
5453
|
-
transform: event.transform
|
|
5454
|
-
});
|
|
5738
|
+
event.sourceEvent?.stopPropagation();
|
|
5739
|
+
event.sourceEvent?.preventDefault();
|
|
5740
|
+
if (this.zoomRenderPending) {
|
|
5741
|
+
return;
|
|
5455
5742
|
}
|
|
5743
|
+
this.zoomRenderPending = true;
|
|
5744
|
+
requestAnimationFrame(() => {
|
|
5745
|
+
this.zoomRenderPending = false;
|
|
5746
|
+
if (!this.config) {
|
|
5747
|
+
return;
|
|
5748
|
+
}
|
|
5749
|
+
this.config.onRender();
|
|
5750
|
+
});
|
|
5456
5751
|
}
|
|
5457
5752
|
/**
|
|
5458
5753
|
* Handle zoom end (when panning ends)
|
|
5459
5754
|
*/
|
|
5460
5755
|
handleZoomEnd(event) {
|
|
5756
|
+
this.isZooming = false;
|
|
5461
5757
|
if (!this.config) return;
|
|
5462
5758
|
try {
|
|
5463
5759
|
event.sourceEvent?.stopPropagation();
|
|
5464
5760
|
event.sourceEvent?.preventDefault();
|
|
5465
|
-
|
|
5466
|
-
|
|
5467
|
-
|
|
5761
|
+
if (this.config.renderer && !this.isProgrammaticZoom) {
|
|
5762
|
+
this.config.renderer.setZoomState(false);
|
|
5763
|
+
}
|
|
5764
|
+
if (!this.isProgrammaticZoom) {
|
|
5765
|
+
const currentCursor = this.config.canvas.style.cursor;
|
|
5766
|
+
if (currentCursor === "grabbing") {
|
|
5767
|
+
this.config.canvas.style.cursor = "grab";
|
|
5768
|
+
}
|
|
5769
|
+
}
|
|
5770
|
+
if (this.config.onZoomEnd) {
|
|
5771
|
+
this.config.onZoomEnd();
|
|
5468
5772
|
}
|
|
5773
|
+
this.isProgrammaticZoom = false;
|
|
5469
5774
|
} catch (error) {
|
|
5470
5775
|
ErrorHandler.logError(error);
|
|
5471
5776
|
}
|
|
5472
5777
|
}
|
|
5778
|
+
isCurrentlyZooming() {
|
|
5779
|
+
return this.isZooming;
|
|
5780
|
+
}
|
|
5473
5781
|
/**
|
|
5474
5782
|
* Get zoom behavior instance
|
|
5475
5783
|
*/
|
|
@@ -5501,6 +5809,7 @@ var ZoomManager = class {
|
|
|
5501
5809
|
zoomIn(factor = 1.5, center) {
|
|
5502
5810
|
if (!this.config?.canvas || !this.zoomBehavior) return;
|
|
5503
5811
|
try {
|
|
5812
|
+
this.isProgrammaticZoom = true;
|
|
5504
5813
|
const canvas = this.config.canvas;
|
|
5505
5814
|
if (center) {
|
|
5506
5815
|
select_default2(canvas).transition().duration(300).call(this.zoomBehavior.scaleBy, factor, center);
|
|
@@ -5523,6 +5832,7 @@ var ZoomManager = class {
|
|
|
5523
5832
|
resetZoom(duration = 500) {
|
|
5524
5833
|
if (!this.config?.canvas || !this.zoomBehavior) return;
|
|
5525
5834
|
try {
|
|
5835
|
+
this.isProgrammaticZoom = true;
|
|
5526
5836
|
const canvasDims = this.config.canvasManager.getDimensions();
|
|
5527
5837
|
const transform2 = identity2.translate(canvasDims.width / 2, canvasDims.height / 2);
|
|
5528
5838
|
select_default2(this.config.canvas).transition().duration(duration).call(this.zoomBehavior.transform, transform2);
|
|
@@ -5536,6 +5846,7 @@ var ZoomManager = class {
|
|
|
5536
5846
|
setTransform(transform2, duration = 0) {
|
|
5537
5847
|
if (!this.config?.canvas || !this.zoomBehavior) return;
|
|
5538
5848
|
try {
|
|
5849
|
+
this.isProgrammaticZoom = true;
|
|
5539
5850
|
const selection2 = select_default2(this.config.canvas);
|
|
5540
5851
|
const zoomTransform = identity2.translate(transform2.x, transform2.y).scale(transform2.k);
|
|
5541
5852
|
if (duration > 0) {
|
|
@@ -5557,6 +5868,7 @@ var ZoomManager = class {
|
|
|
5557
5868
|
return;
|
|
5558
5869
|
}
|
|
5559
5870
|
try {
|
|
5871
|
+
this.isProgrammaticZoom = true;
|
|
5560
5872
|
const canvas = this.config.canvas;
|
|
5561
5873
|
const transitionDuration = 750;
|
|
5562
5874
|
const canvasDims = this.config.canvasManager.getDimensions();
|
|
@@ -5645,6 +5957,8 @@ var HoverManager = class {
|
|
|
5645
5957
|
flushShadowCanvas;
|
|
5646
5958
|
hasValidPointerPosition = false;
|
|
5647
5959
|
containerWarningLogged = false;
|
|
5960
|
+
// Store bound handlers for proper cleanup
|
|
5961
|
+
boundHandlers = /* @__PURE__ */ new Map();
|
|
5648
5962
|
/**
|
|
5649
5963
|
* Initialize hover manager with force-graph pattern
|
|
5650
5964
|
*/
|
|
@@ -5676,7 +5990,7 @@ var HoverManager = class {
|
|
|
5676
5990
|
console.error("Cannot add pointer event listeners - container is null");
|
|
5677
5991
|
return;
|
|
5678
5992
|
}
|
|
5679
|
-
|
|
5993
|
+
const eventHandler = (ev) => {
|
|
5680
5994
|
const pointerEvent = ev;
|
|
5681
5995
|
const container = this.container;
|
|
5682
5996
|
if (!container) {
|
|
@@ -5701,7 +6015,9 @@ var HoverManager = class {
|
|
|
5701
6015
|
this.containerWarningLogged = true;
|
|
5702
6016
|
}
|
|
5703
6017
|
}
|
|
5704
|
-
}
|
|
6018
|
+
};
|
|
6019
|
+
this.boundHandlers.set(evType, eventHandler);
|
|
6020
|
+
this.container.addEventListener(evType, eventHandler, { passive: true });
|
|
5705
6021
|
});
|
|
5706
6022
|
}
|
|
5707
6023
|
/**
|
|
@@ -5922,6 +6238,12 @@ var HoverManager = class {
|
|
|
5922
6238
|
*/
|
|
5923
6239
|
destroy() {
|
|
5924
6240
|
try {
|
|
6241
|
+
if (this.container) {
|
|
6242
|
+
this.boundHandlers.forEach((handler, eventType) => {
|
|
6243
|
+
this.container.removeEventListener(eventType, handler);
|
|
6244
|
+
});
|
|
6245
|
+
}
|
|
6246
|
+
this.boundHandlers.clear();
|
|
5925
6247
|
this.eventHandlers.clear();
|
|
5926
6248
|
this.hoverState = {
|
|
5927
6249
|
currentHovered: null,
|
|
@@ -5950,6 +6272,8 @@ var SelectionManager = class {
|
|
|
5950
6272
|
};
|
|
5951
6273
|
eventHandlers = /* @__PURE__ */ new Map();
|
|
5952
6274
|
container;
|
|
6275
|
+
// Store bound handlers for proper cleanup
|
|
6276
|
+
boundHandlers = /* @__PURE__ */ new Map();
|
|
5953
6277
|
/**
|
|
5954
6278
|
* Initialize selection manager
|
|
5955
6279
|
*/
|
|
@@ -5971,14 +6295,19 @@ var SelectionManager = class {
|
|
|
5971
6295
|
*/
|
|
5972
6296
|
setupSelectionListeners() {
|
|
5973
6297
|
if (!this.container || !this.canvasState) return;
|
|
5974
|
-
|
|
6298
|
+
const clickHandler = (event) => {
|
|
5975
6299
|
this.handleSelectionClick(event);
|
|
5976
|
-
}
|
|
5977
|
-
|
|
5978
|
-
|
|
6300
|
+
};
|
|
6301
|
+
this.boundHandlers.set("click", clickHandler);
|
|
6302
|
+
this.container.addEventListener("click", clickHandler, { passive: false });
|
|
6303
|
+
const keydownHandler = (event) => {
|
|
6304
|
+
const keyboardEvent = event;
|
|
6305
|
+
if (keyboardEvent.key === "Escape") {
|
|
5979
6306
|
this.clearSelection();
|
|
5980
6307
|
}
|
|
5981
|
-
}
|
|
6308
|
+
};
|
|
6309
|
+
this.boundHandlers.set("keydown", keydownHandler);
|
|
6310
|
+
document.addEventListener("keydown", keydownHandler);
|
|
5982
6311
|
}
|
|
5983
6312
|
/**
|
|
5984
6313
|
* Handle selection click
|
|
@@ -6210,14 +6539,13 @@ var SelectionManager = class {
|
|
|
6210
6539
|
*/
|
|
6211
6540
|
destroy() {
|
|
6212
6541
|
try {
|
|
6213
|
-
if (this.container) {
|
|
6214
|
-
this.container.removeEventListener("click", this.
|
|
6542
|
+
if (this.container && this.boundHandlers.has("click")) {
|
|
6543
|
+
this.container.removeEventListener("click", this.boundHandlers.get("click"));
|
|
6215
6544
|
}
|
|
6216
|
-
|
|
6217
|
-
|
|
6218
|
-
|
|
6219
|
-
|
|
6220
|
-
});
|
|
6545
|
+
if (this.boundHandlers.has("keydown")) {
|
|
6546
|
+
document.removeEventListener("keydown", this.boundHandlers.get("keydown"));
|
|
6547
|
+
}
|
|
6548
|
+
this.boundHandlers.clear();
|
|
6221
6549
|
this.eventHandlers.clear();
|
|
6222
6550
|
this.selectionState = {
|
|
6223
6551
|
selectedNode: null,
|
|
@@ -6295,7 +6623,7 @@ var NodesRenderer = class {
|
|
|
6295
6623
|
/**
|
|
6296
6624
|
* Render nodes to canvas using StyleResolver with performance metrics
|
|
6297
6625
|
*/
|
|
6298
|
-
static renderWithStyleResolver(ctx, nodes, styleResolver, isNodeHovered, isNodeSelected, performanceMetrics) {
|
|
6626
|
+
static renderWithStyleResolver(ctx, nodes, styleResolver, isNodeHovered, isNodeSelected, isNodeHighlighted, performanceMetrics) {
|
|
6299
6627
|
try {
|
|
6300
6628
|
for (const node of nodes) {
|
|
6301
6629
|
const x3 = node.x;
|
|
@@ -6303,6 +6631,7 @@ var NodesRenderer = class {
|
|
|
6303
6631
|
const hoverStart = performance.now();
|
|
6304
6632
|
const isHovered = isNodeHovered(node.id);
|
|
6305
6633
|
const isSelected = isNodeSelected ? isNodeSelected(node.id) : false;
|
|
6634
|
+
const isHighlighted = isNodeHighlighted ? isNodeHighlighted(node.id) : false;
|
|
6306
6635
|
if (performanceMetrics) {
|
|
6307
6636
|
performanceMetrics.hoverChecks += performance.now() - hoverStart;
|
|
6308
6637
|
}
|
|
@@ -6310,7 +6639,8 @@ var NodesRenderer = class {
|
|
|
6310
6639
|
const style = styleResolver.resolveNodeStyle({
|
|
6311
6640
|
node,
|
|
6312
6641
|
isHovered,
|
|
6313
|
-
isSelected
|
|
6642
|
+
isSelected,
|
|
6643
|
+
isHighlighted
|
|
6314
6644
|
});
|
|
6315
6645
|
if (performanceMetrics) {
|
|
6316
6646
|
performanceMetrics.styleResolution += performance.now() - styleStart;
|
|
@@ -6460,6 +6790,514 @@ var NodeLabelsRenderer = class {
|
|
|
6460
6790
|
}
|
|
6461
6791
|
};
|
|
6462
6792
|
|
|
6793
|
+
// src/v2/rendering/z-index-renderer.ts
|
|
6794
|
+
var OptimizedZIndexRenderer = class _OptimizedZIndexRenderer {
|
|
6795
|
+
config;
|
|
6796
|
+
styleResolver;
|
|
6797
|
+
// Pre-computed adjacency maps for O(1) lookups
|
|
6798
|
+
nodeToLinksMap = /* @__PURE__ */ new Map();
|
|
6799
|
+
linkToNodesMap = /* @__PURE__ */ new Map();
|
|
6800
|
+
// Reusable arrays to minimize garbage collection
|
|
6801
|
+
reusableArrays = {
|
|
6802
|
+
backgroundNodes: [],
|
|
6803
|
+
foregroundNodes: [],
|
|
6804
|
+
backgroundLinks: [],
|
|
6805
|
+
foregroundLinks: []
|
|
6806
|
+
};
|
|
6807
|
+
// Pre-sorted arrays to avoid redundant sorting
|
|
6808
|
+
sortedBackgroundNodes = [];
|
|
6809
|
+
sortedForegroundNodes = [];
|
|
6810
|
+
// State checkers (injected from parent renderer)
|
|
6811
|
+
isNodeHovered = () => false;
|
|
6812
|
+
isNodeSelected = () => false;
|
|
6813
|
+
isLinkHovered = () => false;
|
|
6814
|
+
isLinkSelected = () => false;
|
|
6815
|
+
getLinkId = () => "";
|
|
6816
|
+
getLinkMidpoint = () => null;
|
|
6817
|
+
// Component renderers (injected)
|
|
6818
|
+
renderNodes;
|
|
6819
|
+
renderLinks;
|
|
6820
|
+
renderNodeLabels;
|
|
6821
|
+
/**
|
|
6822
|
+
* Initialize the optimized z-index renderer
|
|
6823
|
+
*/
|
|
6824
|
+
initialize(config) {
|
|
6825
|
+
this.config = { nodes: config.nodes, links: config.links };
|
|
6826
|
+
this.styleResolver = config.styleResolver;
|
|
6827
|
+
this.isNodeHovered = config.isNodeHovered;
|
|
6828
|
+
this.isNodeSelected = config.isNodeSelected;
|
|
6829
|
+
this.isLinkHovered = config.isLinkHovered;
|
|
6830
|
+
this.isLinkSelected = config.isLinkSelected;
|
|
6831
|
+
this.getLinkId = config.getLinkId;
|
|
6832
|
+
this.getLinkMidpoint = config.getLinkMidpoint;
|
|
6833
|
+
this.renderNodes = config.renderNodes;
|
|
6834
|
+
this.renderLinks = config.renderLinks;
|
|
6835
|
+
this.renderNodeLabels = config.renderNodeLabels;
|
|
6836
|
+
this.buildAdjacencyMaps();
|
|
6837
|
+
}
|
|
6838
|
+
/**
|
|
6839
|
+
* Build adjacency maps once at initialization - O(n) complexity
|
|
6840
|
+
*/
|
|
6841
|
+
buildAdjacencyMaps() {
|
|
6842
|
+
if (!this.config) return;
|
|
6843
|
+
this.nodeToLinksMap.clear();
|
|
6844
|
+
this.linkToNodesMap.clear();
|
|
6845
|
+
try {
|
|
6846
|
+
for (const link of this.config.links) {
|
|
6847
|
+
const sourceId = typeof link.source === "string" ? link.source : link.source.id;
|
|
6848
|
+
const targetId = typeof link.target === "string" ? link.target : link.target.id;
|
|
6849
|
+
const linkId = this.getLinkId(link);
|
|
6850
|
+
this.linkToNodesMap.set(linkId, [sourceId, targetId]);
|
|
6851
|
+
if (!this.nodeToLinksMap.has(sourceId)) {
|
|
6852
|
+
this.nodeToLinksMap.set(sourceId, /* @__PURE__ */ new Set());
|
|
6853
|
+
}
|
|
6854
|
+
if (!this.nodeToLinksMap.has(targetId)) {
|
|
6855
|
+
this.nodeToLinksMap.set(targetId, /* @__PURE__ */ new Set());
|
|
6856
|
+
}
|
|
6857
|
+
this.nodeToLinksMap.get(sourceId).add(linkId);
|
|
6858
|
+
this.nodeToLinksMap.get(targetId).add(linkId);
|
|
6859
|
+
}
|
|
6860
|
+
} catch (error) {
|
|
6861
|
+
ErrorHandler.logError(error);
|
|
6862
|
+
}
|
|
6863
|
+
}
|
|
6864
|
+
/**
|
|
6865
|
+
* Determine interactive entities - O(n) total complexity
|
|
6866
|
+
*/
|
|
6867
|
+
getInteractiveEntities() {
|
|
6868
|
+
if (!this.config) {
|
|
6869
|
+
return { interactiveNodes: /* @__PURE__ */ new Set(), interactiveLinks: /* @__PURE__ */ new Set() };
|
|
6870
|
+
}
|
|
6871
|
+
const interactiveNodes = /* @__PURE__ */ new Set();
|
|
6872
|
+
const interactiveLinks = /* @__PURE__ */ new Set();
|
|
6873
|
+
try {
|
|
6874
|
+
for (const node of this.config.nodes) {
|
|
6875
|
+
if (this.isNodeHovered(node.id) || this.isNodeSelected(node.id)) {
|
|
6876
|
+
interactiveNodes.add(node.id);
|
|
6877
|
+
const connectedLinks = this.nodeToLinksMap.get(node.id);
|
|
6878
|
+
if (connectedLinks) {
|
|
6879
|
+
connectedLinks.forEach((linkId) => interactiveLinks.add(linkId));
|
|
6880
|
+
}
|
|
6881
|
+
}
|
|
6882
|
+
}
|
|
6883
|
+
for (const link of this.config.links) {
|
|
6884
|
+
const linkId = this.getLinkId(link);
|
|
6885
|
+
if (this.isLinkHovered(link) || this.isLinkSelected(link)) {
|
|
6886
|
+
interactiveLinks.add(linkId);
|
|
6887
|
+
const connectedNodes = this.linkToNodesMap.get(linkId);
|
|
6888
|
+
if (connectedNodes) {
|
|
6889
|
+
interactiveNodes.add(connectedNodes[0]);
|
|
6890
|
+
interactiveNodes.add(connectedNodes[1]);
|
|
6891
|
+
}
|
|
6892
|
+
}
|
|
6893
|
+
}
|
|
6894
|
+
} catch (error) {
|
|
6895
|
+
ErrorHandler.logError(error);
|
|
6896
|
+
}
|
|
6897
|
+
return { interactiveNodes, interactiveLinks };
|
|
6898
|
+
}
|
|
6899
|
+
/**
|
|
6900
|
+
* Clear and prepare reusable arrays to avoid garbage collection
|
|
6901
|
+
*/
|
|
6902
|
+
clearReusableArrays() {
|
|
6903
|
+
this.reusableArrays.backgroundNodes.length = 0;
|
|
6904
|
+
this.reusableArrays.foregroundNodes.length = 0;
|
|
6905
|
+
this.reusableArrays.backgroundLinks.length = 0;
|
|
6906
|
+
this.reusableArrays.foregroundLinks.length = 0;
|
|
6907
|
+
this.sortedBackgroundNodes.length = 0;
|
|
6908
|
+
this.sortedForegroundNodes.length = 0;
|
|
6909
|
+
}
|
|
6910
|
+
/**
|
|
6911
|
+
* Main render with optimized layering - O(n) total complexity
|
|
6912
|
+
*/
|
|
6913
|
+
render(ctx, performanceMetrics) {
|
|
6914
|
+
if (!this.config || !this.styleResolver) {
|
|
6915
|
+
throw new RenderError("Z-Index renderer not initialized");
|
|
6916
|
+
}
|
|
6917
|
+
const startTime = performance.now();
|
|
6918
|
+
try {
|
|
6919
|
+
const { interactiveNodes, interactiveLinks } = this.getInteractiveEntities();
|
|
6920
|
+
this.clearReusableArrays();
|
|
6921
|
+
this.separateEntitiesIntoLayers(interactiveNodes, interactiveLinks);
|
|
6922
|
+
this.renderAllComponentsInCorrectOrder(ctx);
|
|
6923
|
+
if (performanceMetrics) {
|
|
6924
|
+
performanceMetrics.renderTotal += performance.now() - startTime;
|
|
6925
|
+
}
|
|
6926
|
+
} catch (error) {
|
|
6927
|
+
ErrorHandler.logError(error);
|
|
6928
|
+
throw new RenderError("Failed to render with z-index optimization");
|
|
6929
|
+
}
|
|
6930
|
+
}
|
|
6931
|
+
/**
|
|
6932
|
+
* Separate entities into background/foreground layers - O(n) - OPTIMIZED
|
|
6933
|
+
* CRITICAL: Ensure entities appear in ONLY ONE layer to prevent overlapping labels
|
|
6934
|
+
* OPTIMIZED: Combined with sorting to reduce iterations
|
|
6935
|
+
*/
|
|
6936
|
+
separateEntitiesIntoLayers(interactiveNodes, interactiveLinks) {
|
|
6937
|
+
if (!this.config) return;
|
|
6938
|
+
for (const node of this.config.nodes) {
|
|
6939
|
+
if (interactiveNodes.has(node.id)) {
|
|
6940
|
+
this.reusableArrays.foregroundNodes.push(node);
|
|
6941
|
+
} else {
|
|
6942
|
+
this.reusableArrays.backgroundNodes.push(node);
|
|
6943
|
+
}
|
|
6944
|
+
}
|
|
6945
|
+
for (const link of this.config.links) {
|
|
6946
|
+
const linkId = this.getLinkId(link);
|
|
6947
|
+
if (interactiveLinks.has(linkId)) {
|
|
6948
|
+
this.reusableArrays.foregroundLinks.push(link);
|
|
6949
|
+
} else {
|
|
6950
|
+
this.reusableArrays.backgroundLinks.push(link);
|
|
6951
|
+
}
|
|
6952
|
+
}
|
|
6953
|
+
this.sortNodeArrays();
|
|
6954
|
+
}
|
|
6955
|
+
/**
|
|
6956
|
+
* Sort node arrays once after separation - O(n log n) but only once per render
|
|
6957
|
+
*/
|
|
6958
|
+
sortNodeArrays() {
|
|
6959
|
+
this.sortedBackgroundNodes = this.sortNodesForSubLayering(this.reusableArrays.backgroundNodes);
|
|
6960
|
+
this.sortedForegroundNodes = this.sortNodesForSubLayering(this.reusableArrays.foregroundNodes);
|
|
6961
|
+
}
|
|
6962
|
+
/**
|
|
6963
|
+
* Render all components in correct z-order across all layers
|
|
6964
|
+
* PROPERLY FIXED: Atomic node+label rendering for correct sub-layering
|
|
6965
|
+
*/
|
|
6966
|
+
renderAllComponentsInCorrectOrder(ctx) {
|
|
6967
|
+
if (this.renderLinks) {
|
|
6968
|
+
this.renderLinks(ctx, this.reusableArrays.backgroundLinks);
|
|
6969
|
+
}
|
|
6970
|
+
this.renderLinkLabels(ctx, this.reusableArrays.backgroundLinks);
|
|
6971
|
+
this.renderNodesWithLabelsAtomically(ctx, this.sortedBackgroundNodes);
|
|
6972
|
+
if (this.renderLinks) {
|
|
6973
|
+
this.renderLinks(ctx, this.reusableArrays.foregroundLinks);
|
|
6974
|
+
}
|
|
6975
|
+
this.renderLinkLabels(ctx, this.reusableArrays.foregroundLinks);
|
|
6976
|
+
this.renderNodesWithLabelsAtomically(ctx, this.sortedForegroundNodes);
|
|
6977
|
+
}
|
|
6978
|
+
/**
|
|
6979
|
+
* Render nodes with atomic circle+label rendering for proper sub-layering
|
|
6980
|
+
* OPTIMIZED: Uses pre-sorted nodes and batch rendering when possible
|
|
6981
|
+
*/
|
|
6982
|
+
renderNodesWithLabelsAtomically(ctx, sortedNodes) {
|
|
6983
|
+
if (sortedNodes.length === 0) return;
|
|
6984
|
+
try {
|
|
6985
|
+
for (const node of sortedNodes) {
|
|
6986
|
+
if (this.renderNodes) {
|
|
6987
|
+
this.renderNodes(ctx, [node]);
|
|
6988
|
+
}
|
|
6989
|
+
if (this.renderNodeLabels) {
|
|
6990
|
+
this.renderNodeLabels(ctx, [node]);
|
|
6991
|
+
}
|
|
6992
|
+
}
|
|
6993
|
+
} catch (error) {
|
|
6994
|
+
ErrorHandler.logError(error, { nodeCount: sortedNodes.length });
|
|
6995
|
+
}
|
|
6996
|
+
}
|
|
6997
|
+
// Static type order for performance (avoid object creation in hot path)
|
|
6998
|
+
static TYPE_ORDER = {
|
|
6999
|
+
"Server": 1,
|
|
7000
|
+
"Database": 2,
|
|
7001
|
+
"Service": 3,
|
|
7002
|
+
"Client": 4,
|
|
7003
|
+
"Gateway": 5
|
|
7004
|
+
};
|
|
7005
|
+
/**
|
|
7006
|
+
* Sort nodes for consistent sub-layer ordering within the same layer
|
|
7007
|
+
* OPTIMIZED: Reduced object allocations and string operations
|
|
7008
|
+
*/
|
|
7009
|
+
sortNodesForSubLayering(nodes) {
|
|
7010
|
+
if (nodes.length <= 1) return [...nodes];
|
|
7011
|
+
return [...nodes].sort((a2, b) => {
|
|
7012
|
+
const aPriority = _OptimizedZIndexRenderer.TYPE_ORDER[a2.type] ?? 999;
|
|
7013
|
+
const bPriority = _OptimizedZIndexRenderer.TYPE_ORDER[b.type] ?? 999;
|
|
7014
|
+
if (aPriority !== bPriority) return aPriority - bPriority;
|
|
7015
|
+
const aRadius = a2.style?.radius ?? 20;
|
|
7016
|
+
const bRadius = b.style?.radius ?? 20;
|
|
7017
|
+
if (aRadius !== bRadius) return bRadius - aRadius;
|
|
7018
|
+
const aY = a2.y ?? 0;
|
|
7019
|
+
const bY = b.y ?? 0;
|
|
7020
|
+
if (aY !== bY) return aY - bY;
|
|
7021
|
+
const aX = a2.x ?? 0;
|
|
7022
|
+
const bX = b.x ?? 0;
|
|
7023
|
+
if (aX !== bX) return aX - bX;
|
|
7024
|
+
return a2.id < b.id ? -1 : a2.id > b.id ? 1 : 0;
|
|
7025
|
+
});
|
|
7026
|
+
}
|
|
7027
|
+
/**
|
|
7028
|
+
* Optimized link label rendering with visibility rules
|
|
7029
|
+
*/
|
|
7030
|
+
renderLinkLabels(ctx, links) {
|
|
7031
|
+
if (!this.styleResolver) return;
|
|
7032
|
+
for (const link of links) {
|
|
7033
|
+
if (!link.label) continue;
|
|
7034
|
+
try {
|
|
7035
|
+
const style = this.styleResolver.resolveLinkStyle({
|
|
7036
|
+
link,
|
|
7037
|
+
isHovered: this.isLinkHovered(link),
|
|
7038
|
+
isSelected: this.isLinkSelected(link)
|
|
7039
|
+
});
|
|
7040
|
+
if (!this.shouldShowLinkLabel(style.label || null, link)) continue;
|
|
7041
|
+
const midpoint = this.getLinkMidpoint(link);
|
|
7042
|
+
if (midpoint) {
|
|
7043
|
+
this.renderSingleLinkLabel(ctx, link.label, midpoint.x, midpoint.y, style.label);
|
|
7044
|
+
}
|
|
7045
|
+
} catch (error) {
|
|
7046
|
+
ErrorHandler.logError(error, { linkId: this.getLinkId(link) });
|
|
7047
|
+
}
|
|
7048
|
+
}
|
|
7049
|
+
}
|
|
7050
|
+
/**
|
|
7051
|
+
* Visibility logic for link labels (matches requirements)
|
|
7052
|
+
*/
|
|
7053
|
+
shouldShowLinkLabel(labelStyle, link) {
|
|
7054
|
+
if (!labelStyle?.enabled) return false;
|
|
7055
|
+
const isHovered = this.isLinkHovered(link);
|
|
7056
|
+
const isSelected = this.isLinkSelected(link);
|
|
7057
|
+
if (isSelected) return true;
|
|
7058
|
+
switch (labelStyle.visibility) {
|
|
7059
|
+
case "always":
|
|
7060
|
+
return true;
|
|
7061
|
+
case "hover":
|
|
7062
|
+
return isHovered;
|
|
7063
|
+
case "selection":
|
|
7064
|
+
return isSelected;
|
|
7065
|
+
// Already handled above
|
|
7066
|
+
default:
|
|
7067
|
+
return true;
|
|
7068
|
+
}
|
|
7069
|
+
}
|
|
7070
|
+
/**
|
|
7071
|
+
* Render a single link label at given coordinates
|
|
7072
|
+
*/
|
|
7073
|
+
renderSingleLinkLabel(ctx, text, x3, y3, style) {
|
|
7074
|
+
try {
|
|
7075
|
+
ctx.font = style.font ?? "10px Arial";
|
|
7076
|
+
const metrics = ctx.measureText(text);
|
|
7077
|
+
const textWidth = metrics.width;
|
|
7078
|
+
const textHeight = (metrics.actualBoundingBoxAscent || 10) + (metrics.actualBoundingBoxDescent || 4);
|
|
7079
|
+
const rectWidth = textWidth + (style.paddingX ?? 8) * 2;
|
|
7080
|
+
const rectHeight = textHeight + (style.paddingY ?? 4) * 2;
|
|
7081
|
+
const rectX = x3 - rectWidth / 2;
|
|
7082
|
+
const rectY = y3 - rectHeight / 2;
|
|
7083
|
+
if (style.backgroundColor && style.backgroundColor !== "transparent") {
|
|
7084
|
+
ctx.fillStyle = style.backgroundColor;
|
|
7085
|
+
this.roundRect(ctx, rectX, rectY, rectWidth, rectHeight, style.borderRadius ?? 4);
|
|
7086
|
+
ctx.fill();
|
|
7087
|
+
}
|
|
7088
|
+
if ((style.borderWidth ?? 0) > 0 && style.borderColor && style.borderColor !== "transparent") {
|
|
7089
|
+
ctx.strokeStyle = style.borderColor;
|
|
7090
|
+
ctx.lineWidth = style.borderWidth ?? 1;
|
|
7091
|
+
this.roundRect(ctx, rectX, rectY, rectWidth, rectHeight, style.borderRadius ?? 4);
|
|
7092
|
+
ctx.stroke();
|
|
7093
|
+
}
|
|
7094
|
+
ctx.fillStyle = style.textColor ?? "#000000";
|
|
7095
|
+
ctx.textAlign = "center";
|
|
7096
|
+
ctx.textBaseline = "middle";
|
|
7097
|
+
ctx.fillText(text, x3, y3);
|
|
7098
|
+
} catch (error) {
|
|
7099
|
+
ErrorHandler.logError(error);
|
|
7100
|
+
}
|
|
7101
|
+
}
|
|
7102
|
+
/**
|
|
7103
|
+
* Helper to draw rounded rectangle
|
|
7104
|
+
*/
|
|
7105
|
+
roundRect(ctx, x3, y3, width, height, radius) {
|
|
7106
|
+
if (radius === 0) {
|
|
7107
|
+
ctx.rect(x3, y3, width, height);
|
|
7108
|
+
return;
|
|
7109
|
+
}
|
|
7110
|
+
ctx.beginPath();
|
|
7111
|
+
ctx.moveTo(x3 + radius, y3);
|
|
7112
|
+
ctx.lineTo(x3 + width - radius, y3);
|
|
7113
|
+
ctx.quadraticCurveTo(x3 + width, y3, x3 + width, y3 + radius);
|
|
7114
|
+
ctx.lineTo(x3 + width, y3 + height - radius);
|
|
7115
|
+
ctx.quadraticCurveTo(x3 + width, y3 + height, x3 + width - radius, y3 + height);
|
|
7116
|
+
ctx.lineTo(x3 + radius, y3 + height);
|
|
7117
|
+
ctx.quadraticCurveTo(x3, y3 + height, x3, y3 + height - radius);
|
|
7118
|
+
ctx.lineTo(x3, y3 + radius);
|
|
7119
|
+
ctx.quadraticCurveTo(x3, y3, x3 + radius, y3);
|
|
7120
|
+
ctx.closePath();
|
|
7121
|
+
}
|
|
7122
|
+
/**
|
|
7123
|
+
* Update configuration and rebuild adjacency maps
|
|
7124
|
+
*/
|
|
7125
|
+
updateConfig(config) {
|
|
7126
|
+
this.config = config;
|
|
7127
|
+
this.buildAdjacencyMaps();
|
|
7128
|
+
this.sortedBackgroundNodes.length = 0;
|
|
7129
|
+
this.sortedForegroundNodes.length = 0;
|
|
7130
|
+
}
|
|
7131
|
+
/**
|
|
7132
|
+
* Destroy and clean up resources
|
|
7133
|
+
*/
|
|
7134
|
+
destroy() {
|
|
7135
|
+
this.config = void 0;
|
|
7136
|
+
this.styleResolver = void 0;
|
|
7137
|
+
this.nodeToLinksMap.clear();
|
|
7138
|
+
this.linkToNodesMap.clear();
|
|
7139
|
+
this.clearReusableArrays();
|
|
7140
|
+
}
|
|
7141
|
+
};
|
|
7142
|
+
|
|
7143
|
+
// src/v2/rendering/zoom-renderer.ts
|
|
7144
|
+
var ZoomRenderer = class {
|
|
7145
|
+
config;
|
|
7146
|
+
stateManager;
|
|
7147
|
+
isZooming = false;
|
|
7148
|
+
// Fast zoom cache for O(1) lookups - everything pre-computed
|
|
7149
|
+
fastZoomNodeCache = /* @__PURE__ */ new Map();
|
|
7150
|
+
/**
|
|
7151
|
+
* Initialize zoom renderer
|
|
7152
|
+
*/
|
|
7153
|
+
initialize(config) {
|
|
7154
|
+
this.config = config;
|
|
7155
|
+
this.stateManager = config.stateManager;
|
|
7156
|
+
this.buildFastZoomCache();
|
|
7157
|
+
}
|
|
7158
|
+
/**
|
|
7159
|
+
* Render with zoom optimization
|
|
7160
|
+
*/
|
|
7161
|
+
render(ctx, zIndexRenderer) {
|
|
7162
|
+
if (!this.config) return;
|
|
7163
|
+
if (this.isZooming) {
|
|
7164
|
+
this.renderFastZoom(ctx);
|
|
7165
|
+
} else {
|
|
7166
|
+
zIndexRenderer.render(ctx);
|
|
7167
|
+
}
|
|
7168
|
+
}
|
|
7169
|
+
/**
|
|
7170
|
+
* Fast rendering during zoom (O(1) cached properties)
|
|
7171
|
+
*/
|
|
7172
|
+
renderFastZoom(ctx) {
|
|
7173
|
+
if (!this.config) return;
|
|
7174
|
+
const { nodes, links } = this.config;
|
|
7175
|
+
ctx.strokeStyle = "#999";
|
|
7176
|
+
ctx.lineWidth = 1;
|
|
7177
|
+
for (const link of links) {
|
|
7178
|
+
const sourceNode = typeof link.source === "string" ? this.stateManager.getNode(link.source) : link.source;
|
|
7179
|
+
const targetNode = typeof link.target === "string" ? this.stateManager.getNode(link.target) : link.target;
|
|
7180
|
+
if (sourceNode && targetNode && sourceNode.x && sourceNode.y && targetNode.x && targetNode.y) {
|
|
7181
|
+
ctx.beginPath();
|
|
7182
|
+
ctx.moveTo(sourceNode.x, sourceNode.y);
|
|
7183
|
+
ctx.lineTo(targetNode.x, targetNode.y);
|
|
7184
|
+
ctx.stroke();
|
|
7185
|
+
}
|
|
7186
|
+
}
|
|
7187
|
+
for (const node of nodes) {
|
|
7188
|
+
if (!node.x || !node.y) continue;
|
|
7189
|
+
const cachedProps = this.fastZoomNodeCache.get(node.id);
|
|
7190
|
+
if (!cachedProps) continue;
|
|
7191
|
+
ctx.beginPath();
|
|
7192
|
+
ctx.arc(node.x, node.y, cachedProps.radius, 0, 2 * Math.PI);
|
|
7193
|
+
ctx.fillStyle = cachedProps.fill;
|
|
7194
|
+
ctx.fill();
|
|
7195
|
+
}
|
|
7196
|
+
this.renderFastZoomLabels(ctx);
|
|
7197
|
+
}
|
|
7198
|
+
/**
|
|
7199
|
+
* Render pre-computed node labels during zoom (true O(1) lookups)
|
|
7200
|
+
*/
|
|
7201
|
+
renderFastZoomLabels(ctx) {
|
|
7202
|
+
if (!this.config) return;
|
|
7203
|
+
const { nodes } = this.config;
|
|
7204
|
+
ctx.font = "9px Arial";
|
|
7205
|
+
ctx.textAlign = "center";
|
|
7206
|
+
ctx.textBaseline = "middle";
|
|
7207
|
+
for (const node of nodes) {
|
|
7208
|
+
if (!node.x || !node.y) continue;
|
|
7209
|
+
const cachedProps = this.fastZoomNodeCache.get(node.id);
|
|
7210
|
+
if (!cachedProps?.truncatedLabel) continue;
|
|
7211
|
+
ctx.fillStyle = cachedProps.textColor;
|
|
7212
|
+
ctx.fillText(cachedProps.truncatedLabel, node.x, node.y);
|
|
7213
|
+
}
|
|
7214
|
+
}
|
|
7215
|
+
/**
|
|
7216
|
+
* Build fast zoom cache with pre-computed properties for true O(1) performance
|
|
7217
|
+
*/
|
|
7218
|
+
buildFastZoomCache() {
|
|
7219
|
+
if (!this.config) return;
|
|
7220
|
+
try {
|
|
7221
|
+
this.fastZoomNodeCache.clear();
|
|
7222
|
+
const tempCanvas = document.createElement("canvas");
|
|
7223
|
+
const tempCtx = tempCanvas.getContext("2d");
|
|
7224
|
+
if (!tempCtx) return;
|
|
7225
|
+
tempCtx.font = "9px Arial";
|
|
7226
|
+
for (const node of this.config.nodes) {
|
|
7227
|
+
const nodeStyle = this.config.styleResolver.resolveNodeStyle({
|
|
7228
|
+
node,
|
|
7229
|
+
isHovered: false,
|
|
7230
|
+
isSelected: false
|
|
7231
|
+
});
|
|
7232
|
+
const label = node.label || node.id;
|
|
7233
|
+
const maxWidth = nodeStyle.radius * 2 - 6;
|
|
7234
|
+
const truncatedLabel = this.preComputeTruncatedLabel(tempCtx, label, maxWidth);
|
|
7235
|
+
const textColor = nodeStyle.label?.textColor || "#ffffff";
|
|
7236
|
+
this.fastZoomNodeCache.set(node.id, {
|
|
7237
|
+
radius: nodeStyle.radius,
|
|
7238
|
+
fill: nodeStyle.fill,
|
|
7239
|
+
truncatedLabel,
|
|
7240
|
+
textColor
|
|
7241
|
+
});
|
|
7242
|
+
}
|
|
7243
|
+
} catch (error) {
|
|
7244
|
+
ErrorHandler.logError(error);
|
|
7245
|
+
}
|
|
7246
|
+
}
|
|
7247
|
+
/**
|
|
7248
|
+
* Pre-compute truncated label (called only during cache build)
|
|
7249
|
+
*/
|
|
7250
|
+
preComputeTruncatedLabel(ctx, label, maxWidth) {
|
|
7251
|
+
if (ctx.measureText(label).width <= maxWidth) {
|
|
7252
|
+
return label;
|
|
7253
|
+
}
|
|
7254
|
+
let truncated = label;
|
|
7255
|
+
while (truncated.length > 1 && ctx.measureText(`${truncated}\u2026`).width > maxWidth) {
|
|
7256
|
+
truncated = truncated.slice(0, -1);
|
|
7257
|
+
}
|
|
7258
|
+
return truncated.length < label.length ? `${truncated}\u2026` : truncated;
|
|
7259
|
+
}
|
|
7260
|
+
/**
|
|
7261
|
+
* Set zoom state for performance optimization
|
|
7262
|
+
*/
|
|
7263
|
+
setZoomState(isZooming) {
|
|
7264
|
+
this.isZooming = isZooming;
|
|
7265
|
+
}
|
|
7266
|
+
/**
|
|
7267
|
+
* Get current zoom state
|
|
7268
|
+
*/
|
|
7269
|
+
getZoomState() {
|
|
7270
|
+
return this.isZooming;
|
|
7271
|
+
}
|
|
7272
|
+
/**
|
|
7273
|
+
* Update configuration and rebuild cache
|
|
7274
|
+
*/
|
|
7275
|
+
updateConfig(updates) {
|
|
7276
|
+
if (this.config) {
|
|
7277
|
+
Object.assign(this.config, updates);
|
|
7278
|
+
this.buildFastZoomCache();
|
|
7279
|
+
}
|
|
7280
|
+
}
|
|
7281
|
+
/**
|
|
7282
|
+
* Get performance stats
|
|
7283
|
+
*/
|
|
7284
|
+
getStats() {
|
|
7285
|
+
return {
|
|
7286
|
+
cachedNodeProperties: this.fastZoomNodeCache.size,
|
|
7287
|
+
isZooming: this.isZooming
|
|
7288
|
+
};
|
|
7289
|
+
}
|
|
7290
|
+
/**
|
|
7291
|
+
* Destroy and clean up
|
|
7292
|
+
*/
|
|
7293
|
+
destroy() {
|
|
7294
|
+
this.config = void 0;
|
|
7295
|
+
this.stateManager = void 0;
|
|
7296
|
+
this.fastZoomNodeCache.clear();
|
|
7297
|
+
this.isZooming = false;
|
|
7298
|
+
}
|
|
7299
|
+
};
|
|
7300
|
+
|
|
6463
7301
|
// src/v2/rendering/link-labels-renderer.ts
|
|
6464
7302
|
var LinkLabelsRenderer = class {
|
|
6465
7303
|
static textMetricsCache = /* @__PURE__ */ new Map();
|
|
@@ -6666,107 +7504,30 @@ var LinkLabelsRenderer = class {
|
|
|
6666
7504
|
}
|
|
6667
7505
|
};
|
|
6668
7506
|
|
|
6669
|
-
// src/v2/rendering/renderer.ts
|
|
6670
|
-
var
|
|
7507
|
+
// src/v2/rendering/hit-detection-renderer.ts
|
|
7508
|
+
var HitDetectionRenderer = class {
|
|
6671
7509
|
config;
|
|
6672
|
-
|
|
6673
|
-
|
|
6674
|
-
selectionManager;
|
|
6675
|
-
styleResolver;
|
|
6676
|
-
// Performance optimization: O(1) node lookups
|
|
6677
|
-
nodeMap = /* @__PURE__ */ new Map();
|
|
6678
|
-
// Shadow canvas optimization (Step 6 optimization)
|
|
7510
|
+
stateManager;
|
|
7511
|
+
// Shadow canvas optimization (throttling)
|
|
6679
7512
|
shadowCanvasDirty = true;
|
|
6680
7513
|
lastShadowRenderTime = 0;
|
|
6681
7514
|
SHADOW_RENDER_THROTTLE = 32;
|
|
6682
7515
|
// ~30 FPS max for shadow canvas
|
|
6683
|
-
// Large graph optimization flag
|
|
6684
|
-
hasLoggedLargeGraphOptimization = false;
|
|
6685
|
-
// Performance metrics
|
|
6686
|
-
performanceMetrics = {
|
|
6687
|
-
renderTotal: 0,
|
|
6688
|
-
renderNodes: 0,
|
|
6689
|
-
renderLinks: 0,
|
|
6690
|
-
renderLinkLabels: 0,
|
|
6691
|
-
renderNodeLabels: 0,
|
|
6692
|
-
styleResolution: 0,
|
|
6693
|
-
hoverChecks: 0,
|
|
6694
|
-
canvasCalls: 0,
|
|
6695
|
-
frameCount: 0
|
|
6696
|
-
};
|
|
6697
|
-
// Force-graph pattern: configurable link hover precision
|
|
6698
|
-
linkHoverPrecision = 4;
|
|
6699
|
-
// Default 4px like force-graph
|
|
6700
7516
|
/**
|
|
6701
|
-
* Initialize
|
|
7517
|
+
* Initialize hit detection renderer
|
|
6702
7518
|
*/
|
|
6703
|
-
initialize(config
|
|
7519
|
+
initialize(config) {
|
|
7520
|
+
this.config = config;
|
|
7521
|
+
this.stateManager = config.stateManager;
|
|
7522
|
+
}
|
|
7523
|
+
/**
|
|
7524
|
+
* Render shadow canvas for hit detection with throttling
|
|
7525
|
+
*/
|
|
7526
|
+
renderShadowCanvas() {
|
|
7527
|
+
if (!this.config) return;
|
|
7528
|
+
const now2 = Date.now();
|
|
6704
7529
|
try {
|
|
6705
|
-
|
|
6706
|
-
this.canvasState = canvasState;
|
|
6707
|
-
this.hoverManager = hoverManager;
|
|
6708
|
-
this.selectionManager = selectionManager;
|
|
6709
|
-
this.styleResolver = createStyleResolver(config.interaction);
|
|
6710
|
-
this.buildNodeIndex();
|
|
6711
|
-
} catch (error) {
|
|
6712
|
-
ErrorHandler.logError(error, {
|
|
6713
|
-
nodeCount: config.nodes.length,
|
|
6714
|
-
linkCount: config.links.length
|
|
6715
|
-
});
|
|
6716
|
-
throw error;
|
|
6717
|
-
}
|
|
6718
|
-
}
|
|
6719
|
-
/**
|
|
6720
|
-
* Main render method with performance metrics (Instrumented)
|
|
6721
|
-
*/
|
|
6722
|
-
render() {
|
|
6723
|
-
if (!this.config || !this.canvasState) {
|
|
6724
|
-
throw new RenderError("Renderer not initialized");
|
|
6725
|
-
}
|
|
6726
|
-
const startTime = performance.now();
|
|
6727
|
-
this.performanceMetrics.frameCount++;
|
|
6728
|
-
try {
|
|
6729
|
-
const { ctx } = this.canvasState;
|
|
6730
|
-
this.clearCanvas(ctx);
|
|
6731
|
-
this.renderWithLayersAndMetrics(ctx);
|
|
6732
|
-
this.markShadowCanvasDirty();
|
|
6733
|
-
this.performanceMetrics.renderTotal += performance.now() - startTime;
|
|
6734
|
-
if (this.performanceMetrics.frameCount % 100 === 0) {
|
|
6735
|
-
this.logPerformanceMetrics();
|
|
6736
|
-
}
|
|
6737
|
-
} catch (error) {
|
|
6738
|
-
ErrorHandler.logError(error);
|
|
6739
|
-
throw new RenderError("Failed to render graph", {
|
|
6740
|
-
nodeCount: this.config.nodes.length,
|
|
6741
|
-
linkCount: this.config.links.length,
|
|
6742
|
-
originalError: error.message
|
|
6743
|
-
});
|
|
6744
|
-
}
|
|
6745
|
-
}
|
|
6746
|
-
/**
|
|
6747
|
-
* Render with transform (called during zoom/pan) with shadow canvas dirty marking (Step 6 optimization)
|
|
6748
|
-
*/
|
|
6749
|
-
renderWithTransform() {
|
|
6750
|
-
if (!this.canvasState) return;
|
|
6751
|
-
try {
|
|
6752
|
-
const { canvas, ctx } = this.canvasState;
|
|
6753
|
-
const transform2 = transform(canvas);
|
|
6754
|
-
this.clearCanvas(ctx);
|
|
6755
|
-
this.applyTransform(transform2, ctx);
|
|
6756
|
-
this.renderWithLayers(ctx);
|
|
6757
|
-
this.markShadowCanvasDirty();
|
|
6758
|
-
} catch (error) {
|
|
6759
|
-
ErrorHandler.logError(error);
|
|
6760
|
-
}
|
|
6761
|
-
}
|
|
6762
|
-
/**
|
|
6763
|
-
* Render shadow canvas for hit detection with throttling (Step 6 optimization)
|
|
6764
|
-
*/
|
|
6765
|
-
renderShadowCanvas() {
|
|
6766
|
-
if (!this.canvasState || !this.config) return;
|
|
6767
|
-
const now2 = Date.now();
|
|
6768
|
-
try {
|
|
6769
|
-
const { shadowCtx, canvas } = this.canvasState;
|
|
7530
|
+
const { shadowCtx, canvas } = this.config;
|
|
6770
7531
|
const transform2 = transform(canvas);
|
|
6771
7532
|
this.clearCanvas(shadowCtx);
|
|
6772
7533
|
this.applyTransform(transform2, shadowCtx);
|
|
@@ -6780,13 +7541,13 @@ var Renderer = class {
|
|
|
6780
7541
|
}
|
|
6781
7542
|
}
|
|
6782
7543
|
/**
|
|
6783
|
-
* Mark shadow canvas as dirty for next render
|
|
7544
|
+
* Mark shadow canvas as dirty for next render
|
|
6784
7545
|
*/
|
|
6785
7546
|
markShadowCanvasDirty() {
|
|
6786
7547
|
this.shadowCanvasDirty = true;
|
|
6787
7548
|
}
|
|
6788
7549
|
/**
|
|
6789
|
-
* Force shadow canvas render
|
|
7550
|
+
* Force shadow canvas render
|
|
6790
7551
|
*/
|
|
6791
7552
|
forceShadowCanvasRender() {
|
|
6792
7553
|
this.shadowCanvasDirty = true;
|
|
@@ -6794,96 +7555,53 @@ var Renderer = class {
|
|
|
6794
7555
|
this.renderShadowCanvas();
|
|
6795
7556
|
}
|
|
6796
7557
|
/**
|
|
6797
|
-
* Clear canvas context
|
|
7558
|
+
* Clear shadow canvas context
|
|
6798
7559
|
*/
|
|
6799
7560
|
clearCanvas(ctx) {
|
|
6800
|
-
if (!this.
|
|
7561
|
+
if (!this.config) return;
|
|
6801
7562
|
try {
|
|
6802
|
-
const {
|
|
7563
|
+
const { canvas } = this.config;
|
|
7564
|
+
const { width, height } = canvas;
|
|
6803
7565
|
CanvasUtils.resetTransform(ctx);
|
|
6804
7566
|
ctx.clearRect(0, 0, width, height);
|
|
6805
|
-
} catch {
|
|
6806
|
-
|
|
7567
|
+
} catch (error) {
|
|
7568
|
+
ErrorHandler.logError(error, { message: "Failed to clear shadow canvas" });
|
|
6807
7569
|
}
|
|
6808
7570
|
}
|
|
6809
7571
|
/**
|
|
6810
|
-
* Apply transform to canvas context
|
|
7572
|
+
* Apply transform to shadow canvas context
|
|
6811
7573
|
*/
|
|
6812
7574
|
applyTransform(transform2, ctx) {
|
|
6813
7575
|
try {
|
|
6814
7576
|
CanvasUtils.resetTransform(ctx);
|
|
6815
7577
|
ctx.translate(transform2.x, transform2.y);
|
|
6816
7578
|
ctx.scale(transform2.k, transform2.k);
|
|
6817
|
-
} catch {
|
|
6818
|
-
throw new RenderError("Failed to apply transform");
|
|
6819
|
-
}
|
|
6820
|
-
}
|
|
6821
|
-
/**
|
|
6822
|
-
* Get unique link ID for tracking (consistent with LinkLabelsRenderer)
|
|
6823
|
-
*/
|
|
6824
|
-
getLinkId(link) {
|
|
6825
|
-
const sourceId = typeof link.source === "string" ? link.source : link.source.id;
|
|
6826
|
-
const targetId = typeof link.target === "string" ? link.target : link.target.id;
|
|
6827
|
-
return `${sourceId}->${targetId}`;
|
|
6828
|
-
}
|
|
6829
|
-
/**
|
|
6830
|
-
* Render main canvas nodes
|
|
6831
|
-
*/
|
|
6832
|
-
renderNodes(ctx) {
|
|
6833
|
-
if (!this.config || !this.styleResolver) return;
|
|
6834
|
-
try {
|
|
6835
|
-
const { nodes } = this.config;
|
|
6836
|
-
NodesRenderer.renderWithStyleResolver(
|
|
6837
|
-
ctx,
|
|
6838
|
-
nodes,
|
|
6839
|
-
this.styleResolver,
|
|
6840
|
-
(nodeId) => this.isNodeHovered(nodeId),
|
|
6841
|
-
(nodeId) => this.isNodeSelected(nodeId),
|
|
6842
|
-
this.performanceMetrics
|
|
6843
|
-
);
|
|
6844
|
-
} catch (error) {
|
|
6845
|
-
ErrorHandler.logError(error);
|
|
6846
|
-
throw new RenderError("Failed to render nodes");
|
|
6847
|
-
}
|
|
6848
|
-
}
|
|
6849
|
-
/**
|
|
6850
|
-
* Render node labels
|
|
6851
|
-
*/
|
|
6852
|
-
renderNodeLabels(ctx) {
|
|
6853
|
-
if (!this.config || !this.styleResolver) return;
|
|
6854
|
-
try {
|
|
6855
|
-
const { nodes } = this.config;
|
|
6856
|
-
const defaultNodeStyle = this.styleResolver.resolveNodeStyle({
|
|
6857
|
-
node: { id: "temp" }
|
|
6858
|
-
});
|
|
6859
|
-
NodeLabelsRenderer.render(ctx, nodes, defaultNodeStyle.radius);
|
|
6860
7579
|
} catch (error) {
|
|
6861
|
-
ErrorHandler.logError(error);
|
|
6862
|
-
throw new RenderError("Failed to render node labels");
|
|
7580
|
+
ErrorHandler.logError(error, { message: "Failed to apply transform to shadow canvas" });
|
|
6863
7581
|
}
|
|
6864
7582
|
}
|
|
6865
7583
|
/**
|
|
6866
|
-
* Render shadow links with __indexColor
|
|
7584
|
+
* Render shadow links with __indexColor for hit detection
|
|
6867
7585
|
*/
|
|
6868
7586
|
renderShadowLinks(shadowCtx) {
|
|
6869
|
-
if (!this.config
|
|
7587
|
+
if (!this.config) return;
|
|
6870
7588
|
try {
|
|
6871
|
-
const { links } = this.config;
|
|
6872
|
-
const defaultLinkStyle =
|
|
7589
|
+
const { links, stateManager, styleResolver, linkHoverPrecision = 4 } = this.config;
|
|
7590
|
+
const defaultLinkStyle = styleResolver.resolveLinkStyle({
|
|
6873
7591
|
link: { source: "", target: "" }
|
|
6874
7592
|
});
|
|
6875
7593
|
for (const link of links) {
|
|
6876
7594
|
if (!link.__indexColor) continue;
|
|
6877
|
-
const sourceNode = typeof link.source === "string" ?
|
|
6878
|
-
const targetNode = typeof link.target === "string" ?
|
|
6879
|
-
if (sourceNode && targetNode && sourceNode.x && sourceNode.y && targetNode.x && targetNode.y && link.__indexColorRGB) {
|
|
7595
|
+
const sourceNode = typeof link.source === "string" ? stateManager.getNode(link.source) : link.source;
|
|
7596
|
+
const targetNode = typeof link.target === "string" ? stateManager.getNode(link.target) : link.target;
|
|
7597
|
+
if (sourceNode && targetNode && sourceNode.x != null && sourceNode.y != null && targetNode.x != null && targetNode.y != null && link.__indexColorRGB) {
|
|
6880
7598
|
const [r, g, b] = link.__indexColorRGB;
|
|
6881
7599
|
const rgbColor = `rgb(${r},${g},${b})`;
|
|
6882
7600
|
const dx = targetNode.x - sourceNode.x;
|
|
6883
7601
|
const dy = targetNode.y - sourceNode.y;
|
|
6884
7602
|
const length = Math.sqrt(dx * dx + dy * dy);
|
|
6885
7603
|
const angle = Math.atan2(dy, dx);
|
|
6886
|
-
const thickness = defaultLinkStyle.strokeWidth +
|
|
7604
|
+
const thickness = defaultLinkStyle.strokeWidth + linkHoverPrecision;
|
|
6887
7605
|
shadowCtx.save();
|
|
6888
7606
|
shadowCtx.translate(sourceNode.x, sourceNode.y);
|
|
6889
7607
|
shadowCtx.rotate(angle);
|
|
@@ -6900,27 +7618,30 @@ var Renderer = class {
|
|
|
6900
7618
|
* Render shadow link labels for hit detection
|
|
6901
7619
|
*/
|
|
6902
7620
|
renderShadowLinkLabels(shadowCtx) {
|
|
6903
|
-
if (!this.config
|
|
7621
|
+
if (!this.config) return;
|
|
6904
7622
|
try {
|
|
7623
|
+
const { links, styleResolver } = this.config;
|
|
6905
7624
|
const linkStateCache = /* @__PURE__ */ new Map();
|
|
6906
7625
|
const linkIdToLinkMap = /* @__PURE__ */ new Map();
|
|
6907
|
-
for (const link of
|
|
7626
|
+
for (const link of links) {
|
|
6908
7627
|
const linkId = this.getLinkId(link);
|
|
6909
7628
|
linkIdToLinkMap.set(linkId, link);
|
|
6910
7629
|
}
|
|
6911
7630
|
const labelPositions = LinkLabelsRenderer.calculateLabelPositions(
|
|
6912
|
-
|
|
7631
|
+
links,
|
|
6913
7632
|
(link) => {
|
|
6914
7633
|
const linkId = this.getLinkId(link);
|
|
6915
7634
|
let linkState = linkStateCache.get(linkId);
|
|
6916
7635
|
if (!linkState) {
|
|
6917
7636
|
linkState = {
|
|
6918
|
-
isHovered:
|
|
6919
|
-
|
|
7637
|
+
isHovered: false,
|
|
7638
|
+
// For hit detection, we don't need actual hover state
|
|
7639
|
+
isSelected: false
|
|
7640
|
+
// For hit detection, we don't need actual selection state
|
|
6920
7641
|
};
|
|
6921
7642
|
linkStateCache.set(linkId, linkState);
|
|
6922
7643
|
}
|
|
6923
|
-
const style =
|
|
7644
|
+
const style = styleResolver.resolveLinkStyle({
|
|
6924
7645
|
link,
|
|
6925
7646
|
isHovered: linkState.isHovered,
|
|
6926
7647
|
isSelected: linkState.isSelected
|
|
@@ -6928,34 +7649,10 @@ var Renderer = class {
|
|
|
6928
7649
|
return style.label || null;
|
|
6929
7650
|
},
|
|
6930
7651
|
(link) => this.getLinkMidpoint(link),
|
|
6931
|
-
(
|
|
6932
|
-
|
|
6933
|
-
|
|
6934
|
-
|
|
6935
|
-
if (link) {
|
|
6936
|
-
linkState = {
|
|
6937
|
-
isHovered: this.isLinkHovered(link),
|
|
6938
|
-
isSelected: this.isLinkSelected(link)
|
|
6939
|
-
};
|
|
6940
|
-
linkStateCache.set(linkId, linkState);
|
|
6941
|
-
}
|
|
6942
|
-
}
|
|
6943
|
-
return linkState?.isHovered || false;
|
|
6944
|
-
},
|
|
6945
|
-
(linkId) => {
|
|
6946
|
-
let linkState = linkStateCache.get(linkId);
|
|
6947
|
-
if (!linkState) {
|
|
6948
|
-
const link = linkIdToLinkMap.get(linkId);
|
|
6949
|
-
if (link) {
|
|
6950
|
-
linkState = {
|
|
6951
|
-
isHovered: this.isLinkHovered(link),
|
|
6952
|
-
isSelected: this.isLinkSelected(link)
|
|
6953
|
-
};
|
|
6954
|
-
linkStateCache.set(linkId, linkState);
|
|
6955
|
-
}
|
|
6956
|
-
}
|
|
6957
|
-
return linkState?.isSelected || false;
|
|
6958
|
-
}
|
|
7652
|
+
(_linkId) => false,
|
|
7653
|
+
// isHovered - not needed for hit detection
|
|
7654
|
+
(_linkId) => false
|
|
7655
|
+
// isSelected - not needed for hit detection
|
|
6959
7656
|
);
|
|
6960
7657
|
for (const [linkId, position] of labelPositions) {
|
|
6961
7658
|
const link = linkIdToLinkMap.get(linkId);
|
|
@@ -6971,13 +7668,13 @@ var Renderer = class {
|
|
|
6971
7668
|
}
|
|
6972
7669
|
}
|
|
6973
7670
|
/**
|
|
6974
|
-
* Render shadow nodes with __indexColor
|
|
7671
|
+
* Render shadow nodes with __indexColor for hit detection
|
|
6975
7672
|
*/
|
|
6976
7673
|
renderShadowNodes(shadowCtx) {
|
|
6977
|
-
if (!this.config
|
|
7674
|
+
if (!this.config) return;
|
|
6978
7675
|
try {
|
|
6979
|
-
const { nodes } = this.config;
|
|
6980
|
-
const defaultNodeStyle =
|
|
7676
|
+
const { nodes, styleResolver } = this.config;
|
|
7677
|
+
const defaultNodeStyle = styleResolver.resolveNodeStyle({
|
|
6981
7678
|
node: { id: "temp" }
|
|
6982
7679
|
});
|
|
6983
7680
|
NodesRenderer.renderShadow(
|
|
@@ -6990,68 +7687,98 @@ var Renderer = class {
|
|
|
6990
7687
|
}
|
|
6991
7688
|
}
|
|
6992
7689
|
/**
|
|
6993
|
-
* Get
|
|
7690
|
+
* Get unique link ID for tracking (delegates to StateManager)
|
|
6994
7691
|
*/
|
|
6995
|
-
|
|
6996
|
-
|
|
6997
|
-
const hoverState = this.hoverManager.getHoverState();
|
|
6998
|
-
if (!hoverState.currentHovered || hoverState.currentHovered.d.entityType !== "Node") {
|
|
6999
|
-
return null;
|
|
7000
|
-
}
|
|
7001
|
-
const hoveredNode = hoverState.currentHovered.d;
|
|
7002
|
-
return hoveredNode ? hoveredNode.id : null;
|
|
7692
|
+
getLinkId(link) {
|
|
7693
|
+
return this.stateManager.getLinkId(link);
|
|
7003
7694
|
}
|
|
7004
7695
|
/**
|
|
7005
|
-
*
|
|
7696
|
+
* Calculate midpoint of a link for label positioning (delegates to StateManager)
|
|
7006
7697
|
*/
|
|
7007
|
-
|
|
7008
|
-
|
|
7009
|
-
const selectionState = this.selectionManager.getSelectionState();
|
|
7010
|
-
return selectionState.selectedNode?.id || null;
|
|
7698
|
+
getLinkMidpoint(link) {
|
|
7699
|
+
return this.stateManager.getLinkMidpoint(link);
|
|
7011
7700
|
}
|
|
7012
7701
|
/**
|
|
7013
|
-
*
|
|
7702
|
+
* Update configuration
|
|
7014
7703
|
*/
|
|
7015
|
-
|
|
7016
|
-
if (
|
|
7017
|
-
|
|
7018
|
-
if (!hoverState.currentHovered || hoverState.currentHovered.d.entityType !== "Link") {
|
|
7019
|
-
return null;
|
|
7704
|
+
updateConfig(updates) {
|
|
7705
|
+
if (this.config) {
|
|
7706
|
+
Object.assign(this.config, updates);
|
|
7020
7707
|
}
|
|
7021
|
-
return hoverState.currentHovered.d;
|
|
7022
7708
|
}
|
|
7023
7709
|
/**
|
|
7024
|
-
*
|
|
7710
|
+
* Debug shadow canvas export
|
|
7025
7711
|
*/
|
|
7026
|
-
|
|
7027
|
-
|
|
7028
|
-
|
|
7029
|
-
|
|
7712
|
+
debugShadowCanvas() {
|
|
7713
|
+
try {
|
|
7714
|
+
if (!this.config) return;
|
|
7715
|
+
const { shadowCtx } = this.config;
|
|
7716
|
+
const shadowCanvas = shadowCtx.canvas;
|
|
7717
|
+
const link = document.createElement("a");
|
|
7718
|
+
link.download = "shadow-canvas-debug.png";
|
|
7719
|
+
link.href = shadowCanvas.toDataURL("image/png");
|
|
7720
|
+
link.click();
|
|
7721
|
+
} catch (error) {
|
|
7722
|
+
ErrorHandler.logError(error);
|
|
7723
|
+
}
|
|
7030
7724
|
}
|
|
7031
7725
|
/**
|
|
7032
|
-
*
|
|
7726
|
+
* Get hit detection stats
|
|
7033
7727
|
*/
|
|
7034
|
-
|
|
7035
|
-
|
|
7036
|
-
|
|
7037
|
-
|
|
7038
|
-
|
|
7039
|
-
|
|
7040
|
-
console.log("\u23F1\uFE0F Total render:", (this.performanceMetrics.renderTotal / frames).toFixed(2), "ms");
|
|
7041
|
-
console.log("\u{1F517} Links render:", (this.performanceMetrics.renderLinks / frames).toFixed(2), "ms");
|
|
7042
|
-
console.log("\u{1F3F7}\uFE0F Link labels:", (this.performanceMetrics.renderLinkLabels / frames).toFixed(2), "ms");
|
|
7043
|
-
console.log("\u2B55 Nodes render:", (this.performanceMetrics.renderNodes / frames).toFixed(2), "ms");
|
|
7044
|
-
console.log("\u{1F4DD} Node labels:", (this.performanceMetrics.renderNodeLabels / frames).toFixed(2), "ms");
|
|
7045
|
-
console.log("\u{1F3A8} Style resolution:", (this.performanceMetrics.styleResolution / frames).toFixed(2), "ms");
|
|
7046
|
-
console.log("\u{1F446} Hover checks:", (this.performanceMetrics.hoverChecks / frames).toFixed(2), "ms");
|
|
7047
|
-
console.log("\u{1F5BC}\uFE0F Canvas calls:", (this.performanceMetrics.canvasCalls / frames).toFixed(2), "ms");
|
|
7048
|
-
console.log("---");
|
|
7728
|
+
getStats() {
|
|
7729
|
+
return {
|
|
7730
|
+
shadowCanvasDirty: this.shadowCanvasDirty,
|
|
7731
|
+
throttleRate: this.SHADOW_RENDER_THROTTLE,
|
|
7732
|
+
lastRenderTime: this.lastShadowRenderTime
|
|
7733
|
+
};
|
|
7049
7734
|
}
|
|
7050
7735
|
/**
|
|
7051
|
-
*
|
|
7736
|
+
* Destroy and clean up
|
|
7052
7737
|
*/
|
|
7053
|
-
|
|
7054
|
-
this.
|
|
7738
|
+
destroy() {
|
|
7739
|
+
this.config = void 0;
|
|
7740
|
+
this.stateManager = void 0;
|
|
7741
|
+
this.shadowCanvasDirty = false;
|
|
7742
|
+
this.lastShadowRenderTime = 0;
|
|
7743
|
+
}
|
|
7744
|
+
};
|
|
7745
|
+
|
|
7746
|
+
// src/v2/rendering/performance-metrics-manager.ts
|
|
7747
|
+
var PerformanceMetricsManager = class {
|
|
7748
|
+
metrics = {
|
|
7749
|
+
renderTotal: 0,
|
|
7750
|
+
renderNodes: 0,
|
|
7751
|
+
renderLinks: 0,
|
|
7752
|
+
renderLinkLabels: 0,
|
|
7753
|
+
renderNodeLabels: 0,
|
|
7754
|
+
styleResolution: 0,
|
|
7755
|
+
hoverChecks: 0,
|
|
7756
|
+
canvasCalls: 0,
|
|
7757
|
+
frameCount: 0
|
|
7758
|
+
};
|
|
7759
|
+
/**
|
|
7760
|
+
* Increment frame count
|
|
7761
|
+
*/
|
|
7762
|
+
incrementFrame() {
|
|
7763
|
+
this.metrics.frameCount++;
|
|
7764
|
+
}
|
|
7765
|
+
/**
|
|
7766
|
+
* Add timing for a specific metric
|
|
7767
|
+
*/
|
|
7768
|
+
addTiming(metric, time) {
|
|
7769
|
+
this.metrics[metric] += time;
|
|
7770
|
+
}
|
|
7771
|
+
/**
|
|
7772
|
+
* Get current metrics (copy to prevent mutation)
|
|
7773
|
+
*/
|
|
7774
|
+
getMetrics() {
|
|
7775
|
+
return { ...this.metrics };
|
|
7776
|
+
}
|
|
7777
|
+
/**
|
|
7778
|
+
* Reset all metrics
|
|
7779
|
+
*/
|
|
7780
|
+
reset() {
|
|
7781
|
+
this.metrics = {
|
|
7055
7782
|
renderTotal: 0,
|
|
7056
7783
|
renderNodes: 0,
|
|
7057
7784
|
renderLinks: 0,
|
|
@@ -7064,69 +7791,204 @@ var Renderer = class {
|
|
|
7064
7791
|
};
|
|
7065
7792
|
}
|
|
7066
7793
|
/**
|
|
7067
|
-
*
|
|
7068
|
-
*/
|
|
7069
|
-
getPerformanceMetrics() {
|
|
7070
|
-
return { ...this.performanceMetrics };
|
|
7071
|
-
}
|
|
7072
|
-
/**
|
|
7073
|
-
* Force log performance metrics immediately (for debugging)
|
|
7794
|
+
* Log performance metrics for analysis
|
|
7074
7795
|
*/
|
|
7075
|
-
|
|
7076
|
-
this.
|
|
7796
|
+
logMetrics(nodeCount, linkCount) {
|
|
7797
|
+
const frames = this.metrics.frameCount;
|
|
7798
|
+
console.log("\u{1F50D} PERFORMANCE METRICS (avg per frame over", frames, "frames):");
|
|
7799
|
+
console.log("\u{1F4CA} Graph size:", nodeCount, "nodes,", linkCount, "links");
|
|
7800
|
+
console.log("\u23F1\uFE0F Total render:", (this.metrics.renderTotal / frames).toFixed(2), "ms");
|
|
7801
|
+
console.log("\u{1F517} Links render:", (this.metrics.renderLinks / frames).toFixed(2), "ms");
|
|
7802
|
+
console.log("\u{1F3F7}\uFE0F Link labels:", (this.metrics.renderLinkLabels / frames).toFixed(2), "ms");
|
|
7803
|
+
console.log("\u2B55 Nodes render:", (this.metrics.renderNodes / frames).toFixed(2), "ms");
|
|
7804
|
+
console.log("\u{1F4DD} Node labels:", (this.metrics.renderNodeLabels / frames).toFixed(2), "ms");
|
|
7805
|
+
console.log("\u{1F3A8} Style resolution:", (this.metrics.styleResolution / frames).toFixed(2), "ms");
|
|
7806
|
+
console.log("\u{1F446} Hover checks:", (this.metrics.hoverChecks / frames).toFixed(2), "ms");
|
|
7807
|
+
console.log("\u{1F5BC}\uFE0F Canvas calls:", (this.metrics.canvasCalls / frames).toFixed(2), "ms");
|
|
7808
|
+
console.log("---");
|
|
7077
7809
|
}
|
|
7078
7810
|
/**
|
|
7079
|
-
* Check if
|
|
7811
|
+
* Check if it's time to log metrics (every N frames)
|
|
7080
7812
|
*/
|
|
7081
|
-
|
|
7082
|
-
|
|
7083
|
-
const hoverState = this.hoverManager.getHoverState();
|
|
7084
|
-
if (!hoverState.currentHovered || hoverState.currentHovered.d.entityType !== "Node") {
|
|
7085
|
-
return false;
|
|
7086
|
-
}
|
|
7087
|
-
const hoveredNode = hoverState.currentHovered.d;
|
|
7088
|
-
return hoveredNode && hoveredNode.id === nodeId;
|
|
7813
|
+
shouldLogMetrics(intervalFrames = 100) {
|
|
7814
|
+
return this.metrics.frameCount % intervalFrames === 0 && this.metrics.frameCount > 0;
|
|
7089
7815
|
}
|
|
7816
|
+
};
|
|
7817
|
+
|
|
7818
|
+
// src/v2/rendering/link-renderer.ts
|
|
7819
|
+
var LinkRenderer = class {
|
|
7090
7820
|
/**
|
|
7091
|
-
*
|
|
7821
|
+
* Render a directed link with optional arrow head
|
|
7092
7822
|
*/
|
|
7093
|
-
|
|
7094
|
-
|
|
7095
|
-
|
|
7096
|
-
|
|
7097
|
-
|
|
7098
|
-
|
|
7099
|
-
|
|
7100
|
-
|
|
7101
|
-
|
|
7102
|
-
|
|
7103
|
-
const
|
|
7104
|
-
|
|
7105
|
-
|
|
7106
|
-
|
|
7107
|
-
|
|
7108
|
-
|
|
7109
|
-
|
|
7110
|
-
|
|
7111
|
-
|
|
7823
|
+
static renderDirectedLink(ctx, source, target, style, styleResolver, isNodeHovered, isNodeSelected) {
|
|
7824
|
+
try {
|
|
7825
|
+
const sourcePoint = this.getShortenedSourcePoint(
|
|
7826
|
+
source,
|
|
7827
|
+
target,
|
|
7828
|
+
style,
|
|
7829
|
+
styleResolver,
|
|
7830
|
+
isNodeHovered,
|
|
7831
|
+
isNodeSelected
|
|
7832
|
+
);
|
|
7833
|
+
const targetPoint = this.getShortenedTargetPoint(
|
|
7834
|
+
source,
|
|
7835
|
+
target,
|
|
7836
|
+
style,
|
|
7837
|
+
styleResolver,
|
|
7838
|
+
isNodeHovered,
|
|
7839
|
+
isNodeSelected
|
|
7840
|
+
);
|
|
7841
|
+
ctx.strokeStyle = style.stroke;
|
|
7842
|
+
ctx.lineWidth = style.strokeWidth;
|
|
7843
|
+
ctx.globalAlpha = style.opacity;
|
|
7844
|
+
ctx.beginPath();
|
|
7845
|
+
ctx.moveTo(sourcePoint.x, sourcePoint.y);
|
|
7846
|
+
ctx.lineTo(targetPoint.x, targetPoint.y);
|
|
7847
|
+
ctx.stroke();
|
|
7848
|
+
if (style.arrow?.enabled) {
|
|
7849
|
+
this.renderArrow(ctx, sourcePoint, targetPoint, style.arrow);
|
|
7850
|
+
}
|
|
7851
|
+
ctx.globalAlpha = 1;
|
|
7852
|
+
} catch (error) {
|
|
7853
|
+
ErrorHandler.logError(error);
|
|
7112
7854
|
}
|
|
7113
|
-
return false;
|
|
7114
7855
|
}
|
|
7115
7856
|
/**
|
|
7116
|
-
*
|
|
7857
|
+
* V1-compatible link shortening for source point
|
|
7117
7858
|
*/
|
|
7118
|
-
isNodeSelected
|
|
7119
|
-
|
|
7120
|
-
const
|
|
7121
|
-
|
|
7859
|
+
static getShortenedSourcePoint(source, target, _style, styleResolver, isNodeHovered, isNodeSelected) {
|
|
7860
|
+
const sourceX = source.x ?? 0;
|
|
7861
|
+
const sourceY = source.y ?? 0;
|
|
7862
|
+
const targetX = target.x ?? 0;
|
|
7863
|
+
const targetY = target.y ?? 0;
|
|
7864
|
+
const dx = targetX - sourceX;
|
|
7865
|
+
const dy = targetY - sourceY;
|
|
7866
|
+
const distance = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
7867
|
+
const sourceNodeStyle = styleResolver.resolveNodeStyle({
|
|
7868
|
+
node: source,
|
|
7869
|
+
isHovered: isNodeHovered(source.id),
|
|
7870
|
+
isSelected: isNodeSelected(source.id)
|
|
7871
|
+
});
|
|
7872
|
+
const visualRadius = sourceNodeStyle.radius + sourceNodeStyle.strokeWidth / 2;
|
|
7873
|
+
const offset = visualRadius + 1;
|
|
7874
|
+
return {
|
|
7875
|
+
x: sourceX + dx / distance * offset,
|
|
7876
|
+
y: sourceY + dy / distance * offset
|
|
7877
|
+
};
|
|
7122
7878
|
}
|
|
7123
7879
|
/**
|
|
7124
|
-
*
|
|
7880
|
+
* V1-compatible link shortening for target point
|
|
7125
7881
|
*/
|
|
7126
|
-
|
|
7127
|
-
|
|
7128
|
-
const
|
|
7129
|
-
|
|
7882
|
+
static getShortenedTargetPoint(source, target, style, styleResolver, isNodeHovered, isNodeSelected) {
|
|
7883
|
+
const sourceX = source.x ?? 0;
|
|
7884
|
+
const sourceY = source.y ?? 0;
|
|
7885
|
+
const targetX = target.x ?? 0;
|
|
7886
|
+
const targetY = target.y ?? 0;
|
|
7887
|
+
const dx = targetX - sourceX;
|
|
7888
|
+
const dy = targetY - sourceY;
|
|
7889
|
+
const distance = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
7890
|
+
const targetNodeStyle = styleResolver.resolveNodeStyle({
|
|
7891
|
+
node: target,
|
|
7892
|
+
isHovered: isNodeHovered(target.id),
|
|
7893
|
+
isSelected: isNodeSelected(target.id)
|
|
7894
|
+
});
|
|
7895
|
+
const visualRadius = targetNodeStyle.radius + targetNodeStyle.strokeWidth / 2;
|
|
7896
|
+
const arrowLength = style.arrow?.enabled ? style.arrow.size ?? 4 : 0;
|
|
7897
|
+
const offset = style.arrow?.enabled ? visualRadius + arrowLength : visualRadius + 1;
|
|
7898
|
+
const shortenedPoint = {
|
|
7899
|
+
x: targetX - dx / distance * offset,
|
|
7900
|
+
y: targetY - dy / distance * offset
|
|
7901
|
+
};
|
|
7902
|
+
return shortenedPoint;
|
|
7903
|
+
}
|
|
7904
|
+
/**
|
|
7905
|
+
* Render arrow head at specific points
|
|
7906
|
+
*/
|
|
7907
|
+
static renderArrow(ctx, sourcePoint, targetPoint, arrowStyle) {
|
|
7908
|
+
try {
|
|
7909
|
+
const dx = targetPoint.x - sourcePoint.x;
|
|
7910
|
+
const dy = targetPoint.y - sourcePoint.y;
|
|
7911
|
+
const angle = Math.atan2(dy, dx);
|
|
7912
|
+
const arrowLength = arrowStyle.size ?? 4;
|
|
7913
|
+
const arrowTipX = targetPoint.x + arrowLength * Math.cos(angle);
|
|
7914
|
+
const arrowTipY = targetPoint.y + arrowLength * Math.sin(angle);
|
|
7915
|
+
const x1 = arrowTipX - arrowLength * Math.cos(angle - Math.PI / 6);
|
|
7916
|
+
const y1 = arrowTipY - arrowLength * Math.sin(angle - Math.PI / 6);
|
|
7917
|
+
const x22 = arrowTipX - arrowLength * Math.cos(angle + Math.PI / 6);
|
|
7918
|
+
const y22 = arrowTipY - arrowLength * Math.sin(angle + Math.PI / 6);
|
|
7919
|
+
ctx.fillStyle = arrowStyle.fill ?? "#000000";
|
|
7920
|
+
ctx.beginPath();
|
|
7921
|
+
ctx.moveTo(arrowTipX, arrowTipY);
|
|
7922
|
+
ctx.lineTo(x1, y1);
|
|
7923
|
+
ctx.lineTo(x22, y22);
|
|
7924
|
+
ctx.closePath();
|
|
7925
|
+
ctx.fill();
|
|
7926
|
+
} catch (error) {
|
|
7927
|
+
ErrorHandler.logError(error);
|
|
7928
|
+
}
|
|
7929
|
+
}
|
|
7930
|
+
};
|
|
7931
|
+
|
|
7932
|
+
// src/v2/rendering/interaction-state-resolver.ts
|
|
7933
|
+
var InteractionStateResolver = class {
|
|
7934
|
+
constructor(hoverManager, selectionManager) {
|
|
7935
|
+
this.hoverManager = hoverManager;
|
|
7936
|
+
this.selectionManager = selectionManager;
|
|
7937
|
+
}
|
|
7938
|
+
hoverManager;
|
|
7939
|
+
selectionManager;
|
|
7940
|
+
/**
|
|
7941
|
+
* Check if a node is currently hovered
|
|
7942
|
+
*/
|
|
7943
|
+
isNodeHovered(nodeId) {
|
|
7944
|
+
if (!this.hoverManager) return false;
|
|
7945
|
+
const hoverState = this.hoverManager.getHoverState();
|
|
7946
|
+
if (!hoverState.currentHovered || hoverState.currentHovered.d.entityType !== "Node") {
|
|
7947
|
+
return false;
|
|
7948
|
+
}
|
|
7949
|
+
const hoveredNode = hoverState.currentHovered.d;
|
|
7950
|
+
return hoveredNode && hoveredNode.id === nodeId;
|
|
7951
|
+
}
|
|
7952
|
+
/**
|
|
7953
|
+
* Check if a link is currently hovered (either directly or through associated node hover)
|
|
7954
|
+
*/
|
|
7955
|
+
isLinkHovered(link) {
|
|
7956
|
+
if (!this.hoverManager) return false;
|
|
7957
|
+
const hoverState = this.hoverManager.getHoverState();
|
|
7958
|
+
if (!hoverState.currentHovered) return false;
|
|
7959
|
+
if (hoverState.currentHovered.d.entityType === "Link") {
|
|
7960
|
+
const hoveredLink = hoverState.currentHovered.d;
|
|
7961
|
+
if (!hoveredLink) return false;
|
|
7962
|
+
const sourceId1 = typeof link.source === "string" ? link.source : link.source.id;
|
|
7963
|
+
const targetId1 = typeof link.target === "string" ? link.target : link.target.id;
|
|
7964
|
+
const sourceId2 = typeof hoveredLink.source === "string" ? hoveredLink.source : hoveredLink.source.id;
|
|
7965
|
+
const targetId2 = typeof hoveredLink.target === "string" ? hoveredLink.target : hoveredLink.target.id;
|
|
7966
|
+
return sourceId1 === sourceId2 && targetId1 === targetId2;
|
|
7967
|
+
}
|
|
7968
|
+
if (hoverState.currentHovered.d.entityType === "Node") {
|
|
7969
|
+
const hoveredNode = hoverState.currentHovered.d;
|
|
7970
|
+
if (!hoveredNode) return false;
|
|
7971
|
+
const linkSourceId = typeof link.source === "string" ? link.source : link.source.id;
|
|
7972
|
+
const linkTargetId = typeof link.target === "string" ? link.target : link.target.id;
|
|
7973
|
+
return hoveredNode.id === linkSourceId || hoveredNode.id === linkTargetId;
|
|
7974
|
+
}
|
|
7975
|
+
return false;
|
|
7976
|
+
}
|
|
7977
|
+
/**
|
|
7978
|
+
* Check if a node is currently selected
|
|
7979
|
+
*/
|
|
7980
|
+
isNodeSelected(nodeId) {
|
|
7981
|
+
if (!this.selectionManager) return false;
|
|
7982
|
+
const selectionState = this.selectionManager.getSelectionState();
|
|
7983
|
+
return selectionState.selectedNode?.id === nodeId;
|
|
7984
|
+
}
|
|
7985
|
+
/**
|
|
7986
|
+
* Check if a link is currently selected
|
|
7987
|
+
*/
|
|
7988
|
+
isLinkSelected(link) {
|
|
7989
|
+
if (!this.selectionManager) return false;
|
|
7990
|
+
const selectionState = this.selectionManager.getSelectionState();
|
|
7991
|
+
if (!selectionState.selectedLink) return false;
|
|
7130
7992
|
const sourceId1 = typeof link.source === "string" ? link.source : link.source.id;
|
|
7131
7993
|
const targetId1 = typeof link.target === "string" ? link.target : link.target.id;
|
|
7132
7994
|
const selectedLink = selectionState.selectedLink;
|
|
@@ -7135,151 +7997,857 @@ var Renderer = class {
|
|
|
7135
7997
|
return sourceId1 === sourceId2 && targetId1 === targetId2;
|
|
7136
7998
|
}
|
|
7137
7999
|
/**
|
|
7138
|
-
*
|
|
7139
|
-
* (either the link itself is selected, or its connected node is selected)
|
|
8000
|
+
* Get interaction state for a node (optimized for caching)
|
|
7140
8001
|
*/
|
|
7141
|
-
|
|
7142
|
-
|
|
7143
|
-
|
|
7144
|
-
|
|
7145
|
-
|
|
7146
|
-
|
|
7147
|
-
|
|
7148
|
-
|
|
7149
|
-
|
|
7150
|
-
|
|
7151
|
-
|
|
8002
|
+
getNodeState(nodeId) {
|
|
8003
|
+
return {
|
|
8004
|
+
isHovered: this.isNodeHovered(nodeId),
|
|
8005
|
+
isSelected: this.isNodeSelected(nodeId)
|
|
8006
|
+
};
|
|
8007
|
+
}
|
|
8008
|
+
/**
|
|
8009
|
+
* Get interaction state for a link (optimized for caching)
|
|
8010
|
+
*/
|
|
8011
|
+
getLinkState(link) {
|
|
8012
|
+
return {
|
|
8013
|
+
isHovered: this.isLinkHovered(link),
|
|
8014
|
+
isSelected: this.isLinkSelected(link)
|
|
8015
|
+
};
|
|
8016
|
+
}
|
|
8017
|
+
/**
|
|
8018
|
+
* Create callback functions for external use (useful for renderers)
|
|
8019
|
+
*/
|
|
8020
|
+
createCallbacks() {
|
|
8021
|
+
return {
|
|
8022
|
+
isNodeHovered: (nodeId) => this.isNodeHovered(nodeId),
|
|
8023
|
+
isNodeSelected: (nodeId) => this.isNodeSelected(nodeId),
|
|
8024
|
+
isLinkHovered: (link) => this.isLinkHovered(link),
|
|
8025
|
+
isLinkSelected: (link) => this.isLinkSelected(link)
|
|
8026
|
+
};
|
|
8027
|
+
}
|
|
8028
|
+
/**
|
|
8029
|
+
* Update the managers (useful for re-initialization)
|
|
8030
|
+
*/
|
|
8031
|
+
updateManagers(hoverManager, selectionManager) {
|
|
8032
|
+
this.hoverManager = hoverManager;
|
|
8033
|
+
this.selectionManager = selectionManager;
|
|
8034
|
+
}
|
|
8035
|
+
};
|
|
8036
|
+
|
|
8037
|
+
// src/v2/utils/object-pool.ts
|
|
8038
|
+
var ObjectPool = class {
|
|
8039
|
+
pool = [];
|
|
8040
|
+
createFn;
|
|
8041
|
+
resetFn;
|
|
8042
|
+
maxSize;
|
|
8043
|
+
constructor(createFn, resetFn, maxSize = 1e3) {
|
|
8044
|
+
this.createFn = createFn;
|
|
8045
|
+
this.resetFn = resetFn;
|
|
8046
|
+
this.maxSize = maxSize;
|
|
8047
|
+
}
|
|
8048
|
+
/**
|
|
8049
|
+
* Get object from pool or create new one
|
|
8050
|
+
*/
|
|
8051
|
+
acquire() {
|
|
8052
|
+
const obj = this.pool.pop();
|
|
8053
|
+
if (obj) {
|
|
8054
|
+
if (this.resetFn) {
|
|
8055
|
+
this.resetFn(obj);
|
|
8056
|
+
} else if (obj.reset) {
|
|
8057
|
+
obj.reset();
|
|
7152
8058
|
}
|
|
8059
|
+
return obj;
|
|
7153
8060
|
}
|
|
7154
|
-
|
|
7155
|
-
|
|
7156
|
-
|
|
7157
|
-
|
|
7158
|
-
|
|
8061
|
+
return this.createFn();
|
|
8062
|
+
}
|
|
8063
|
+
/**
|
|
8064
|
+
* Return object to pool for reuse
|
|
8065
|
+
*/
|
|
8066
|
+
release(obj) {
|
|
8067
|
+
if (this.pool.length < this.maxSize) {
|
|
8068
|
+
this.pool.push(obj);
|
|
7159
8069
|
}
|
|
7160
|
-
return false;
|
|
7161
8070
|
}
|
|
7162
8071
|
/**
|
|
7163
|
-
*
|
|
8072
|
+
* Pre-warm pool with initial objects
|
|
7164
8073
|
*/
|
|
7165
|
-
|
|
7166
|
-
|
|
7167
|
-
|
|
7168
|
-
const targetNode = typeof link.target === "string" ? this.nodeMap.get(link.target) : link.target;
|
|
7169
|
-
if (!sourceNode || !targetNode || sourceNode.x === void 0 || sourceNode.y === void 0 || targetNode.x === void 0 || targetNode.y === void 0) {
|
|
7170
|
-
return null;
|
|
8074
|
+
prewarm(count) {
|
|
8075
|
+
for (let i = 0; i < count; i++) {
|
|
8076
|
+
this.pool.push(this.createFn());
|
|
7171
8077
|
}
|
|
8078
|
+
}
|
|
8079
|
+
/**
|
|
8080
|
+
* Get pool statistics
|
|
8081
|
+
*/
|
|
8082
|
+
getStats() {
|
|
7172
8083
|
return {
|
|
7173
|
-
|
|
7174
|
-
|
|
8084
|
+
available: this.pool.length,
|
|
8085
|
+
maxSize: this.maxSize,
|
|
8086
|
+
utilization: (this.maxSize - this.pool.length) / this.maxSize
|
|
7175
8087
|
};
|
|
7176
8088
|
}
|
|
7177
8089
|
/**
|
|
7178
|
-
*
|
|
8090
|
+
* Clear the pool
|
|
8091
|
+
*/
|
|
8092
|
+
clear() {
|
|
8093
|
+
this.pool.length = 0;
|
|
8094
|
+
}
|
|
8095
|
+
};
|
|
8096
|
+
var DragObjectPoolManager = class {
|
|
8097
|
+
vector2DPool;
|
|
8098
|
+
nodeStatePool;
|
|
8099
|
+
linkStatePool;
|
|
8100
|
+
renderContextPool;
|
|
8101
|
+
// Pre-allocated arrays to avoid GC during drag
|
|
8102
|
+
reusableNodeArray = [];
|
|
8103
|
+
reusableLinkArray = [];
|
|
8104
|
+
reusableVector2DArray = [];
|
|
8105
|
+
constructor() {
|
|
8106
|
+
this.vector2DPool = new ObjectPool(
|
|
8107
|
+
() => ({ x: 0, y: 0, reset() {
|
|
8108
|
+
this.x = 0;
|
|
8109
|
+
this.y = 0;
|
|
8110
|
+
} }),
|
|
8111
|
+
(obj) => {
|
|
8112
|
+
obj.x = 0;
|
|
8113
|
+
obj.y = 0;
|
|
8114
|
+
},
|
|
8115
|
+
500
|
|
8116
|
+
);
|
|
8117
|
+
this.nodeStatePool = new ObjectPool(
|
|
8118
|
+
() => ({
|
|
8119
|
+
id: "",
|
|
8120
|
+
x: 0,
|
|
8121
|
+
y: 0,
|
|
8122
|
+
isHovered: false,
|
|
8123
|
+
isSelected: false,
|
|
8124
|
+
reset() {
|
|
8125
|
+
this.id = "";
|
|
8126
|
+
this.x = 0;
|
|
8127
|
+
this.y = 0;
|
|
8128
|
+
this.isHovered = false;
|
|
8129
|
+
this.isSelected = false;
|
|
8130
|
+
}
|
|
8131
|
+
}),
|
|
8132
|
+
(obj) => {
|
|
8133
|
+
obj.id = "";
|
|
8134
|
+
obj.x = 0;
|
|
8135
|
+
obj.y = 0;
|
|
8136
|
+
obj.isHovered = false;
|
|
8137
|
+
obj.isSelected = false;
|
|
8138
|
+
},
|
|
8139
|
+
1e3
|
|
8140
|
+
);
|
|
8141
|
+
this.linkStatePool = new ObjectPool(
|
|
8142
|
+
() => ({
|
|
8143
|
+
sourceId: "",
|
|
8144
|
+
targetId: "",
|
|
8145
|
+
sourceX: 0,
|
|
8146
|
+
sourceY: 0,
|
|
8147
|
+
targetX: 0,
|
|
8148
|
+
targetY: 0,
|
|
8149
|
+
isHovered: false,
|
|
8150
|
+
isSelected: false,
|
|
8151
|
+
reset() {
|
|
8152
|
+
this.sourceId = "";
|
|
8153
|
+
this.targetId = "";
|
|
8154
|
+
this.sourceX = 0;
|
|
8155
|
+
this.sourceY = 0;
|
|
8156
|
+
this.targetX = 0;
|
|
8157
|
+
this.targetY = 0;
|
|
8158
|
+
this.isHovered = false;
|
|
8159
|
+
this.isSelected = false;
|
|
8160
|
+
}
|
|
8161
|
+
}),
|
|
8162
|
+
(obj) => {
|
|
8163
|
+
obj.sourceId = "";
|
|
8164
|
+
obj.targetId = "";
|
|
8165
|
+
obj.sourceX = 0;
|
|
8166
|
+
obj.sourceY = 0;
|
|
8167
|
+
obj.targetX = 0;
|
|
8168
|
+
obj.targetY = 0;
|
|
8169
|
+
obj.isHovered = false;
|
|
8170
|
+
obj.isSelected = false;
|
|
8171
|
+
},
|
|
8172
|
+
2e3
|
|
8173
|
+
);
|
|
8174
|
+
this.renderContextPool = new ObjectPool(
|
|
8175
|
+
() => ({
|
|
8176
|
+
nodeStates: /* @__PURE__ */ new Map(),
|
|
8177
|
+
linkStates: /* @__PURE__ */ new Map(),
|
|
8178
|
+
tempVectors: [],
|
|
8179
|
+
reset() {
|
|
8180
|
+
this.nodeStates.clear();
|
|
8181
|
+
this.linkStates.clear();
|
|
8182
|
+
this.tempVectors.length = 0;
|
|
8183
|
+
}
|
|
8184
|
+
}),
|
|
8185
|
+
(obj) => {
|
|
8186
|
+
obj.nodeStates.clear();
|
|
8187
|
+
obj.linkStates.clear();
|
|
8188
|
+
obj.tempVectors.length = 0;
|
|
8189
|
+
},
|
|
8190
|
+
10
|
|
8191
|
+
);
|
|
8192
|
+
this.prewarmPools();
|
|
8193
|
+
}
|
|
8194
|
+
/**
|
|
8195
|
+
* Pre-warm pools with initial objects
|
|
8196
|
+
*/
|
|
8197
|
+
prewarmPools() {
|
|
8198
|
+
this.vector2DPool.prewarm(50);
|
|
8199
|
+
this.nodeStatePool.prewarm(100);
|
|
8200
|
+
this.linkStatePool.prewarm(200);
|
|
8201
|
+
this.renderContextPool.prewarm(2);
|
|
8202
|
+
}
|
|
8203
|
+
/**
|
|
8204
|
+
* Acquire temporary 2D vector
|
|
8205
|
+
*/
|
|
8206
|
+
acquireVector2D(x3 = 0, y3 = 0) {
|
|
8207
|
+
const vector = this.vector2DPool.acquire();
|
|
8208
|
+
vector.x = x3;
|
|
8209
|
+
vector.y = y3;
|
|
8210
|
+
return vector;
|
|
8211
|
+
}
|
|
8212
|
+
/**
|
|
8213
|
+
* Release 2D vector back to pool
|
|
7179
8214
|
*/
|
|
7180
|
-
|
|
8215
|
+
releaseVector2D(vector) {
|
|
8216
|
+
this.vector2DPool.release(vector);
|
|
8217
|
+
}
|
|
8218
|
+
/**
|
|
8219
|
+
* Acquire node state object
|
|
8220
|
+
*/
|
|
8221
|
+
acquireNodeState(id2, x3, y3, isHovered = false, isSelected = false) {
|
|
8222
|
+
const state = this.nodeStatePool.acquire();
|
|
8223
|
+
state.id = id2;
|
|
8224
|
+
state.x = x3;
|
|
8225
|
+
state.y = y3;
|
|
8226
|
+
state.isHovered = isHovered;
|
|
8227
|
+
state.isSelected = isSelected;
|
|
8228
|
+
return state;
|
|
8229
|
+
}
|
|
8230
|
+
/**
|
|
8231
|
+
* Release node state back to pool
|
|
8232
|
+
*/
|
|
8233
|
+
releaseNodeState(state) {
|
|
8234
|
+
this.nodeStatePool.release(state);
|
|
8235
|
+
}
|
|
8236
|
+
/**
|
|
8237
|
+
* Acquire link state object
|
|
8238
|
+
*/
|
|
8239
|
+
acquireLinkState(sourceId, targetId, sourceX, sourceY, targetX, targetY, isHovered = false, isSelected = false) {
|
|
8240
|
+
const state = this.linkStatePool.acquire();
|
|
8241
|
+
state.sourceId = sourceId;
|
|
8242
|
+
state.targetId = targetId;
|
|
8243
|
+
state.sourceX = sourceX;
|
|
8244
|
+
state.sourceY = sourceY;
|
|
8245
|
+
state.targetX = targetX;
|
|
8246
|
+
state.targetY = targetY;
|
|
8247
|
+
state.isHovered = isHovered;
|
|
8248
|
+
state.isSelected = isSelected;
|
|
8249
|
+
return state;
|
|
8250
|
+
}
|
|
8251
|
+
/**
|
|
8252
|
+
* Release link state back to pool
|
|
8253
|
+
*/
|
|
8254
|
+
releaseLinkState(state) {
|
|
8255
|
+
this.linkStatePool.release(state);
|
|
8256
|
+
}
|
|
8257
|
+
/**
|
|
8258
|
+
* Acquire render context
|
|
8259
|
+
*/
|
|
8260
|
+
acquireRenderContext() {
|
|
8261
|
+
return this.renderContextPool.acquire();
|
|
8262
|
+
}
|
|
8263
|
+
/**
|
|
8264
|
+
* Release render context back to pool
|
|
8265
|
+
*/
|
|
8266
|
+
releaseRenderContext(context) {
|
|
8267
|
+
this.renderContextPool.release(context);
|
|
8268
|
+
}
|
|
8269
|
+
/**
|
|
8270
|
+
* Get reusable node array (cleared and ready for use)
|
|
8271
|
+
*/
|
|
8272
|
+
getReusableNodeArray() {
|
|
8273
|
+
this.reusableNodeArray.length = 0;
|
|
8274
|
+
return this.reusableNodeArray;
|
|
8275
|
+
}
|
|
8276
|
+
/**
|
|
8277
|
+
* Get reusable link array (cleared and ready for use)
|
|
8278
|
+
*/
|
|
8279
|
+
getReusableLinkArray() {
|
|
8280
|
+
this.reusableLinkArray.length = 0;
|
|
8281
|
+
return this.reusableLinkArray;
|
|
8282
|
+
}
|
|
8283
|
+
/**
|
|
8284
|
+
* Get reusable vector array (cleared and ready for use)
|
|
8285
|
+
*/
|
|
8286
|
+
getReusableVector2DArray() {
|
|
8287
|
+
this.reusableVector2DArray.length = 0;
|
|
8288
|
+
return this.reusableVector2DArray;
|
|
8289
|
+
}
|
|
8290
|
+
/**
|
|
8291
|
+
* Batch acquire multiple vectors
|
|
8292
|
+
*/
|
|
8293
|
+
batchAcquireVectors(count) {
|
|
8294
|
+
const vectors = this.getReusableVector2DArray();
|
|
8295
|
+
for (let i = 0; i < count; i++) {
|
|
8296
|
+
vectors.push(this.acquireVector2D());
|
|
8297
|
+
}
|
|
8298
|
+
return vectors;
|
|
8299
|
+
}
|
|
8300
|
+
/**
|
|
8301
|
+
* Batch release multiple vectors
|
|
8302
|
+
*/
|
|
8303
|
+
batchReleaseVectors(vectors) {
|
|
8304
|
+
for (const vector of vectors) {
|
|
8305
|
+
this.releaseVector2D(vector);
|
|
8306
|
+
}
|
|
8307
|
+
}
|
|
8308
|
+
/**
|
|
8309
|
+
* Get comprehensive pool statistics
|
|
8310
|
+
*/
|
|
8311
|
+
getStats() {
|
|
8312
|
+
const vector2DStats = this.vector2DPool.getStats();
|
|
8313
|
+
const nodeStateStats = this.nodeStatePool.getStats();
|
|
8314
|
+
const linkStateStats = this.linkStatePool.getStats();
|
|
8315
|
+
const renderContextStats = this.renderContextPool.getStats();
|
|
8316
|
+
const vector2DBytes = vector2DStats.maxSize * (2 * 8);
|
|
8317
|
+
const nodeStateBytes = nodeStateStats.maxSize * (32 + 2 * 8 + 2 * 1);
|
|
8318
|
+
const linkStateBytes = linkStateStats.maxSize * (64 + 4 * 8 + 2 * 1);
|
|
8319
|
+
return {
|
|
8320
|
+
vector2D: vector2DStats,
|
|
8321
|
+
nodeState: nodeStateStats,
|
|
8322
|
+
linkState: linkStateStats,
|
|
8323
|
+
renderContext: renderContextStats,
|
|
8324
|
+
memoryEstimate: {
|
|
8325
|
+
vector2DBytes,
|
|
8326
|
+
nodeStateBytes,
|
|
8327
|
+
linkStateBytes,
|
|
8328
|
+
totalBytes: vector2DBytes + nodeStateBytes + linkStateBytes
|
|
8329
|
+
}
|
|
8330
|
+
};
|
|
8331
|
+
}
|
|
8332
|
+
/**
|
|
8333
|
+
* Force garbage collection optimization by clearing and re-prewarming pools
|
|
8334
|
+
*/
|
|
8335
|
+
optimizeMemory() {
|
|
8336
|
+
this.vector2DPool.clear();
|
|
8337
|
+
this.nodeStatePool.clear();
|
|
8338
|
+
this.linkStatePool.clear();
|
|
8339
|
+
this.renderContextPool.clear();
|
|
8340
|
+
this.reusableNodeArray.length = 0;
|
|
8341
|
+
this.reusableLinkArray.length = 0;
|
|
8342
|
+
this.reusableVector2DArray.length = 0;
|
|
8343
|
+
this.prewarmPools();
|
|
8344
|
+
}
|
|
8345
|
+
/**
|
|
8346
|
+
* Destroy and clean up all pools
|
|
8347
|
+
*/
|
|
8348
|
+
destroy() {
|
|
8349
|
+
this.vector2DPool.clear();
|
|
8350
|
+
this.nodeStatePool.clear();
|
|
8351
|
+
this.linkStatePool.clear();
|
|
8352
|
+
this.renderContextPool.clear();
|
|
8353
|
+
this.reusableNodeArray.length = 0;
|
|
8354
|
+
this.reusableLinkArray.length = 0;
|
|
8355
|
+
this.reusableVector2DArray.length = 0;
|
|
8356
|
+
}
|
|
8357
|
+
};
|
|
8358
|
+
var dragObjectPool = new DragObjectPoolManager();
|
|
8359
|
+
|
|
8360
|
+
// src/v2/rendering/drag-optimizer.ts
|
|
8361
|
+
var DragOptimizer = class {
|
|
8362
|
+
config;
|
|
8363
|
+
state = {
|
|
8364
|
+
isDragModeActive: false,
|
|
8365
|
+
lastDragRenderTime: 0
|
|
8366
|
+
};
|
|
8367
|
+
// Fast drag cache for O(1) lookups - everything pre-computed
|
|
8368
|
+
fastDragNodeCache = /* @__PURE__ */ new Map();
|
|
8369
|
+
// Object pool optimization
|
|
8370
|
+
reusableNodeStates = [];
|
|
8371
|
+
reusableVectors = [];
|
|
8372
|
+
/**
|
|
8373
|
+
* Initialize drag optimizer with object pooling optimizations
|
|
8374
|
+
*/
|
|
8375
|
+
initialize(config) {
|
|
8376
|
+
this.config = config;
|
|
8377
|
+
this.buildFastDragCache();
|
|
8378
|
+
this.reusableNodeStates = dragObjectPool.getReusableNodeArray();
|
|
8379
|
+
this.reusableVectors = dragObjectPool.getReusableVector2DArray();
|
|
8380
|
+
}
|
|
8381
|
+
/**
|
|
8382
|
+
* Set the dragged node (like zoom renderer - simple state tracking)
|
|
8383
|
+
*/
|
|
8384
|
+
setDraggedNode(draggedNode) {
|
|
8385
|
+
if (!this.config) return;
|
|
7181
8386
|
try {
|
|
7182
|
-
|
|
7183
|
-
|
|
7184
|
-
|
|
7185
|
-
|
|
7186
|
-
|
|
7187
|
-
|
|
7188
|
-
|
|
7189
|
-
|
|
7190
|
-
|
|
7191
|
-
|
|
7192
|
-
|
|
8387
|
+
this.state.draggedNodeId = draggedNode.id;
|
|
8388
|
+
console.log(`\u{1F680} Fast drag mode - rendering ALL ${this.config.nodes.length} nodes and ${this.config.links.length} links with simplified styling`);
|
|
8389
|
+
} catch (error) {
|
|
8390
|
+
ErrorHandler.logError(error);
|
|
8391
|
+
}
|
|
8392
|
+
}
|
|
8393
|
+
/**
|
|
8394
|
+
* Render with drag optimization using object pooling
|
|
8395
|
+
*/
|
|
8396
|
+
render(ctx, performanceMetrics) {
|
|
8397
|
+
if (!this.config) return;
|
|
8398
|
+
if (this.state.isDragModeActive) {
|
|
8399
|
+
this.renderFastDragOptimized(ctx);
|
|
8400
|
+
} else {
|
|
8401
|
+
this.config.zIndexRenderer.render(ctx, performanceMetrics);
|
|
8402
|
+
}
|
|
8403
|
+
}
|
|
8404
|
+
/**
|
|
8405
|
+
* Handle drag movement with RAF throttling
|
|
8406
|
+
*/
|
|
8407
|
+
handleDragMove() {
|
|
8408
|
+
if (!this.config || !this.state.isDragModeActive) return;
|
|
8409
|
+
this.state.lastDragRenderTime = performance.now();
|
|
8410
|
+
}
|
|
8411
|
+
/**
|
|
8412
|
+
* End drag mode and restore full rendering
|
|
8413
|
+
*/
|
|
8414
|
+
endDragMode() {
|
|
8415
|
+
if (!this.config) return;
|
|
8416
|
+
try {
|
|
8417
|
+
this.state.isDragModeActive = false;
|
|
8418
|
+
this.state.draggedNodeId = void 0;
|
|
8419
|
+
} catch (error) {
|
|
8420
|
+
ErrorHandler.logError(error);
|
|
8421
|
+
}
|
|
8422
|
+
}
|
|
8423
|
+
/**
|
|
8424
|
+
* Optimized fast rendering with object pooling and minimal GC
|
|
8425
|
+
*/
|
|
8426
|
+
renderFastDragOptimized(ctx) {
|
|
8427
|
+
if (!this.config) return;
|
|
8428
|
+
const { nodes, links } = this.config;
|
|
8429
|
+
const startTime = performance.now();
|
|
8430
|
+
this.reusableNodeStates = dragObjectPool.getReusableNodeArray();
|
|
8431
|
+
this.reusableVectors = dragObjectPool.getReusableVector2DArray();
|
|
8432
|
+
try {
|
|
8433
|
+
const linkVectors = dragObjectPool.batchAcquireVectors(links.length * 2);
|
|
8434
|
+
let vectorIndex = 0;
|
|
8435
|
+
ctx.strokeStyle = "#999";
|
|
8436
|
+
ctx.lineWidth = 1;
|
|
8437
|
+
ctx.globalAlpha = 0.6;
|
|
8438
|
+
for (const link of links) {
|
|
8439
|
+
const sourceNode = typeof link.source === "string" ? this.config.stateManager.getNode(link.source) : link.source;
|
|
8440
|
+
const targetNode = typeof link.target === "string" ? this.config.stateManager.getNode(link.target) : link.target;
|
|
8441
|
+
if (sourceNode?.x != null && sourceNode?.y != null && targetNode?.x != null && targetNode?.y != null) {
|
|
8442
|
+
const sourceVec = linkVectors[vectorIndex++];
|
|
8443
|
+
const targetVec = linkVectors[vectorIndex++];
|
|
8444
|
+
if (sourceVec && targetVec) {
|
|
8445
|
+
sourceVec.x = sourceNode.x;
|
|
8446
|
+
sourceVec.y = sourceNode.y;
|
|
8447
|
+
targetVec.x = targetNode.x;
|
|
8448
|
+
targetVec.y = targetNode.y;
|
|
8449
|
+
ctx.beginPath();
|
|
8450
|
+
ctx.moveTo(sourceVec.x, sourceVec.y);
|
|
8451
|
+
ctx.lineTo(targetVec.x, targetVec.y);
|
|
8452
|
+
ctx.stroke();
|
|
8453
|
+
}
|
|
8454
|
+
}
|
|
7193
8455
|
}
|
|
7194
8456
|
ctx.globalAlpha = 1;
|
|
8457
|
+
for (const node of nodes) {
|
|
8458
|
+
if (node.x == null || node.y == null) continue;
|
|
8459
|
+
const cachedProps = this.fastDragNodeCache.get(node.id);
|
|
8460
|
+
if (!cachedProps) continue;
|
|
8461
|
+
ctx.beginPath();
|
|
8462
|
+
ctx.arc(node.x, node.y, cachedProps.radius, 0, 2 * Math.PI);
|
|
8463
|
+
ctx.fillStyle = cachedProps.fill;
|
|
8464
|
+
ctx.fill();
|
|
8465
|
+
if (cachedProps.strokeWidth > 0) {
|
|
8466
|
+
ctx.strokeStyle = cachedProps.stroke;
|
|
8467
|
+
ctx.lineWidth = cachedProps.strokeWidth;
|
|
8468
|
+
ctx.stroke();
|
|
8469
|
+
}
|
|
8470
|
+
}
|
|
8471
|
+
this.renderFastDragLabelsOptimized(ctx);
|
|
8472
|
+
dragObjectPool.batchReleaseVectors(linkVectors);
|
|
7195
8473
|
} catch (error) {
|
|
7196
8474
|
ErrorHandler.logError(error);
|
|
7197
8475
|
}
|
|
8476
|
+
this.state.lastDragRenderTime = performance.now() - startTime;
|
|
7198
8477
|
}
|
|
7199
8478
|
/**
|
|
7200
|
-
*
|
|
8479
|
+
* Optimized label rendering with minimal object allocation
|
|
7201
8480
|
*/
|
|
7202
|
-
|
|
7203
|
-
|
|
7204
|
-
const
|
|
7205
|
-
|
|
7206
|
-
|
|
7207
|
-
|
|
7208
|
-
const
|
|
7209
|
-
|
|
7210
|
-
|
|
7211
|
-
|
|
7212
|
-
|
|
7213
|
-
|
|
7214
|
-
}
|
|
7215
|
-
|
|
7216
|
-
|
|
8481
|
+
renderFastDragLabelsOptimized(ctx) {
|
|
8482
|
+
if (!this.config) return;
|
|
8483
|
+
const { nodes } = this.config;
|
|
8484
|
+
ctx.font = "10px Arial";
|
|
8485
|
+
ctx.textAlign = "center";
|
|
8486
|
+
ctx.textBaseline = "middle";
|
|
8487
|
+
for (const node of nodes) {
|
|
8488
|
+
if (node.x == null || node.y == null) continue;
|
|
8489
|
+
const cachedProps = this.fastDragNodeCache.get(node.id);
|
|
8490
|
+
if (!cachedProps?.truncatedLabel) continue;
|
|
8491
|
+
ctx.fillStyle = cachedProps.textColor;
|
|
8492
|
+
ctx.fillText(cachedProps.truncatedLabel, node.x, node.y);
|
|
8493
|
+
}
|
|
8494
|
+
}
|
|
8495
|
+
/**
|
|
8496
|
+
* Build fast drag cache with pre-computed properties (zoom-renderer pattern)
|
|
8497
|
+
*/
|
|
8498
|
+
buildFastDragCache() {
|
|
8499
|
+
if (!this.config) return;
|
|
8500
|
+
try {
|
|
8501
|
+
this.fastDragNodeCache.clear();
|
|
8502
|
+
const tempCanvas = document.createElement("canvas");
|
|
8503
|
+
const tempCtx = tempCanvas.getContext("2d");
|
|
8504
|
+
if (!tempCtx) return;
|
|
8505
|
+
tempCtx.font = "10px Arial";
|
|
8506
|
+
for (const node of this.config.nodes) {
|
|
8507
|
+
const nodeStyle = this.config.styleResolver.resolveNodeStyle({
|
|
8508
|
+
node,
|
|
8509
|
+
isHovered: false,
|
|
8510
|
+
isSelected: false
|
|
8511
|
+
});
|
|
8512
|
+
const label = node.label || node.id;
|
|
8513
|
+
const maxWidth = nodeStyle.radius * 2 - 6;
|
|
8514
|
+
const truncatedLabel = this.preComputeTruncatedLabel(tempCtx, label, maxWidth);
|
|
8515
|
+
const textColor = nodeStyle.label?.textColor || "#ffffff";
|
|
8516
|
+
this.fastDragNodeCache.set(node.id, {
|
|
8517
|
+
radius: nodeStyle.radius,
|
|
8518
|
+
fill: nodeStyle.fill,
|
|
8519
|
+
stroke: nodeStyle.stroke,
|
|
8520
|
+
strokeWidth: nodeStyle.strokeWidth,
|
|
8521
|
+
textColor,
|
|
8522
|
+
truncatedLabel
|
|
8523
|
+
});
|
|
8524
|
+
}
|
|
8525
|
+
} catch (error) {
|
|
8526
|
+
ErrorHandler.logError(error);
|
|
8527
|
+
}
|
|
8528
|
+
}
|
|
8529
|
+
/**
|
|
8530
|
+
* Pre-compute truncated label (zoom-renderer pattern)
|
|
8531
|
+
*/
|
|
8532
|
+
preComputeTruncatedLabel(ctx, label, maxWidth) {
|
|
8533
|
+
if (ctx.measureText(label).width <= maxWidth) {
|
|
8534
|
+
return label;
|
|
8535
|
+
}
|
|
8536
|
+
let truncated = label;
|
|
8537
|
+
while (truncated.length > 1 && ctx.measureText(`${truncated}\u2026`).width > maxWidth) {
|
|
8538
|
+
truncated = truncated.slice(0, -1);
|
|
8539
|
+
}
|
|
8540
|
+
return truncated.length < label.length ? `${truncated}\u2026` : truncated;
|
|
8541
|
+
}
|
|
8542
|
+
/**
|
|
8543
|
+
* Set drag state for performance optimization (like ZoomRenderer.setZoomState)
|
|
8544
|
+
*/
|
|
8545
|
+
setDragState(isDragging) {
|
|
8546
|
+
if (isDragging) {
|
|
8547
|
+
this.state.isDragModeActive = true;
|
|
8548
|
+
} else {
|
|
8549
|
+
this.state.isDragModeActive = false;
|
|
8550
|
+
this.state.draggedNodeId = void 0;
|
|
8551
|
+
}
|
|
8552
|
+
}
|
|
8553
|
+
/**
|
|
8554
|
+
* Update configuration and rebuild cache
|
|
8555
|
+
*/
|
|
8556
|
+
updateConfig(updates) {
|
|
8557
|
+
if (this.config) {
|
|
8558
|
+
Object.assign(this.config, updates);
|
|
8559
|
+
this.buildFastDragCache();
|
|
8560
|
+
}
|
|
8561
|
+
}
|
|
8562
|
+
/**
|
|
8563
|
+
* Get drag optimization statistics including object pool metrics
|
|
8564
|
+
*/
|
|
8565
|
+
getStats() {
|
|
8566
|
+
const poolStats = dragObjectPool.getStats();
|
|
7217
8567
|
return {
|
|
7218
|
-
|
|
7219
|
-
|
|
8568
|
+
isDragModeActive: this.state.isDragModeActive,
|
|
8569
|
+
cachedNodeProperties: this.fastDragNodeCache.size,
|
|
8570
|
+
lastRenderTime: this.state.lastDragRenderTime,
|
|
8571
|
+
objectPool: {
|
|
8572
|
+
vector2D: poolStats.vector2D,
|
|
8573
|
+
nodeState: poolStats.nodeState,
|
|
8574
|
+
memoryEstimate: poolStats.memoryEstimate
|
|
8575
|
+
}
|
|
7220
8576
|
};
|
|
7221
8577
|
}
|
|
7222
8578
|
/**
|
|
7223
|
-
*
|
|
8579
|
+
* Destroy and clean up all resources
|
|
8580
|
+
*/
|
|
8581
|
+
destroy() {
|
|
8582
|
+
this.reusableNodeStates.length = 0;
|
|
8583
|
+
this.reusableVectors.length = 0;
|
|
8584
|
+
this.config = void 0;
|
|
8585
|
+
this.fastDragNodeCache.clear();
|
|
8586
|
+
this.state = {
|
|
8587
|
+
isDragModeActive: false,
|
|
8588
|
+
lastDragRenderTime: 0
|
|
8589
|
+
};
|
|
8590
|
+
}
|
|
8591
|
+
/**
|
|
8592
|
+
* Force memory optimization by clearing and rebuilding caches
|
|
8593
|
+
*/
|
|
8594
|
+
optimizeMemory() {
|
|
8595
|
+
this.buildFastDragCache();
|
|
8596
|
+
dragObjectPool.optimizeMemory();
|
|
8597
|
+
}
|
|
8598
|
+
};
|
|
8599
|
+
|
|
8600
|
+
// src/v2/rendering/renderer.ts
|
|
8601
|
+
var Renderer = class {
|
|
8602
|
+
config;
|
|
8603
|
+
canvasState;
|
|
8604
|
+
hoverManager;
|
|
8605
|
+
selectionManager;
|
|
8606
|
+
styleResolver;
|
|
8607
|
+
// Centralized state management for O(1) lookups
|
|
8608
|
+
stateManager = new StateManager();
|
|
8609
|
+
// Performance metrics tracking
|
|
8610
|
+
metricsManager = new PerformanceMetricsManager();
|
|
8611
|
+
// Interaction state resolution
|
|
8612
|
+
interactionResolver = new InteractionStateResolver();
|
|
8613
|
+
// Drag optimization
|
|
8614
|
+
dragOptimizer = new DragOptimizer();
|
|
8615
|
+
// Optimized Z-Index Renderer
|
|
8616
|
+
zIndexRenderer = new OptimizedZIndexRenderer();
|
|
8617
|
+
// Dedicated Zoom Renderer for separation of concerns
|
|
8618
|
+
zoomRenderer = new ZoomRenderer();
|
|
8619
|
+
// Dedicated Hit Detection Renderer for shadow canvas
|
|
8620
|
+
hitDetectionRenderer = new HitDetectionRenderer();
|
|
8621
|
+
/**
|
|
8622
|
+
* Initialize the renderer
|
|
8623
|
+
*/
|
|
8624
|
+
initialize(config, canvasState, hoverManager, selectionManager) {
|
|
8625
|
+
try {
|
|
8626
|
+
this.config = config;
|
|
8627
|
+
this.canvasState = canvasState;
|
|
8628
|
+
this.hoverManager = hoverManager;
|
|
8629
|
+
this.selectionManager = selectionManager;
|
|
8630
|
+
this.styleResolver = createStyleResolver(config.interaction);
|
|
8631
|
+
this.stateManager.initialize({ nodes: config.nodes, links: config.links });
|
|
8632
|
+
this.interactionResolver.updateManagers(hoverManager, selectionManager);
|
|
8633
|
+
this.initializeZIndexRenderer();
|
|
8634
|
+
this.initializeZoomRenderer();
|
|
8635
|
+
this.initializeHitDetectionRenderer();
|
|
8636
|
+
this.initializeDragOptimizer();
|
|
8637
|
+
} catch (error) {
|
|
8638
|
+
ErrorHandler.logError(error, {
|
|
8639
|
+
nodeCount: config.nodes.length,
|
|
8640
|
+
linkCount: config.links.length
|
|
8641
|
+
});
|
|
8642
|
+
throw error;
|
|
8643
|
+
}
|
|
8644
|
+
}
|
|
8645
|
+
/**
|
|
8646
|
+
* Initialize the optimized z-index renderer with all required dependencies
|
|
8647
|
+
*/
|
|
8648
|
+
initializeZIndexRenderer() {
|
|
8649
|
+
if (!this.config || !this.styleResolver) return;
|
|
8650
|
+
const callbacks = this.interactionResolver.createCallbacks();
|
|
8651
|
+
this.zIndexRenderer.initialize({
|
|
8652
|
+
nodes: this.config.nodes,
|
|
8653
|
+
links: this.config.links,
|
|
8654
|
+
styleResolver: this.styleResolver,
|
|
8655
|
+
isNodeHovered: callbacks.isNodeHovered,
|
|
8656
|
+
isNodeSelected: callbacks.isNodeSelected,
|
|
8657
|
+
isLinkHovered: callbacks.isLinkHovered,
|
|
8658
|
+
isLinkSelected: callbacks.isLinkSelected,
|
|
8659
|
+
getLinkId: (link) => this.getLinkId(link),
|
|
8660
|
+
getLinkMidpoint: (link) => this.getLinkMidpoint(link),
|
|
8661
|
+
renderNodes: (ctx, nodes) => this.renderNodesLayer(ctx, nodes),
|
|
8662
|
+
renderLinks: (ctx, links) => this.renderLinksLayer(ctx, links),
|
|
8663
|
+
renderNodeLabels: (ctx, nodes) => this.renderNodeLabelsLayer(ctx, nodes)
|
|
8664
|
+
});
|
|
8665
|
+
}
|
|
8666
|
+
/**
|
|
8667
|
+
* Initialize the zoom renderer for zoom-specific optimizations
|
|
8668
|
+
*/
|
|
8669
|
+
initializeZoomRenderer() {
|
|
8670
|
+
if (!this.config || !this.styleResolver) return;
|
|
8671
|
+
this.zoomRenderer.initialize({
|
|
8672
|
+
nodes: this.config.nodes,
|
|
8673
|
+
links: this.config.links,
|
|
8674
|
+
styleResolver: this.styleResolver,
|
|
8675
|
+
stateManager: this.stateManager
|
|
8676
|
+
});
|
|
8677
|
+
}
|
|
8678
|
+
/**
|
|
8679
|
+
* Initialize the hit detection renderer for shadow canvas
|
|
8680
|
+
*/
|
|
8681
|
+
initializeHitDetectionRenderer() {
|
|
8682
|
+
if (!this.config || !this.styleResolver || !this.canvasState) return;
|
|
8683
|
+
this.hitDetectionRenderer.initialize({
|
|
8684
|
+
nodes: this.config.nodes,
|
|
8685
|
+
links: this.config.links,
|
|
8686
|
+
styleResolver: this.styleResolver,
|
|
8687
|
+
stateManager: this.stateManager,
|
|
8688
|
+
canvas: this.canvasState.canvas,
|
|
8689
|
+
shadowCtx: this.canvasState.shadowCtx,
|
|
8690
|
+
linkHoverPrecision: 4
|
|
8691
|
+
// Default 4px like force-graph
|
|
8692
|
+
});
|
|
8693
|
+
}
|
|
8694
|
+
/**
|
|
8695
|
+
* Initialize the drag optimizer for fast drag rendering
|
|
8696
|
+
*/
|
|
8697
|
+
initializeDragOptimizer() {
|
|
8698
|
+
if (!this.config || !this.styleResolver) return;
|
|
8699
|
+
this.dragOptimizer.initialize({
|
|
8700
|
+
nodes: this.config.nodes,
|
|
8701
|
+
links: this.config.links,
|
|
8702
|
+
stateManager: this.stateManager,
|
|
8703
|
+
styleResolver: this.styleResolver,
|
|
8704
|
+
interactionResolver: this.interactionResolver,
|
|
8705
|
+
zIndexRenderer: this.zIndexRenderer
|
|
8706
|
+
});
|
|
8707
|
+
}
|
|
8708
|
+
/**
|
|
8709
|
+
* Main render method with performance metrics (Instrumented)
|
|
8710
|
+
*/
|
|
8711
|
+
render() {
|
|
8712
|
+
if (!this.config || !this.canvasState) {
|
|
8713
|
+
throw new RenderError("Renderer not initialized");
|
|
8714
|
+
}
|
|
8715
|
+
const startTime = performance.now();
|
|
8716
|
+
this.metricsManager.incrementFrame();
|
|
8717
|
+
try {
|
|
8718
|
+
const { ctx } = this.canvasState;
|
|
8719
|
+
this.clearCanvas(ctx);
|
|
8720
|
+
this.dragOptimizer.render(ctx, this.metricsManager.getMetrics());
|
|
8721
|
+
this.hitDetectionRenderer.markShadowCanvasDirty();
|
|
8722
|
+
this.metricsManager.addTiming("renderTotal", performance.now() - startTime);
|
|
8723
|
+
if (this.metricsManager.shouldLogMetrics()) {
|
|
8724
|
+
this.metricsManager.logMetrics(this.config.nodes.length, this.config.links.length);
|
|
8725
|
+
}
|
|
8726
|
+
} catch (error) {
|
|
8727
|
+
ErrorHandler.logError(error);
|
|
8728
|
+
throw new RenderError("Failed to render graph", {
|
|
8729
|
+
nodeCount: this.config.nodes.length,
|
|
8730
|
+
linkCount: this.config.links.length,
|
|
8731
|
+
originalError: error.message
|
|
8732
|
+
});
|
|
8733
|
+
}
|
|
8734
|
+
}
|
|
8735
|
+
/**
|
|
8736
|
+
* Render with transform (called during zoom/pan) - OPTIMIZED PATTERN
|
|
8737
|
+
* Fast rendering during zoom, full rendering when stopped
|
|
8738
|
+
*/
|
|
8739
|
+
renderWithTransform() {
|
|
8740
|
+
if (!this.canvasState) return;
|
|
8741
|
+
try {
|
|
8742
|
+
const { canvas, ctx } = this.canvasState;
|
|
8743
|
+
const transform2 = transform(canvas);
|
|
8744
|
+
this.clearCanvas(ctx);
|
|
8745
|
+
this.applyTransform(transform2, ctx);
|
|
8746
|
+
const isDragging = this.dragOptimizer.getStats().isDragModeActive;
|
|
8747
|
+
const isZooming = this.zoomRenderer.getZoomState();
|
|
8748
|
+
if (isDragging) {
|
|
8749
|
+
this.dragOptimizer.render(ctx, this.metricsManager.getMetrics());
|
|
8750
|
+
} else if (isZooming) {
|
|
8751
|
+
this.zoomRenderer.render(ctx, this.zIndexRenderer);
|
|
8752
|
+
} else {
|
|
8753
|
+
this.zIndexRenderer.render(ctx, this.metricsManager.getMetrics());
|
|
8754
|
+
}
|
|
8755
|
+
} catch (error) {
|
|
8756
|
+
ErrorHandler.logError(error);
|
|
8757
|
+
}
|
|
8758
|
+
}
|
|
8759
|
+
/**
|
|
8760
|
+
* Set zoom state for performance optimization (delegates to ZoomRenderer)
|
|
8761
|
+
*/
|
|
8762
|
+
setZoomState(isZooming) {
|
|
8763
|
+
this.zoomRenderer.setZoomState(isZooming);
|
|
8764
|
+
}
|
|
8765
|
+
/**
|
|
8766
|
+
* Set drag state for performance optimization (like setZoomState)
|
|
8767
|
+
*/
|
|
8768
|
+
setDragState(isDragging) {
|
|
8769
|
+
this.dragOptimizer.setDragState(isDragging);
|
|
8770
|
+
}
|
|
8771
|
+
/**
|
|
8772
|
+
* Set the currently dragged node
|
|
8773
|
+
*/
|
|
8774
|
+
setDraggedNode(draggedNode) {
|
|
8775
|
+
this.dragOptimizer.setDraggedNode(draggedNode);
|
|
8776
|
+
}
|
|
8777
|
+
/**
|
|
8778
|
+
* Render shadow canvas for hit detection (delegates to HitDetectionRenderer)
|
|
8779
|
+
*/
|
|
8780
|
+
renderShadowCanvas() {
|
|
8781
|
+
this.hitDetectionRenderer.renderShadowCanvas();
|
|
8782
|
+
}
|
|
8783
|
+
/**
|
|
8784
|
+
* Mark shadow canvas as dirty for next render (delegates to HitDetectionRenderer)
|
|
8785
|
+
*/
|
|
8786
|
+
markShadowCanvasDirty() {
|
|
8787
|
+
this.hitDetectionRenderer.markShadowCanvasDirty();
|
|
8788
|
+
}
|
|
8789
|
+
/**
|
|
8790
|
+
* Force shadow canvas render (delegates to HitDetectionRenderer)
|
|
8791
|
+
*/
|
|
8792
|
+
forceShadowCanvasRender() {
|
|
8793
|
+
this.hitDetectionRenderer.forceShadowCanvasRender();
|
|
8794
|
+
}
|
|
8795
|
+
/**
|
|
8796
|
+
* Clear canvas context
|
|
8797
|
+
*/
|
|
8798
|
+
clearCanvas(ctx) {
|
|
8799
|
+
if (!this.canvasState) return;
|
|
8800
|
+
try {
|
|
8801
|
+
const { width, height } = this.canvasState;
|
|
8802
|
+
CanvasUtils.resetTransform(ctx);
|
|
8803
|
+
ctx.clearRect(0, 0, width, height);
|
|
8804
|
+
} catch {
|
|
8805
|
+
throw new RenderError("Failed to clear canvas");
|
|
8806
|
+
}
|
|
8807
|
+
}
|
|
8808
|
+
/**
|
|
8809
|
+
* Apply transform to canvas context
|
|
8810
|
+
*/
|
|
8811
|
+
applyTransform(transform2, ctx) {
|
|
8812
|
+
try {
|
|
8813
|
+
CanvasUtils.resetTransform(ctx);
|
|
8814
|
+
ctx.translate(transform2.x, transform2.y);
|
|
8815
|
+
ctx.scale(transform2.k, transform2.k);
|
|
8816
|
+
} catch {
|
|
8817
|
+
throw new RenderError("Failed to apply transform");
|
|
8818
|
+
}
|
|
8819
|
+
}
|
|
8820
|
+
/**
|
|
8821
|
+
* Get unique link ID for tracking (delegates to StateManager)
|
|
8822
|
+
*/
|
|
8823
|
+
getLinkId(link) {
|
|
8824
|
+
return this.stateManager.getLinkId(link);
|
|
8825
|
+
}
|
|
8826
|
+
/**
|
|
8827
|
+
* Reset performance metrics
|
|
8828
|
+
*/
|
|
8829
|
+
resetPerformanceMetrics() {
|
|
8830
|
+
this.metricsManager.reset();
|
|
8831
|
+
}
|
|
8832
|
+
/**
|
|
8833
|
+
* Get current performance metrics
|
|
7224
8834
|
*/
|
|
7225
|
-
|
|
7226
|
-
|
|
7227
|
-
const sourceY = source.y ?? 0;
|
|
7228
|
-
const targetX = target.x ?? 0;
|
|
7229
|
-
const targetY = target.y ?? 0;
|
|
7230
|
-
const dx = targetX - sourceX;
|
|
7231
|
-
const dy = targetY - sourceY;
|
|
7232
|
-
const distance = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
7233
|
-
const targetNodeStyle = this.styleResolver.resolveNodeStyle({
|
|
7234
|
-
node: target,
|
|
7235
|
-
isHovered: this.isNodeHovered(target.id),
|
|
7236
|
-
isSelected: this.isNodeSelected(target.id)
|
|
7237
|
-
});
|
|
7238
|
-
const visualRadius = targetNodeStyle.radius + targetNodeStyle.strokeWidth / 2;
|
|
7239
|
-
const arrowLength = style.arrow?.enabled ? style.arrow.size ?? 4 : 0;
|
|
7240
|
-
const offset = style.arrow?.enabled ? visualRadius + arrowLength : visualRadius + 1;
|
|
7241
|
-
const shortenedPoint = {
|
|
7242
|
-
x: targetX - dx / distance * offset,
|
|
7243
|
-
y: targetY - dy / distance * offset
|
|
7244
|
-
};
|
|
7245
|
-
return shortenedPoint;
|
|
8835
|
+
getPerformanceMetrics() {
|
|
8836
|
+
return this.metricsManager.getMetrics();
|
|
7246
8837
|
}
|
|
7247
8838
|
/**
|
|
7248
|
-
*
|
|
8839
|
+
* Force log performance metrics immediately (for debugging)
|
|
7249
8840
|
*/
|
|
7250
|
-
|
|
7251
|
-
|
|
7252
|
-
|
|
7253
|
-
|
|
7254
|
-
const angle = Math.atan2(dy, dx);
|
|
7255
|
-
const arrowLength = arrowStyle.size ?? 4;
|
|
7256
|
-
const arrowTipX = targetPoint.x + arrowLength * Math.cos(angle);
|
|
7257
|
-
const arrowTipY = targetPoint.y + arrowLength * Math.sin(angle);
|
|
7258
|
-
const x1 = arrowTipX - arrowLength * Math.cos(angle - Math.PI / 6);
|
|
7259
|
-
const y1 = arrowTipY - arrowLength * Math.sin(angle - Math.PI / 6);
|
|
7260
|
-
const x22 = arrowTipX - arrowLength * Math.cos(angle + Math.PI / 6);
|
|
7261
|
-
const y22 = arrowTipY - arrowLength * Math.sin(angle + Math.PI / 6);
|
|
7262
|
-
ctx.fillStyle = arrowStyle.fill ?? "#000000";
|
|
7263
|
-
ctx.beginPath();
|
|
7264
|
-
ctx.moveTo(arrowTipX, arrowTipY);
|
|
7265
|
-
ctx.lineTo(x1, y1);
|
|
7266
|
-
ctx.lineTo(x22, y22);
|
|
7267
|
-
ctx.closePath();
|
|
7268
|
-
ctx.fill();
|
|
7269
|
-
} catch (error) {
|
|
7270
|
-
ErrorHandler.logError(error);
|
|
7271
|
-
}
|
|
8841
|
+
forceLogMetrics() {
|
|
8842
|
+
const nodeCount = this.config?.nodes.length || 0;
|
|
8843
|
+
const linkCount = this.config?.links.length || 0;
|
|
8844
|
+
this.metricsManager.logMetrics(nodeCount, linkCount);
|
|
7272
8845
|
}
|
|
7273
8846
|
/**
|
|
7274
|
-
*
|
|
8847
|
+
* Calculate midpoint of a link for label positioning (delegates to StateManager)
|
|
7275
8848
|
*/
|
|
7276
|
-
|
|
7277
|
-
this.
|
|
7278
|
-
ctx,
|
|
7279
|
-
{ x: source.x, y: source.y },
|
|
7280
|
-
{ x: target.x, y: target.y },
|
|
7281
|
-
arrowStyle
|
|
7282
|
-
);
|
|
8849
|
+
getLinkMidpoint(link) {
|
|
8850
|
+
return this.stateManager.getLinkMidpoint(link);
|
|
7283
8851
|
}
|
|
7284
8852
|
/**
|
|
7285
8853
|
* Initialize node positions if needed
|
|
@@ -7304,6 +8872,29 @@ var Renderer = class {
|
|
|
7304
8872
|
if (!this.config) return;
|
|
7305
8873
|
try {
|
|
7306
8874
|
Object.assign(this.config, updates);
|
|
8875
|
+
this.zIndexRenderer.updateConfig({
|
|
8876
|
+
nodes: this.config.nodes,
|
|
8877
|
+
links: this.config.links
|
|
8878
|
+
});
|
|
8879
|
+
this.stateManager.updateState({ nodes: this.config.nodes, links: this.config.links });
|
|
8880
|
+
this.zoomRenderer.updateConfig({
|
|
8881
|
+
nodes: this.config.nodes,
|
|
8882
|
+
links: this.config.links,
|
|
8883
|
+
stateManager: this.stateManager
|
|
8884
|
+
});
|
|
8885
|
+
this.hitDetectionRenderer.updateConfig({
|
|
8886
|
+
nodes: this.config.nodes,
|
|
8887
|
+
links: this.config.links,
|
|
8888
|
+
stateManager: this.stateManager
|
|
8889
|
+
});
|
|
8890
|
+
this.dragOptimizer.updateConfig({
|
|
8891
|
+
nodes: this.config.nodes,
|
|
8892
|
+
links: this.config.links,
|
|
8893
|
+
stateManager: this.stateManager,
|
|
8894
|
+
styleResolver: this.styleResolver,
|
|
8895
|
+
interactionResolver: this.interactionResolver,
|
|
8896
|
+
zIndexRenderer: this.zIndexRenderer
|
|
8897
|
+
});
|
|
7307
8898
|
} catch (error) {
|
|
7308
8899
|
ErrorHandler.logError(error);
|
|
7309
8900
|
}
|
|
@@ -7326,247 +8917,22 @@ var Renderer = class {
|
|
|
7326
8917
|
};
|
|
7327
8918
|
}
|
|
7328
8919
|
/**
|
|
7329
|
-
* Debug shadow canvas export (
|
|
8920
|
+
* Debug shadow canvas export (delegates to HitDetectionRenderer)
|
|
7330
8921
|
*/
|
|
7331
8922
|
debugShadowCanvas() {
|
|
7332
|
-
|
|
7333
|
-
if (!this.canvasState) return;
|
|
7334
|
-
const { shadowCanvas } = this.canvasState;
|
|
7335
|
-
const link = document.createElement("a");
|
|
7336
|
-
link.download = "shadow-canvas-debug.png";
|
|
7337
|
-
link.href = shadowCanvas.toDataURL("image/png");
|
|
7338
|
-
link.click();
|
|
7339
|
-
} catch (error) {
|
|
7340
|
-
ErrorHandler.logError(error);
|
|
7341
|
-
}
|
|
7342
|
-
}
|
|
7343
|
-
/**
|
|
7344
|
-
* Build node index for O(1) lookups (Step 3 optimization)
|
|
7345
|
-
*/
|
|
7346
|
-
buildNodeIndex() {
|
|
7347
|
-
if (!this.config) return;
|
|
7348
|
-
try {
|
|
7349
|
-
this.nodeMap.clear();
|
|
7350
|
-
for (const node of this.config.nodes) {
|
|
7351
|
-
this.nodeMap.set(node.id, node);
|
|
7352
|
-
}
|
|
7353
|
-
for (const link of this.config.links) {
|
|
7354
|
-
if (typeof link.source === "string") {
|
|
7355
|
-
const sourceNode = this.nodeMap.get(link.source);
|
|
7356
|
-
if (sourceNode) {
|
|
7357
|
-
link.source = sourceNode;
|
|
7358
|
-
}
|
|
7359
|
-
}
|
|
7360
|
-
if (typeof link.target === "string") {
|
|
7361
|
-
const targetNode = this.nodeMap.get(link.target);
|
|
7362
|
-
if (targetNode) {
|
|
7363
|
-
link.target = targetNode;
|
|
7364
|
-
}
|
|
7365
|
-
}
|
|
7366
|
-
}
|
|
7367
|
-
} catch (error) {
|
|
7368
|
-
ErrorHandler.logError(error);
|
|
7369
|
-
}
|
|
7370
|
-
}
|
|
7371
|
-
/**
|
|
7372
|
-
* Get node by ID using O(1) lookup (Step 3 optimization)
|
|
7373
|
-
*/
|
|
7374
|
-
getNodeById(nodeId) {
|
|
7375
|
-
return this.nodeMap.get(nodeId);
|
|
7376
|
-
}
|
|
7377
|
-
/**
|
|
7378
|
-
* Render with z-index layers (for renderWithTransform)
|
|
7379
|
-
*/
|
|
7380
|
-
renderWithLayers(ctx) {
|
|
7381
|
-
if (!this.config) return;
|
|
7382
|
-
try {
|
|
7383
|
-
const hoveredNode = this.getHoveredNode();
|
|
7384
|
-
const selectedNode = this.getSelectedNode();
|
|
7385
|
-
const hoveredLink = this.getHoveredLink();
|
|
7386
|
-
const selectedLink = this.getSelectedLink();
|
|
7387
|
-
const nodeHighlightChecker = ZIndexManager.createNodeHighlightChecker(
|
|
7388
|
-
hoveredNode?.id || null,
|
|
7389
|
-
selectedNode?.id || null
|
|
7390
|
-
);
|
|
7391
|
-
const linkHighlightChecker = ZIndexManager.createLinkHighlightChecker(
|
|
7392
|
-
hoveredNode?.id || null,
|
|
7393
|
-
selectedNode?.id || null,
|
|
7394
|
-
hoveredLink ? this.getLinkId(hoveredLink) : null,
|
|
7395
|
-
selectedLink ? this.getLinkId(selectedLink) : null
|
|
7396
|
-
);
|
|
7397
|
-
const linkLayers = ZIndexManager.separateIntoLayers(this.config.links, linkHighlightChecker);
|
|
7398
|
-
const nodeLayers = ZIndexManager.separateIntoLayers(this.config.nodes, nodeHighlightChecker);
|
|
7399
|
-
this.renderLinksLayer(ctx, linkLayers.background);
|
|
7400
|
-
this.renderLinkLabelsLayer(ctx, linkLayers.background);
|
|
7401
|
-
this.renderNodesWithLabelsLayer(ctx, nodeLayers.background);
|
|
7402
|
-
this.renderLinksLayer(ctx, linkLayers.foreground);
|
|
7403
|
-
this.renderLinkLabelsLayer(ctx, linkLayers.foreground);
|
|
7404
|
-
this.renderNodesWithLabelsLayer(ctx, nodeLayers.foreground);
|
|
7405
|
-
} catch (error) {
|
|
7406
|
-
ErrorHandler.logError(error);
|
|
7407
|
-
}
|
|
7408
|
-
}
|
|
7409
|
-
/**
|
|
7410
|
-
* Render with z-index layers and performance metrics (for main render)
|
|
7411
|
-
*/
|
|
7412
|
-
renderWithLayersAndMetrics(ctx) {
|
|
7413
|
-
if (!this.config) return;
|
|
7414
|
-
try {
|
|
7415
|
-
const hoveredNode = this.getHoveredNode();
|
|
7416
|
-
const selectedNode = this.getSelectedNode();
|
|
7417
|
-
const hoveredLink = this.getHoveredLink();
|
|
7418
|
-
const selectedLink = this.getSelectedLink();
|
|
7419
|
-
const nodeHighlightChecker = ZIndexManager.createNodeHighlightChecker(
|
|
7420
|
-
hoveredNode?.id || null,
|
|
7421
|
-
selectedNode?.id || null
|
|
7422
|
-
);
|
|
7423
|
-
const linkHighlightChecker = ZIndexManager.createLinkHighlightChecker(
|
|
7424
|
-
hoveredNode?.id || null,
|
|
7425
|
-
selectedNode?.id || null,
|
|
7426
|
-
hoveredLink ? this.getLinkId(hoveredLink) : null,
|
|
7427
|
-
selectedLink ? this.getLinkId(selectedLink) : null
|
|
7428
|
-
);
|
|
7429
|
-
const linkLayers = ZIndexManager.separateIntoLayers(this.config.links, linkHighlightChecker);
|
|
7430
|
-
const nodeLayers = ZIndexManager.separateIntoLayers(this.config.nodes, nodeHighlightChecker);
|
|
7431
|
-
let stepStart = performance.now();
|
|
7432
|
-
this.renderLinksLayer(ctx, linkLayers.background);
|
|
7433
|
-
this.renderLinksLayer(ctx, linkLayers.foreground);
|
|
7434
|
-
this.performanceMetrics.renderLinks += performance.now() - stepStart;
|
|
7435
|
-
stepStart = performance.now();
|
|
7436
|
-
this.renderNodesLayer(ctx, nodeLayers.background);
|
|
7437
|
-
this.renderNodesLayer(ctx, nodeLayers.foreground);
|
|
7438
|
-
this.performanceMetrics.renderNodes += performance.now() - stepStart;
|
|
7439
|
-
stepStart = performance.now();
|
|
7440
|
-
this.renderLinkLabelsLayer(ctx, linkLayers.background);
|
|
7441
|
-
this.renderLinkLabelsLayer(ctx, linkLayers.foreground);
|
|
7442
|
-
this.performanceMetrics.renderLinkLabels += performance.now() - stepStart;
|
|
7443
|
-
stepStart = performance.now();
|
|
7444
|
-
this.renderNodeLabelsLayer(ctx, nodeLayers.background);
|
|
7445
|
-
this.renderNodeLabelsLayer(ctx, nodeLayers.foreground);
|
|
7446
|
-
this.performanceMetrics.renderNodeLabels += performance.now() - stepStart;
|
|
7447
|
-
} catch (error) {
|
|
7448
|
-
ErrorHandler.logError(error);
|
|
7449
|
-
}
|
|
7450
|
-
}
|
|
7451
|
-
/**
|
|
7452
|
-
* Helper methods for getting current interaction states
|
|
7453
|
-
*/
|
|
7454
|
-
getHoveredNode() {
|
|
7455
|
-
if (!this.hoverManager) return null;
|
|
7456
|
-
const hoverState = this.hoverManager.getHoverState();
|
|
7457
|
-
return hoverState.currentHovered?.d.entityType === "Node" ? hoverState.currentHovered.d : null;
|
|
7458
|
-
}
|
|
7459
|
-
getSelectedNode() {
|
|
7460
|
-
if (!this.selectionManager) return null;
|
|
7461
|
-
const selectionState = this.selectionManager.getSelectionState();
|
|
7462
|
-
return selectionState.selectedNode;
|
|
7463
|
-
}
|
|
7464
|
-
getHoveredLink() {
|
|
7465
|
-
if (!this.hoverManager) return null;
|
|
7466
|
-
const hoverState = this.hoverManager.getHoverState();
|
|
7467
|
-
return hoverState.currentHovered?.d.entityType === "Link" ? hoverState.currentHovered.d : null;
|
|
7468
|
-
}
|
|
7469
|
-
getSelectedLink() {
|
|
7470
|
-
if (!this.selectionManager) return null;
|
|
7471
|
-
const selectionState = this.selectionManager.getSelectionState();
|
|
7472
|
-
return selectionState.selectedLink;
|
|
7473
|
-
}
|
|
7474
|
-
/**
|
|
7475
|
-
* Layer-specific rendering methods (render subsets of entities)
|
|
7476
|
-
*/
|
|
7477
|
-
renderNodesWithLabelsLayer(ctx, nodes) {
|
|
7478
|
-
if (!this.config || !this.styleResolver) return;
|
|
7479
|
-
const nodeCount = this.config.nodes.length;
|
|
7480
|
-
const isLargeGraph = nodeCount > 1e4;
|
|
7481
|
-
let currentZoom = 1;
|
|
7482
|
-
let isZoomedOutForNodes = false;
|
|
7483
|
-
if (isLargeGraph) {
|
|
7484
|
-
currentZoom = this.canvasState ? transform(this.canvasState.canvas).k : 1;
|
|
7485
|
-
isZoomedOutForNodes = currentZoom <= 0.8;
|
|
7486
|
-
}
|
|
7487
|
-
const nodeStateCache = /* @__PURE__ */ new Map();
|
|
7488
|
-
for (const node of nodes) {
|
|
7489
|
-
if (!node.x || !node.y) continue;
|
|
7490
|
-
const nodeId = node.id;
|
|
7491
|
-
let nodeState = nodeStateCache.get(nodeId);
|
|
7492
|
-
if (!nodeState) {
|
|
7493
|
-
nodeState = {
|
|
7494
|
-
isHovered: this.isNodeHovered(nodeId),
|
|
7495
|
-
isSelected: this.isNodeSelected(nodeId)
|
|
7496
|
-
};
|
|
7497
|
-
nodeStateCache.set(nodeId, nodeState);
|
|
7498
|
-
}
|
|
7499
|
-
const nodeStyle = this.styleResolver.resolveNodeStyle({
|
|
7500
|
-
node,
|
|
7501
|
-
isHovered: nodeState.isHovered,
|
|
7502
|
-
isSelected: nodeState.isSelected
|
|
7503
|
-
});
|
|
7504
|
-
ctx.beginPath();
|
|
7505
|
-
ctx.arc(node.x, node.y, nodeStyle.radius, 0, 2 * Math.PI);
|
|
7506
|
-
ctx.fillStyle = nodeStyle.fill;
|
|
7507
|
-
ctx.fill();
|
|
7508
|
-
if (nodeStyle.stroke && nodeStyle.strokeWidth > 0) {
|
|
7509
|
-
ctx.strokeStyle = nodeStyle.stroke;
|
|
7510
|
-
ctx.lineWidth = nodeStyle.strokeWidth;
|
|
7511
|
-
ctx.stroke();
|
|
7512
|
-
}
|
|
7513
|
-
if (isLargeGraph && isZoomedOutForNodes) {
|
|
7514
|
-
continue;
|
|
7515
|
-
}
|
|
7516
|
-
const nodeStyleWithLabel = nodeStyle;
|
|
7517
|
-
if (nodeStyleWithLabel.label && !nodeStyleWithLabel.label.enabled) continue;
|
|
7518
|
-
const fullLabel = node.label || node.id;
|
|
7519
|
-
const defaultStyle = {
|
|
7520
|
-
font: "9px sans-serif",
|
|
7521
|
-
textAlign: "center",
|
|
7522
|
-
textBaseline: "middle",
|
|
7523
|
-
fillStyle: "#ffffff",
|
|
7524
|
-
offsetY: 0
|
|
7525
|
-
};
|
|
7526
|
-
if (nodeStyleWithLabel.label) {
|
|
7527
|
-
ctx.font = nodeStyleWithLabel.label.font || defaultStyle.font;
|
|
7528
|
-
ctx.textAlign = nodeStyleWithLabel.label.textAlign || defaultStyle.textAlign;
|
|
7529
|
-
ctx.textBaseline = nodeStyleWithLabel.label.textBaseline || defaultStyle.textBaseline;
|
|
7530
|
-
ctx.fillStyle = nodeStyleWithLabel.label.textColor || defaultStyle.fillStyle;
|
|
7531
|
-
} else {
|
|
7532
|
-
ctx.font = defaultStyle.font;
|
|
7533
|
-
ctx.textAlign = defaultStyle.textAlign;
|
|
7534
|
-
ctx.textBaseline = defaultStyle.textBaseline;
|
|
7535
|
-
ctx.fillStyle = defaultStyle.fillStyle;
|
|
7536
|
-
}
|
|
7537
|
-
const maxWidth = nodeStyle.radius * 2 - 6;
|
|
7538
|
-
const truncatedLabel = this.truncateLabel(ctx, fullLabel, maxWidth);
|
|
7539
|
-
const labelY = node.y + (nodeStyleWithLabel.label?.offsetY || defaultStyle.offsetY);
|
|
7540
|
-
ctx.fillText(truncatedLabel, node.x, labelY);
|
|
7541
|
-
}
|
|
7542
|
-
}
|
|
7543
|
-
/**
|
|
7544
|
-
* Helper method to truncate labels (copied from NodeLabelsRenderer)
|
|
7545
|
-
*/
|
|
7546
|
-
truncateLabel(ctx, label, maxWidth) {
|
|
7547
|
-
let truncatedLabel = label;
|
|
7548
|
-
if (ctx.measureText(truncatedLabel).width <= maxWidth) {
|
|
7549
|
-
return truncatedLabel;
|
|
7550
|
-
}
|
|
7551
|
-
while (truncatedLabel.length > 1 && ctx.measureText(`${truncatedLabel}\u2026`).width > maxWidth) {
|
|
7552
|
-
truncatedLabel = truncatedLabel.slice(0, -1);
|
|
7553
|
-
}
|
|
7554
|
-
return truncatedLabel.length < label.length ? `${truncatedLabel}\u2026` : truncatedLabel;
|
|
8923
|
+
this.hitDetectionRenderer.debugShadowCanvas();
|
|
7555
8924
|
}
|
|
7556
8925
|
renderLinksLayer(ctx, links) {
|
|
7557
8926
|
if (!this.config || !this.styleResolver) return;
|
|
7558
8927
|
const linkStateCache = /* @__PURE__ */ new Map();
|
|
7559
8928
|
for (const link of links) {
|
|
7560
|
-
const sourceNode = typeof link.source === "string" ? this.
|
|
7561
|
-
const targetNode = typeof link.target === "string" ? this.
|
|
8929
|
+
const sourceNode = typeof link.source === "string" ? this.stateManager.getNode(link.source) : link.source;
|
|
8930
|
+
const targetNode = typeof link.target === "string" ? this.stateManager.getNode(link.target) : link.target;
|
|
7562
8931
|
if (sourceNode && targetNode && sourceNode.x && sourceNode.y && targetNode.x && targetNode.y) {
|
|
7563
8932
|
const linkId = this.getLinkId(link);
|
|
7564
8933
|
let linkState = linkStateCache.get(linkId);
|
|
7565
8934
|
if (!linkState) {
|
|
7566
|
-
linkState =
|
|
7567
|
-
isHovered: this.isLinkHovered(link),
|
|
7568
|
-
isSelected: this.isLinkSelected(link)
|
|
7569
|
-
};
|
|
8935
|
+
linkState = this.interactionResolver.getLinkState(link);
|
|
7570
8936
|
linkStateCache.set(linkId, linkState);
|
|
7571
8937
|
}
|
|
7572
8938
|
const style = this.styleResolver.resolveLinkStyle({
|
|
@@ -7574,85 +8940,18 @@ var Renderer = class {
|
|
|
7574
8940
|
isHovered: linkState.isHovered,
|
|
7575
8941
|
isSelected: linkState.isSelected
|
|
7576
8942
|
});
|
|
7577
|
-
this.
|
|
7578
|
-
|
|
7579
|
-
|
|
7580
|
-
|
|
7581
|
-
|
|
7582
|
-
|
|
7583
|
-
|
|
7584
|
-
|
|
7585
|
-
|
|
7586
|
-
|
|
7587
|
-
this.hasLoggedLargeGraphOptimization = true;
|
|
7588
|
-
}
|
|
7589
|
-
if (isLargeGraph) {
|
|
7590
|
-
const currentZoom = this.canvasState ? transform(this.canvasState.canvas).k : 1;
|
|
7591
|
-
const isZoomedOut = currentZoom <= 1;
|
|
7592
|
-
if (isZoomedOut) {
|
|
7593
|
-
return;
|
|
8943
|
+
const callbacks = this.interactionResolver.createCallbacks();
|
|
8944
|
+
LinkRenderer.renderDirectedLink(
|
|
8945
|
+
ctx,
|
|
8946
|
+
sourceNode,
|
|
8947
|
+
targetNode,
|
|
8948
|
+
style,
|
|
8949
|
+
this.styleResolver,
|
|
8950
|
+
callbacks.isNodeHovered,
|
|
8951
|
+
callbacks.isNodeSelected
|
|
8952
|
+
);
|
|
7594
8953
|
}
|
|
7595
8954
|
}
|
|
7596
|
-
const linkStateCache = /* @__PURE__ */ new Map();
|
|
7597
|
-
LinkLabelsRenderer.renderWithVisibility(
|
|
7598
|
-
ctx,
|
|
7599
|
-
links,
|
|
7600
|
-
(link) => {
|
|
7601
|
-
if (!link.label) {
|
|
7602
|
-
return null;
|
|
7603
|
-
}
|
|
7604
|
-
const linkId = this.getLinkId(link);
|
|
7605
|
-
let linkState = linkStateCache.get(linkId);
|
|
7606
|
-
if (!linkState) {
|
|
7607
|
-
linkState = {
|
|
7608
|
-
isHovered: this.isLinkHovered(link),
|
|
7609
|
-
isSelected: this.isLinkSelected(link)
|
|
7610
|
-
};
|
|
7611
|
-
linkStateCache.set(linkId, linkState);
|
|
7612
|
-
}
|
|
7613
|
-
if (isLargeGraph) {
|
|
7614
|
-
const isInteractive = linkState.isHovered || linkState.isSelected;
|
|
7615
|
-
if (!isInteractive) {
|
|
7616
|
-
return null;
|
|
7617
|
-
}
|
|
7618
|
-
}
|
|
7619
|
-
const style = this.styleResolver.resolveLinkStyle({
|
|
7620
|
-
link,
|
|
7621
|
-
isHovered: linkState.isHovered,
|
|
7622
|
-
isSelected: linkState.isSelected
|
|
7623
|
-
});
|
|
7624
|
-
return style.label || null;
|
|
7625
|
-
},
|
|
7626
|
-
(link) => this.getLinkMidpoint(link),
|
|
7627
|
-
(linkId) => {
|
|
7628
|
-
let linkState = linkStateCache.get(linkId);
|
|
7629
|
-
if (!linkState) {
|
|
7630
|
-
const link = links.find((l) => this.getLinkId(l) === linkId);
|
|
7631
|
-
if (link) {
|
|
7632
|
-
linkState = {
|
|
7633
|
-
isHovered: this.isLinkHovered(link),
|
|
7634
|
-
isSelected: this.isLinkSelected(link)
|
|
7635
|
-
};
|
|
7636
|
-
linkStateCache.set(linkId, linkState);
|
|
7637
|
-
}
|
|
7638
|
-
}
|
|
7639
|
-
return linkState?.isHovered || false;
|
|
7640
|
-
},
|
|
7641
|
-
(linkId) => {
|
|
7642
|
-
let linkState = linkStateCache.get(linkId);
|
|
7643
|
-
if (!linkState) {
|
|
7644
|
-
const link = links.find((l) => this.getLinkId(l) === linkId);
|
|
7645
|
-
if (link) {
|
|
7646
|
-
linkState = {
|
|
7647
|
-
isHovered: this.isLinkHovered(link),
|
|
7648
|
-
isSelected: this.isLinkSelected(link)
|
|
7649
|
-
};
|
|
7650
|
-
linkStateCache.set(linkId, linkState);
|
|
7651
|
-
}
|
|
7652
|
-
}
|
|
7653
|
-
return linkState?.isSelected || false;
|
|
7654
|
-
}
|
|
7655
|
-
);
|
|
7656
8955
|
}
|
|
7657
8956
|
renderNodesLayer(ctx, nodes) {
|
|
7658
8957
|
if (!this.config || !this.styleResolver) return;
|
|
@@ -7664,9 +8963,11 @@ var Renderer = class {
|
|
|
7664
8963
|
(nodeId) => {
|
|
7665
8964
|
let nodeState = nodeStateCache.get(nodeId);
|
|
7666
8965
|
if (!nodeState) {
|
|
8966
|
+
const interactionState = this.interactionResolver.getNodeState(nodeId);
|
|
7667
8967
|
nodeState = {
|
|
7668
|
-
isHovered:
|
|
7669
|
-
isSelected:
|
|
8968
|
+
isHovered: interactionState.isHovered,
|
|
8969
|
+
isSelected: interactionState.isSelected,
|
|
8970
|
+
isHighlighted: this.stateManager.isNodeHighlighted(nodeId)
|
|
7670
8971
|
};
|
|
7671
8972
|
nodeStateCache.set(nodeId, nodeState);
|
|
7672
8973
|
}
|
|
@@ -7675,13 +8976,28 @@ var Renderer = class {
|
|
|
7675
8976
|
(nodeId) => {
|
|
7676
8977
|
let nodeState = nodeStateCache.get(nodeId);
|
|
7677
8978
|
if (!nodeState) {
|
|
8979
|
+
const interactionState = this.interactionResolver.getNodeState(nodeId);
|
|
7678
8980
|
nodeState = {
|
|
7679
|
-
isHovered:
|
|
7680
|
-
isSelected:
|
|
8981
|
+
isHovered: interactionState.isHovered,
|
|
8982
|
+
isSelected: interactionState.isSelected,
|
|
8983
|
+
isHighlighted: this.stateManager.isNodeHighlighted(nodeId)
|
|
7681
8984
|
};
|
|
7682
8985
|
nodeStateCache.set(nodeId, nodeState);
|
|
7683
8986
|
}
|
|
7684
8987
|
return nodeState.isSelected;
|
|
8988
|
+
},
|
|
8989
|
+
(nodeId) => {
|
|
8990
|
+
let nodeState = nodeStateCache.get(nodeId);
|
|
8991
|
+
if (!nodeState) {
|
|
8992
|
+
const interactionState = this.interactionResolver.getNodeState(nodeId);
|
|
8993
|
+
nodeState = {
|
|
8994
|
+
isHovered: interactionState.isHovered,
|
|
8995
|
+
isSelected: interactionState.isSelected,
|
|
8996
|
+
isHighlighted: this.stateManager.isNodeHighlighted(nodeId)
|
|
8997
|
+
};
|
|
8998
|
+
nodeStateCache.set(nodeId, nodeState);
|
|
8999
|
+
}
|
|
9000
|
+
return nodeState.isHighlighted;
|
|
7685
9001
|
}
|
|
7686
9002
|
);
|
|
7687
9003
|
}
|
|
@@ -7704,10 +9020,7 @@ var Renderer = class {
|
|
|
7704
9020
|
(nodeId) => {
|
|
7705
9021
|
let nodeState = nodeStateCache.get(nodeId);
|
|
7706
9022
|
if (!nodeState) {
|
|
7707
|
-
nodeState =
|
|
7708
|
-
isHovered: this.isNodeHovered(nodeId),
|
|
7709
|
-
isSelected: this.isNodeSelected(nodeId)
|
|
7710
|
-
};
|
|
9023
|
+
nodeState = this.interactionResolver.getNodeState(nodeId);
|
|
7711
9024
|
nodeStateCache.set(nodeId, nodeState);
|
|
7712
9025
|
}
|
|
7713
9026
|
return nodeState.isHovered;
|
|
@@ -7715,28 +9028,33 @@ var Renderer = class {
|
|
|
7715
9028
|
(nodeId) => {
|
|
7716
9029
|
let nodeState = nodeStateCache.get(nodeId);
|
|
7717
9030
|
if (!nodeState) {
|
|
7718
|
-
nodeState =
|
|
7719
|
-
isHovered: this.isNodeHovered(nodeId),
|
|
7720
|
-
isSelected: this.isNodeSelected(nodeId)
|
|
7721
|
-
};
|
|
9031
|
+
nodeState = this.interactionResolver.getNodeState(nodeId);
|
|
7722
9032
|
nodeStateCache.set(nodeId, nodeState);
|
|
7723
9033
|
}
|
|
7724
9034
|
return nodeState.isSelected;
|
|
7725
9035
|
}
|
|
7726
9036
|
);
|
|
7727
9037
|
}
|
|
9038
|
+
/**
|
|
9039
|
+
* Get the state manager for highlight operations
|
|
9040
|
+
*/
|
|
9041
|
+
getStateManager() {
|
|
9042
|
+
return this.stateManager;
|
|
9043
|
+
}
|
|
7728
9044
|
/**
|
|
7729
9045
|
* Destroy renderer and clean up resources
|
|
7730
9046
|
*/
|
|
7731
9047
|
destroy() {
|
|
7732
9048
|
try {
|
|
9049
|
+
this.zIndexRenderer.destroy();
|
|
9050
|
+
this.zoomRenderer.destroy();
|
|
9051
|
+
this.hitDetectionRenderer.destroy();
|
|
9052
|
+
this.dragOptimizer.destroy();
|
|
7733
9053
|
this.config = void 0;
|
|
7734
9054
|
this.canvasState = void 0;
|
|
7735
9055
|
this.hoverManager = void 0;
|
|
7736
9056
|
this.styleResolver = void 0;
|
|
7737
|
-
this.
|
|
7738
|
-
this.shadowCanvasDirty = false;
|
|
7739
|
-
this.lastShadowRenderTime = 0;
|
|
9057
|
+
this.stateManager.destroy();
|
|
7740
9058
|
} catch (error) {
|
|
7741
9059
|
ErrorHandler.logError(error);
|
|
7742
9060
|
}
|
|
@@ -8187,13 +9505,22 @@ var V2Graph = class {
|
|
|
8187
9505
|
// pointerManager: this.pointerManager,
|
|
8188
9506
|
physicsManager: this.physicsManager,
|
|
8189
9507
|
hoverManager: this.hoverManager,
|
|
9508
|
+
renderer: this.renderer,
|
|
9509
|
+
// Pass renderer for drag state management
|
|
8190
9510
|
onRender: () => this.renderer.renderWithTransform()
|
|
8191
9511
|
});
|
|
8192
9512
|
this.coordinateDragHover();
|
|
8193
9513
|
this.zoomManager.initialize({
|
|
8194
9514
|
canvas: canvasState.canvas,
|
|
8195
9515
|
canvasManager: this.canvasManager,
|
|
8196
|
-
|
|
9516
|
+
renderer: this.renderer,
|
|
9517
|
+
// Pass renderer for zoom state management
|
|
9518
|
+
onRender: () => {
|
|
9519
|
+
this.renderer.renderWithTransform();
|
|
9520
|
+
},
|
|
9521
|
+
onZoomEnd: () => {
|
|
9522
|
+
this.renderer.renderWithTransform();
|
|
9523
|
+
},
|
|
8197
9524
|
isOverEntity: () => {
|
|
8198
9525
|
const hoverState = this.hoverManager.getHoverState();
|
|
8199
9526
|
return hoverState.currentHovered !== null;
|
|
@@ -8705,6 +10032,76 @@ var V2Graph = class {
|
|
|
8705
10032
|
}
|
|
8706
10033
|
});
|
|
8707
10034
|
}
|
|
10035
|
+
/**
|
|
10036
|
+
* Highlight a node by ID
|
|
10037
|
+
*/
|
|
10038
|
+
highlightNode(nodeId) {
|
|
10039
|
+
try {
|
|
10040
|
+
if (!this.config) {
|
|
10041
|
+
throw new ValidationError("Graph not initialized");
|
|
10042
|
+
}
|
|
10043
|
+
this.renderer.getStateManager().highlightNode(nodeId);
|
|
10044
|
+
this.renderer.renderWithTransform();
|
|
10045
|
+
} catch (error) {
|
|
10046
|
+
ErrorHandler.logError(error, { nodeId });
|
|
10047
|
+
}
|
|
10048
|
+
}
|
|
10049
|
+
/**
|
|
10050
|
+
* Highlight multiple nodes by IDs
|
|
10051
|
+
*/
|
|
10052
|
+
highlightNodes(nodeIds) {
|
|
10053
|
+
try {
|
|
10054
|
+
if (!this.config) {
|
|
10055
|
+
throw new ValidationError("Graph not initialized");
|
|
10056
|
+
}
|
|
10057
|
+
this.renderer.getStateManager().highlightNodes(nodeIds);
|
|
10058
|
+
this.renderer.renderWithTransform();
|
|
10059
|
+
} catch (error) {
|
|
10060
|
+
ErrorHandler.logError(error, { nodeIds });
|
|
10061
|
+
}
|
|
10062
|
+
}
|
|
10063
|
+
/**
|
|
10064
|
+
* Remove highlight from a node
|
|
10065
|
+
*/
|
|
10066
|
+
unhighlightNode(nodeId) {
|
|
10067
|
+
try {
|
|
10068
|
+
if (!this.config) {
|
|
10069
|
+
throw new ValidationError("Graph not initialized");
|
|
10070
|
+
}
|
|
10071
|
+
this.renderer.getStateManager().unhighlightNode(nodeId);
|
|
10072
|
+
this.renderer.renderWithTransform();
|
|
10073
|
+
} catch (error) {
|
|
10074
|
+
ErrorHandler.logError(error, { nodeId });
|
|
10075
|
+
}
|
|
10076
|
+
}
|
|
10077
|
+
/**
|
|
10078
|
+
* Clear all node highlights
|
|
10079
|
+
*/
|
|
10080
|
+
clearHighlights() {
|
|
10081
|
+
try {
|
|
10082
|
+
if (!this.config) {
|
|
10083
|
+
throw new ValidationError("Graph not initialized");
|
|
10084
|
+
}
|
|
10085
|
+
this.renderer.getStateManager().clearHighlights();
|
|
10086
|
+
this.renderer.renderWithTransform();
|
|
10087
|
+
} catch (error) {
|
|
10088
|
+
ErrorHandler.logError(error);
|
|
10089
|
+
}
|
|
10090
|
+
}
|
|
10091
|
+
/**
|
|
10092
|
+
* Get all highlighted node IDs
|
|
10093
|
+
*/
|
|
10094
|
+
getHighlightedNodes() {
|
|
10095
|
+
try {
|
|
10096
|
+
if (!this.config) {
|
|
10097
|
+
throw new ValidationError("Graph not initialized");
|
|
10098
|
+
}
|
|
10099
|
+
return this.renderer.getStateManager().getHighlightedNodes();
|
|
10100
|
+
} catch (error) {
|
|
10101
|
+
ErrorHandler.logError(error);
|
|
10102
|
+
return /* @__PURE__ */ new Set();
|
|
10103
|
+
}
|
|
10104
|
+
}
|
|
8708
10105
|
/**
|
|
8709
10106
|
* Destroy the graph and clean up resources
|
|
8710
10107
|
*/
|