polly-graph 0.2.3 → 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 +2351 -900
- package/dist/index.css +2 -0
- package/dist/index.d.cts +280 -140
- package/dist/index.d.ts +280 -140
- package/dist/index.js +2351 -900
- 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,15 +1986,278 @@ 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;
|
|
2060
2252
|
config;
|
|
2061
2253
|
simulationStartTime;
|
|
2062
2254
|
simulationEndTime;
|
|
2063
|
-
cooldownTimer;
|
|
2064
2255
|
hasInitialAutoFitCompleted = false;
|
|
2065
2256
|
timerManager;
|
|
2257
|
+
isVisibilityListenerAttached = false;
|
|
2258
|
+
stateManager = new StateManager();
|
|
2259
|
+
isWarmingUp = false;
|
|
2260
|
+
warmupSteps = 0;
|
|
2066
2261
|
constructor(timerManager) {
|
|
2067
2262
|
this.timerManager = timerManager;
|
|
2068
2263
|
}
|
|
@@ -2076,34 +2271,52 @@ var PhysicsManager = class {
|
|
|
2076
2271
|
if (typeof config.onTick !== "function") {
|
|
2077
2272
|
throw new ValidationError("onTick callback is required and must be a function");
|
|
2078
2273
|
}
|
|
2274
|
+
if (!this.isVisibilityListenerAttached) {
|
|
2275
|
+
document.addEventListener("visibilitychange", this.handleVisibilityChange);
|
|
2276
|
+
this.isVisibilityListenerAttached = true;
|
|
2277
|
+
}
|
|
2079
2278
|
this.config = config;
|
|
2080
2279
|
this.simulationStartTime = performance.now();
|
|
2081
2280
|
const nodeCount = config.nodes.length;
|
|
2082
|
-
const graphArea = config.width * config.height;
|
|
2281
|
+
const graphArea = Math.max(config.width * config.height, 1);
|
|
2083
2282
|
const nodeDensity = nodeCount / (graphArea / 1e5);
|
|
2084
2283
|
const densityFactor = Math.min(nodeDensity, 2);
|
|
2085
2284
|
const baseVelocityDecay = 0.4;
|
|
2086
2285
|
const adaptiveVelocityDecay = Math.min(baseVelocityDecay + densityFactor * 0.2, 0.8);
|
|
2087
2286
|
const baseAlphaDecay = 0.02;
|
|
2088
2287
|
const adaptiveAlphaDecay = Math.min(baseAlphaDecay + densityFactor * 0.01, 0.05);
|
|
2288
|
+
if (this.simulation) {
|
|
2289
|
+
this.simulation.stop();
|
|
2290
|
+
}
|
|
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;
|
|
2089
2298
|
this.simulation = simulation_default(config.nodes).force(
|
|
2090
2299
|
"link",
|
|
2091
|
-
link_default(config.links).id((d) => d.id).distance(
|
|
2092
|
-
// Much weaker link strength to allow repulsion to work
|
|
2300
|
+
link_default(config.links).id((d) => d.id).distance(linkDistance).strength(linkStrength).iterations(1)
|
|
2093
2301
|
).force(
|
|
2094
2302
|
"charge",
|
|
2095
|
-
manyBody_default().strength(
|
|
2096
|
-
// Adaptive repulsion strength
|
|
2097
|
-
// .distanceMin(1) // Minimum distance for repulsion
|
|
2098
|
-
// .distanceMax(Math.max(300, 600 - densityFactor * 100)) // Reduce max distance for dense graphs
|
|
2303
|
+
manyBody_default().strength(chargeStrength).theta(nodeCount > 6e3 ? 0.8 : 0.9).distanceMax(nodeCount > 1e4 ? 800 : nodeCount > 6e3 ? 700 : 1e3)
|
|
2099
2304
|
).force(
|
|
2100
2305
|
"collision",
|
|
2101
|
-
collide_default().radius((node) =>
|
|
2306
|
+
collide_default().radius((node) => (node.style?.radius ?? 20) + collisionRadius).strength(1).iterations(collisionIterations)
|
|
2102
2307
|
).force(
|
|
2103
2308
|
"center",
|
|
2104
|
-
center_default(0, 0).strength(
|
|
2105
|
-
|
|
2106
|
-
|
|
2309
|
+
center_default(0, 0).strength(centerStrength)
|
|
2310
|
+
).velocityDecay(
|
|
2311
|
+
nodeCount > 1e4 ? 0.4 : nodeCount > 6e3 ? 0.5 : adaptiveVelocityDecay
|
|
2312
|
+
).alphaDecay(
|
|
2313
|
+
nodeCount > 1e4 ? 0.01 : nodeCount > 6e3 ? 0.02 : adaptiveAlphaDecay
|
|
2314
|
+
).alphaMin(
|
|
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
|
+
}
|
|
2107
2320
|
if (config.cooldownTime) {
|
|
2108
2321
|
this.setupCooldownTimer(config.cooldownTime);
|
|
2109
2322
|
}
|
|
@@ -2116,12 +2329,64 @@ var PhysicsManager = class {
|
|
|
2116
2329
|
throw error;
|
|
2117
2330
|
}
|
|
2118
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
|
+
}
|
|
2119
2383
|
/**
|
|
2120
2384
|
* Handle simulation end
|
|
2121
2385
|
*/
|
|
2122
2386
|
handleSimulationEnd() {
|
|
2123
2387
|
try {
|
|
2124
2388
|
this.simulationEndTime = performance.now();
|
|
2389
|
+
this.isWarmingUp = false;
|
|
2125
2390
|
if (this.config?.onEnd) {
|
|
2126
2391
|
this.config.onEnd();
|
|
2127
2392
|
}
|
|
@@ -2168,11 +2433,15 @@ var PhysicsManager = class {
|
|
|
2168
2433
|
/**
|
|
2169
2434
|
* Reheat simulation for drag interactions
|
|
2170
2435
|
*/
|
|
2171
|
-
reheat(alphaTarget
|
|
2172
|
-
if (!this.simulation)
|
|
2436
|
+
reheat(alphaTarget) {
|
|
2437
|
+
if (!this.simulation || !this.config) {
|
|
2438
|
+
return;
|
|
2439
|
+
}
|
|
2440
|
+
const nodeCount = this.config.nodes.length;
|
|
2441
|
+
const effectiveAlpha = alphaTarget ?? (nodeCount > 1e4 ? 0.03 : nodeCount > 5e3 ? 0.05 : nodeCount > 2e3 ? 0.08 : 0.15);
|
|
2173
2442
|
try {
|
|
2174
|
-
this.simulation.alphaTarget(
|
|
2175
|
-
if (this.config
|
|
2443
|
+
this.simulation.alphaTarget(effectiveAlpha).restart();
|
|
2444
|
+
if (this.config.cooldownTime) {
|
|
2176
2445
|
this.setupCooldownTimer(this.config.cooldownTime);
|
|
2177
2446
|
}
|
|
2178
2447
|
} catch (error) {
|
|
@@ -2242,14 +2511,14 @@ var PhysicsManager = class {
|
|
|
2242
2511
|
adjustLinkDistancesForVisualShortening() {
|
|
2243
2512
|
if (!this.simulation || !this.config) return;
|
|
2244
2513
|
try {
|
|
2514
|
+
const baseDistance = this.calculateBaseDistance();
|
|
2245
2515
|
const linkForce = this.simulation.force("link");
|
|
2246
2516
|
if (linkForce) {
|
|
2247
2517
|
linkForce.distance((link) => {
|
|
2248
|
-
const
|
|
2249
|
-
const
|
|
2250
|
-
const
|
|
2251
|
-
const
|
|
2252
|
-
const targetRadius = this.getNodeRadius(targetNode);
|
|
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;
|
|
2520
|
+
const sourceRadius = sourceNode?.style?.radius ?? 20;
|
|
2521
|
+
const targetRadius = targetNode?.style?.radius ?? 20;
|
|
2253
2522
|
const arrowLength = this.getLinkArrowLength(link);
|
|
2254
2523
|
const visualCompensation = sourceRadius + targetRadius + arrowLength;
|
|
2255
2524
|
const spacingBuffer = Math.max(20, (sourceRadius + targetRadius) * 0.5);
|
|
@@ -2265,24 +2534,13 @@ var PhysicsManager = class {
|
|
|
2265
2534
|
* Calculate base distance based on graph size and node count
|
|
2266
2535
|
*/
|
|
2267
2536
|
calculateBaseDistance() {
|
|
2268
|
-
if (!this.config) return
|
|
2537
|
+
if (!this.config) return 150;
|
|
2269
2538
|
const nodeCount = this.config.nodes.length;
|
|
2270
|
-
const graphArea = this.config.width * this.config.height;
|
|
2539
|
+
const graphArea = Math.max(this.config.width * this.config.height, 1);
|
|
2271
2540
|
const nodeAreaRatio = nodeCount / (graphArea / 1e4);
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
* Find node by ID
|
|
2276
|
-
*/
|
|
2277
|
-
findNodeById(id2) {
|
|
2278
|
-
return this.config?.nodes.find((node) => node.id === id2);
|
|
2279
|
-
}
|
|
2280
|
-
/**
|
|
2281
|
-
* Get node radius from style or default
|
|
2282
|
-
*/
|
|
2283
|
-
getNodeRadius(node) {
|
|
2284
|
-
if (!node) return 20;
|
|
2285
|
-
return node.style?.radius ?? 20;
|
|
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);
|
|
2286
2544
|
}
|
|
2287
2545
|
/**
|
|
2288
2546
|
* Get arrow length from link style or default
|
|
@@ -2294,15 +2552,38 @@ var PhysicsManager = class {
|
|
|
2294
2552
|
return linkStyle?.arrow?.size ?? 8;
|
|
2295
2553
|
}
|
|
2296
2554
|
/**
|
|
2297
|
-
* Initialize node positions
|
|
2555
|
+
* Initialize node positions with improved distribution for smoother startup
|
|
2298
2556
|
*/
|
|
2299
2557
|
initializePositions() {
|
|
2300
2558
|
if (!this.config) return;
|
|
2301
2559
|
try {
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
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;
|
|
2567
|
+
if (node.x == null || node.y == null) {
|
|
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
|
+
}
|
|
2306
2587
|
}
|
|
2307
2588
|
}
|
|
2308
2589
|
} catch (error) {
|
|
@@ -2348,33 +2629,57 @@ var PhysicsManager = class {
|
|
|
2348
2629
|
* Pause the simulation
|
|
2349
2630
|
*/
|
|
2350
2631
|
pause() {
|
|
2351
|
-
if (this.simulation) {
|
|
2352
|
-
|
|
2632
|
+
if (!this.simulation) {
|
|
2633
|
+
return;
|
|
2353
2634
|
}
|
|
2635
|
+
this.timerManager.clearTimer("simulationCooldown");
|
|
2636
|
+
this.simulation.stop();
|
|
2354
2637
|
}
|
|
2355
2638
|
/**
|
|
2356
2639
|
* Resume the simulation
|
|
2357
2640
|
*/
|
|
2358
2641
|
resume() {
|
|
2359
|
-
if (this.simulation) {
|
|
2360
|
-
|
|
2642
|
+
if (!this.simulation) {
|
|
2643
|
+
return;
|
|
2644
|
+
}
|
|
2645
|
+
const nodeCount = this.config?.nodes.length ?? 0;
|
|
2646
|
+
const alpha = nodeCount > 1e4 ? 0.05 : nodeCount > 5e3 ? 0.08 : nodeCount > 2e3 ? 0.12 : 0.3;
|
|
2647
|
+
this.simulation.alpha(alpha).alphaTarget(0).restart();
|
|
2648
|
+
if (this.config?.cooldownTime) {
|
|
2649
|
+
this.setupCooldownTimer(this.config.cooldownTime);
|
|
2361
2650
|
}
|
|
2362
2651
|
}
|
|
2652
|
+
handleVisibilityChange = () => {
|
|
2653
|
+
if (document.visibilityState !== "visible") {
|
|
2654
|
+
this.pause();
|
|
2655
|
+
} else {
|
|
2656
|
+
this.resume();
|
|
2657
|
+
if (this.hasInitialAutoFitCompleted && this.simulation && this.simulation.alpha() > 0) {
|
|
2658
|
+
this.hasInitialAutoFitCompleted = false;
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2661
|
+
};
|
|
2363
2662
|
/**
|
|
2364
2663
|
* Destroy physics simulation
|
|
2365
2664
|
*/
|
|
2366
2665
|
destroy() {
|
|
2367
2666
|
try {
|
|
2667
|
+
if (this.isVisibilityListenerAttached) {
|
|
2668
|
+
document.removeEventListener("visibilitychange", this.handleVisibilityChange);
|
|
2669
|
+
this.isVisibilityListenerAttached = false;
|
|
2670
|
+
}
|
|
2368
2671
|
this.timerManager.clearTimer("simulationCooldown");
|
|
2369
|
-
this.cooldownTimer = void 0;
|
|
2370
2672
|
if (this.simulation) {
|
|
2371
2673
|
this.simulation.stop();
|
|
2372
2674
|
this.simulation = void 0;
|
|
2373
2675
|
}
|
|
2374
2676
|
this.config = void 0;
|
|
2677
|
+
this.stateManager.destroy();
|
|
2375
2678
|
this.simulationStartTime = void 0;
|
|
2376
2679
|
this.simulationEndTime = void 0;
|
|
2377
2680
|
this.hasInitialAutoFitCompleted = false;
|
|
2681
|
+
this.isWarmingUp = false;
|
|
2682
|
+
this.warmupSteps = 0;
|
|
2378
2683
|
} catch (error) {
|
|
2379
2684
|
ErrorHandler.logError(error);
|
|
2380
2685
|
}
|
|
@@ -5160,6 +5465,10 @@ var DragManager = class {
|
|
|
5160
5465
|
};
|
|
5161
5466
|
DRAG_CLICK_TOLERANCE_PX = 3;
|
|
5162
5467
|
// Force-graph constant
|
|
5468
|
+
// RAF throttling for smooth drag performance
|
|
5469
|
+
dragRenderPending = false;
|
|
5470
|
+
lastDragRenderTime = 0;
|
|
5471
|
+
pendingAnimationFrame;
|
|
5163
5472
|
/**
|
|
5164
5473
|
* Initialize drag behavior
|
|
5165
5474
|
*/
|
|
@@ -5204,11 +5513,15 @@ var DragManager = class {
|
|
|
5204
5513
|
fy: obj.fy
|
|
5205
5514
|
};
|
|
5206
5515
|
if (!event.active) {
|
|
5207
|
-
this.config.physicsManager.reheat(
|
|
5516
|
+
this.config.physicsManager.reheat();
|
|
5208
5517
|
obj.fx = obj.x;
|
|
5209
5518
|
obj.fy = obj.y;
|
|
5210
5519
|
}
|
|
5211
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
|
+
}
|
|
5212
5525
|
} catch (error) {
|
|
5213
5526
|
ErrorHandler.logError(error, {
|
|
5214
5527
|
nodeId: obj?.id,
|
|
@@ -5241,7 +5554,7 @@ var DragManager = class {
|
|
|
5241
5554
|
this.state.isDragging = true;
|
|
5242
5555
|
this.state.isPointerDragging = true;
|
|
5243
5556
|
obj.__dragged = true;
|
|
5244
|
-
this.
|
|
5557
|
+
this.throttledDragRender();
|
|
5245
5558
|
} catch (error) {
|
|
5246
5559
|
ErrorHandler.logError(error, {
|
|
5247
5560
|
nodeId: obj?.id,
|
|
@@ -5249,6 +5562,24 @@ var DragManager = class {
|
|
|
5249
5562
|
});
|
|
5250
5563
|
}
|
|
5251
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
|
+
}
|
|
5252
5583
|
/**
|
|
5253
5584
|
* Handle drag end
|
|
5254
5585
|
*/
|
|
@@ -5273,6 +5604,9 @@ var DragManager = class {
|
|
|
5273
5604
|
this.config.canvas.classList.remove("grabbable");
|
|
5274
5605
|
this.state.isDragging = false;
|
|
5275
5606
|
this.state.isPointerDragging = false;
|
|
5607
|
+
if (this.config.renderer) {
|
|
5608
|
+
this.config.renderer.setDragState(false);
|
|
5609
|
+
}
|
|
5276
5610
|
if (obj.__dragged) {
|
|
5277
5611
|
delete obj.__dragged;
|
|
5278
5612
|
this.config.onRender();
|
|
@@ -5307,6 +5641,10 @@ var DragManager = class {
|
|
|
5307
5641
|
*/
|
|
5308
5642
|
destroy() {
|
|
5309
5643
|
try {
|
|
5644
|
+
if (this.pendingAnimationFrame !== void 0) {
|
|
5645
|
+
cancelAnimationFrame(this.pendingAnimationFrame);
|
|
5646
|
+
this.pendingAnimationFrame = void 0;
|
|
5647
|
+
}
|
|
5310
5648
|
if (this.config?.canvas) {
|
|
5311
5649
|
select_default2(this.config.canvas).on(".drag", null);
|
|
5312
5650
|
this.config.canvas.classList.remove("grabbable");
|
|
@@ -5326,6 +5664,10 @@ var DragManager = class {
|
|
|
5326
5664
|
var ZoomManager = class {
|
|
5327
5665
|
config;
|
|
5328
5666
|
zoomBehavior;
|
|
5667
|
+
isZooming = false;
|
|
5668
|
+
zoomRenderPending = false;
|
|
5669
|
+
isProgrammaticZoom = false;
|
|
5670
|
+
// Flag for programmatic zoom operations
|
|
5329
5671
|
/**
|
|
5330
5672
|
* Initialize zoom behavior
|
|
5331
5673
|
*/
|
|
@@ -5373,49 +5715,69 @@ var ZoomManager = class {
|
|
|
5373
5715
|
* Handle zoom start (when panning begins)
|
|
5374
5716
|
*/
|
|
5375
5717
|
handleZoomStart(event) {
|
|
5718
|
+
this.isZooming = true;
|
|
5376
5719
|
if (!this.config) return;
|
|
5377
5720
|
try {
|
|
5378
5721
|
event.sourceEvent?.stopPropagation();
|
|
5379
5722
|
event.sourceEvent?.preventDefault();
|
|
5380
|
-
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
|
+
}
|
|
5381
5729
|
} catch (error) {
|
|
5382
5730
|
ErrorHandler.logError(error);
|
|
5383
5731
|
}
|
|
5384
5732
|
}
|
|
5385
5733
|
/**
|
|
5386
|
-
* Handle zoom events
|
|
5734
|
+
* Handle zoom events with RAF throttling for smooth performance
|
|
5387
5735
|
*/
|
|
5388
5736
|
handleZoom(event) {
|
|
5389
5737
|
if (!this.config) return;
|
|
5390
|
-
|
|
5391
|
-
|
|
5392
|
-
|
|
5393
|
-
|
|
5394
|
-
this.config.canvasManager.clear();
|
|
5395
|
-
this.config.canvasManager.applyTransform(transform2);
|
|
5396
|
-
this.config.onRender();
|
|
5397
|
-
} catch (error) {
|
|
5398
|
-
ErrorHandler.logError(error, {
|
|
5399
|
-
transform: event.transform
|
|
5400
|
-
});
|
|
5738
|
+
event.sourceEvent?.stopPropagation();
|
|
5739
|
+
event.sourceEvent?.preventDefault();
|
|
5740
|
+
if (this.zoomRenderPending) {
|
|
5741
|
+
return;
|
|
5401
5742
|
}
|
|
5743
|
+
this.zoomRenderPending = true;
|
|
5744
|
+
requestAnimationFrame(() => {
|
|
5745
|
+
this.zoomRenderPending = false;
|
|
5746
|
+
if (!this.config) {
|
|
5747
|
+
return;
|
|
5748
|
+
}
|
|
5749
|
+
this.config.onRender();
|
|
5750
|
+
});
|
|
5402
5751
|
}
|
|
5403
5752
|
/**
|
|
5404
5753
|
* Handle zoom end (when panning ends)
|
|
5405
5754
|
*/
|
|
5406
5755
|
handleZoomEnd(event) {
|
|
5756
|
+
this.isZooming = false;
|
|
5407
5757
|
if (!this.config) return;
|
|
5408
5758
|
try {
|
|
5409
5759
|
event.sourceEvent?.stopPropagation();
|
|
5410
5760
|
event.sourceEvent?.preventDefault();
|
|
5411
|
-
|
|
5412
|
-
|
|
5413
|
-
this.config.canvas.style.cursor = "grab";
|
|
5761
|
+
if (this.config.renderer && !this.isProgrammaticZoom) {
|
|
5762
|
+
this.config.renderer.setZoomState(false);
|
|
5414
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();
|
|
5772
|
+
}
|
|
5773
|
+
this.isProgrammaticZoom = false;
|
|
5415
5774
|
} catch (error) {
|
|
5416
5775
|
ErrorHandler.logError(error);
|
|
5417
5776
|
}
|
|
5418
5777
|
}
|
|
5778
|
+
isCurrentlyZooming() {
|
|
5779
|
+
return this.isZooming;
|
|
5780
|
+
}
|
|
5419
5781
|
/**
|
|
5420
5782
|
* Get zoom behavior instance
|
|
5421
5783
|
*/
|
|
@@ -5447,6 +5809,7 @@ var ZoomManager = class {
|
|
|
5447
5809
|
zoomIn(factor = 1.5, center) {
|
|
5448
5810
|
if (!this.config?.canvas || !this.zoomBehavior) return;
|
|
5449
5811
|
try {
|
|
5812
|
+
this.isProgrammaticZoom = true;
|
|
5450
5813
|
const canvas = this.config.canvas;
|
|
5451
5814
|
if (center) {
|
|
5452
5815
|
select_default2(canvas).transition().duration(300).call(this.zoomBehavior.scaleBy, factor, center);
|
|
@@ -5469,6 +5832,7 @@ var ZoomManager = class {
|
|
|
5469
5832
|
resetZoom(duration = 500) {
|
|
5470
5833
|
if (!this.config?.canvas || !this.zoomBehavior) return;
|
|
5471
5834
|
try {
|
|
5835
|
+
this.isProgrammaticZoom = true;
|
|
5472
5836
|
const canvasDims = this.config.canvasManager.getDimensions();
|
|
5473
5837
|
const transform2 = identity2.translate(canvasDims.width / 2, canvasDims.height / 2);
|
|
5474
5838
|
select_default2(this.config.canvas).transition().duration(duration).call(this.zoomBehavior.transform, transform2);
|
|
@@ -5482,6 +5846,7 @@ var ZoomManager = class {
|
|
|
5482
5846
|
setTransform(transform2, duration = 0) {
|
|
5483
5847
|
if (!this.config?.canvas || !this.zoomBehavior) return;
|
|
5484
5848
|
try {
|
|
5849
|
+
this.isProgrammaticZoom = true;
|
|
5485
5850
|
const selection2 = select_default2(this.config.canvas);
|
|
5486
5851
|
const zoomTransform = identity2.translate(transform2.x, transform2.y).scale(transform2.k);
|
|
5487
5852
|
if (duration > 0) {
|
|
@@ -5503,6 +5868,7 @@ var ZoomManager = class {
|
|
|
5503
5868
|
return;
|
|
5504
5869
|
}
|
|
5505
5870
|
try {
|
|
5871
|
+
this.isProgrammaticZoom = true;
|
|
5506
5872
|
const canvas = this.config.canvas;
|
|
5507
5873
|
const transitionDuration = 750;
|
|
5508
5874
|
const canvasDims = this.config.canvasManager.getDimensions();
|
|
@@ -5591,6 +5957,8 @@ var HoverManager = class {
|
|
|
5591
5957
|
flushShadowCanvas;
|
|
5592
5958
|
hasValidPointerPosition = false;
|
|
5593
5959
|
containerWarningLogged = false;
|
|
5960
|
+
// Store bound handlers for proper cleanup
|
|
5961
|
+
boundHandlers = /* @__PURE__ */ new Map();
|
|
5594
5962
|
/**
|
|
5595
5963
|
* Initialize hover manager with force-graph pattern
|
|
5596
5964
|
*/
|
|
@@ -5622,7 +5990,7 @@ var HoverManager = class {
|
|
|
5622
5990
|
console.error("Cannot add pointer event listeners - container is null");
|
|
5623
5991
|
return;
|
|
5624
5992
|
}
|
|
5625
|
-
|
|
5993
|
+
const eventHandler = (ev) => {
|
|
5626
5994
|
const pointerEvent = ev;
|
|
5627
5995
|
const container = this.container;
|
|
5628
5996
|
if (!container) {
|
|
@@ -5647,7 +6015,9 @@ var HoverManager = class {
|
|
|
5647
6015
|
this.containerWarningLogged = true;
|
|
5648
6016
|
}
|
|
5649
6017
|
}
|
|
5650
|
-
}
|
|
6018
|
+
};
|
|
6019
|
+
this.boundHandlers.set(evType, eventHandler);
|
|
6020
|
+
this.container.addEventListener(evType, eventHandler, { passive: true });
|
|
5651
6021
|
});
|
|
5652
6022
|
}
|
|
5653
6023
|
/**
|
|
@@ -5868,6 +6238,12 @@ var HoverManager = class {
|
|
|
5868
6238
|
*/
|
|
5869
6239
|
destroy() {
|
|
5870
6240
|
try {
|
|
6241
|
+
if (this.container) {
|
|
6242
|
+
this.boundHandlers.forEach((handler, eventType) => {
|
|
6243
|
+
this.container.removeEventListener(eventType, handler);
|
|
6244
|
+
});
|
|
6245
|
+
}
|
|
6246
|
+
this.boundHandlers.clear();
|
|
5871
6247
|
this.eventHandlers.clear();
|
|
5872
6248
|
this.hoverState = {
|
|
5873
6249
|
currentHovered: null,
|
|
@@ -5896,6 +6272,8 @@ var SelectionManager = class {
|
|
|
5896
6272
|
};
|
|
5897
6273
|
eventHandlers = /* @__PURE__ */ new Map();
|
|
5898
6274
|
container;
|
|
6275
|
+
// Store bound handlers for proper cleanup
|
|
6276
|
+
boundHandlers = /* @__PURE__ */ new Map();
|
|
5899
6277
|
/**
|
|
5900
6278
|
* Initialize selection manager
|
|
5901
6279
|
*/
|
|
@@ -5917,14 +6295,19 @@ var SelectionManager = class {
|
|
|
5917
6295
|
*/
|
|
5918
6296
|
setupSelectionListeners() {
|
|
5919
6297
|
if (!this.container || !this.canvasState) return;
|
|
5920
|
-
|
|
6298
|
+
const clickHandler = (event) => {
|
|
5921
6299
|
this.handleSelectionClick(event);
|
|
5922
|
-
}
|
|
5923
|
-
|
|
5924
|
-
|
|
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") {
|
|
5925
6306
|
this.clearSelection();
|
|
5926
6307
|
}
|
|
5927
|
-
}
|
|
6308
|
+
};
|
|
6309
|
+
this.boundHandlers.set("keydown", keydownHandler);
|
|
6310
|
+
document.addEventListener("keydown", keydownHandler);
|
|
5928
6311
|
}
|
|
5929
6312
|
/**
|
|
5930
6313
|
* Handle selection click
|
|
@@ -6156,14 +6539,13 @@ var SelectionManager = class {
|
|
|
6156
6539
|
*/
|
|
6157
6540
|
destroy() {
|
|
6158
6541
|
try {
|
|
6159
|
-
if (this.container) {
|
|
6160
|
-
this.container.removeEventListener("click", this.
|
|
6542
|
+
if (this.container && this.boundHandlers.has("click")) {
|
|
6543
|
+
this.container.removeEventListener("click", this.boundHandlers.get("click"));
|
|
6161
6544
|
}
|
|
6162
|
-
|
|
6163
|
-
|
|
6164
|
-
|
|
6165
|
-
|
|
6166
|
-
});
|
|
6545
|
+
if (this.boundHandlers.has("keydown")) {
|
|
6546
|
+
document.removeEventListener("keydown", this.boundHandlers.get("keydown"));
|
|
6547
|
+
}
|
|
6548
|
+
this.boundHandlers.clear();
|
|
6167
6549
|
this.eventHandlers.clear();
|
|
6168
6550
|
this.selectionState = {
|
|
6169
6551
|
selectedNode: null,
|
|
@@ -6241,7 +6623,7 @@ var NodesRenderer = class {
|
|
|
6241
6623
|
/**
|
|
6242
6624
|
* Render nodes to canvas using StyleResolver with performance metrics
|
|
6243
6625
|
*/
|
|
6244
|
-
static renderWithStyleResolver(ctx, nodes, styleResolver, isNodeHovered, isNodeSelected, performanceMetrics) {
|
|
6626
|
+
static renderWithStyleResolver(ctx, nodes, styleResolver, isNodeHovered, isNodeSelected, isNodeHighlighted, performanceMetrics) {
|
|
6245
6627
|
try {
|
|
6246
6628
|
for (const node of nodes) {
|
|
6247
6629
|
const x3 = node.x;
|
|
@@ -6249,6 +6631,7 @@ var NodesRenderer = class {
|
|
|
6249
6631
|
const hoverStart = performance.now();
|
|
6250
6632
|
const isHovered = isNodeHovered(node.id);
|
|
6251
6633
|
const isSelected = isNodeSelected ? isNodeSelected(node.id) : false;
|
|
6634
|
+
const isHighlighted = isNodeHighlighted ? isNodeHighlighted(node.id) : false;
|
|
6252
6635
|
if (performanceMetrics) {
|
|
6253
6636
|
performanceMetrics.hoverChecks += performance.now() - hoverStart;
|
|
6254
6637
|
}
|
|
@@ -6256,7 +6639,8 @@ var NodesRenderer = class {
|
|
|
6256
6639
|
const style = styleResolver.resolveNodeStyle({
|
|
6257
6640
|
node,
|
|
6258
6641
|
isHovered,
|
|
6259
|
-
isSelected
|
|
6642
|
+
isSelected,
|
|
6643
|
+
isHighlighted
|
|
6260
6644
|
});
|
|
6261
6645
|
if (performanceMetrics) {
|
|
6262
6646
|
performanceMetrics.styleResolution += performance.now() - styleStart;
|
|
@@ -6406,6 +6790,514 @@ var NodeLabelsRenderer = class {
|
|
|
6406
6790
|
}
|
|
6407
6791
|
};
|
|
6408
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
|
+
|
|
6409
7301
|
// src/v2/rendering/link-labels-renderer.ts
|
|
6410
7302
|
var LinkLabelsRenderer = class {
|
|
6411
7303
|
static textMetricsCache = /* @__PURE__ */ new Map();
|
|
@@ -6612,107 +7504,30 @@ var LinkLabelsRenderer = class {
|
|
|
6612
7504
|
}
|
|
6613
7505
|
};
|
|
6614
7506
|
|
|
6615
|
-
// src/v2/rendering/renderer.ts
|
|
6616
|
-
var
|
|
7507
|
+
// src/v2/rendering/hit-detection-renderer.ts
|
|
7508
|
+
var HitDetectionRenderer = class {
|
|
6617
7509
|
config;
|
|
6618
|
-
|
|
6619
|
-
|
|
6620
|
-
selectionManager;
|
|
6621
|
-
styleResolver;
|
|
6622
|
-
// Performance optimization: O(1) node lookups
|
|
6623
|
-
nodeMap = /* @__PURE__ */ new Map();
|
|
6624
|
-
// Shadow canvas optimization (Step 6 optimization)
|
|
7510
|
+
stateManager;
|
|
7511
|
+
// Shadow canvas optimization (throttling)
|
|
6625
7512
|
shadowCanvasDirty = true;
|
|
6626
7513
|
lastShadowRenderTime = 0;
|
|
6627
7514
|
SHADOW_RENDER_THROTTLE = 32;
|
|
6628
7515
|
// ~30 FPS max for shadow canvas
|
|
6629
|
-
// Large graph optimization flag
|
|
6630
|
-
hasLoggedLargeGraphOptimization = false;
|
|
6631
|
-
// Performance metrics
|
|
6632
|
-
performanceMetrics = {
|
|
6633
|
-
renderTotal: 0,
|
|
6634
|
-
renderNodes: 0,
|
|
6635
|
-
renderLinks: 0,
|
|
6636
|
-
renderLinkLabels: 0,
|
|
6637
|
-
renderNodeLabels: 0,
|
|
6638
|
-
styleResolution: 0,
|
|
6639
|
-
hoverChecks: 0,
|
|
6640
|
-
canvasCalls: 0,
|
|
6641
|
-
frameCount: 0
|
|
6642
|
-
};
|
|
6643
|
-
// Force-graph pattern: configurable link hover precision
|
|
6644
|
-
linkHoverPrecision = 4;
|
|
6645
|
-
// Default 4px like force-graph
|
|
6646
7516
|
/**
|
|
6647
|
-
* Initialize
|
|
7517
|
+
* Initialize hit detection renderer
|
|
6648
7518
|
*/
|
|
6649
|
-
initialize(config
|
|
6650
|
-
|
|
6651
|
-
|
|
6652
|
-
this.canvasState = canvasState;
|
|
6653
|
-
this.hoverManager = hoverManager;
|
|
6654
|
-
this.selectionManager = selectionManager;
|
|
6655
|
-
this.styleResolver = createStyleResolver(config.interaction);
|
|
6656
|
-
this.buildNodeIndex();
|
|
6657
|
-
} catch (error) {
|
|
6658
|
-
ErrorHandler.logError(error, {
|
|
6659
|
-
nodeCount: config.nodes.length,
|
|
6660
|
-
linkCount: config.links.length
|
|
6661
|
-
});
|
|
6662
|
-
throw error;
|
|
6663
|
-
}
|
|
6664
|
-
}
|
|
6665
|
-
/**
|
|
6666
|
-
* Main render method with performance metrics (Instrumented)
|
|
6667
|
-
*/
|
|
6668
|
-
render() {
|
|
6669
|
-
if (!this.config || !this.canvasState) {
|
|
6670
|
-
throw new RenderError("Renderer not initialized");
|
|
6671
|
-
}
|
|
6672
|
-
const startTime = performance.now();
|
|
6673
|
-
this.performanceMetrics.frameCount++;
|
|
6674
|
-
try {
|
|
6675
|
-
const { ctx } = this.canvasState;
|
|
6676
|
-
this.clearCanvas(ctx);
|
|
6677
|
-
this.renderWithLayersAndMetrics(ctx);
|
|
6678
|
-
this.markShadowCanvasDirty();
|
|
6679
|
-
this.performanceMetrics.renderTotal += performance.now() - startTime;
|
|
6680
|
-
if (this.performanceMetrics.frameCount % 100 === 0) {
|
|
6681
|
-
this.logPerformanceMetrics();
|
|
6682
|
-
}
|
|
6683
|
-
} catch (error) {
|
|
6684
|
-
ErrorHandler.logError(error);
|
|
6685
|
-
throw new RenderError("Failed to render graph", {
|
|
6686
|
-
nodeCount: this.config.nodes.length,
|
|
6687
|
-
linkCount: this.config.links.length,
|
|
6688
|
-
originalError: error.message
|
|
6689
|
-
});
|
|
6690
|
-
}
|
|
6691
|
-
}
|
|
6692
|
-
/**
|
|
6693
|
-
* Render with transform (called during zoom/pan) with shadow canvas dirty marking (Step 6 optimization)
|
|
6694
|
-
*/
|
|
6695
|
-
renderWithTransform() {
|
|
6696
|
-
if (!this.canvasState) return;
|
|
6697
|
-
try {
|
|
6698
|
-
const { canvas, ctx } = this.canvasState;
|
|
6699
|
-
const transform2 = transform(canvas);
|
|
6700
|
-
this.clearCanvas(ctx);
|
|
6701
|
-
this.applyTransform(transform2, ctx);
|
|
6702
|
-
this.renderWithLayers(ctx);
|
|
6703
|
-
this.markShadowCanvasDirty();
|
|
6704
|
-
} catch (error) {
|
|
6705
|
-
ErrorHandler.logError(error);
|
|
6706
|
-
}
|
|
7519
|
+
initialize(config) {
|
|
7520
|
+
this.config = config;
|
|
7521
|
+
this.stateManager = config.stateManager;
|
|
6707
7522
|
}
|
|
6708
7523
|
/**
|
|
6709
|
-
* Render shadow canvas for hit detection with throttling
|
|
7524
|
+
* Render shadow canvas for hit detection with throttling
|
|
6710
7525
|
*/
|
|
6711
7526
|
renderShadowCanvas() {
|
|
6712
|
-
if (!this.
|
|
7527
|
+
if (!this.config) return;
|
|
6713
7528
|
const now2 = Date.now();
|
|
6714
7529
|
try {
|
|
6715
|
-
const { shadowCtx, canvas } = this.
|
|
7530
|
+
const { shadowCtx, canvas } = this.config;
|
|
6716
7531
|
const transform2 = transform(canvas);
|
|
6717
7532
|
this.clearCanvas(shadowCtx);
|
|
6718
7533
|
this.applyTransform(transform2, shadowCtx);
|
|
@@ -6726,13 +7541,13 @@ var Renderer = class {
|
|
|
6726
7541
|
}
|
|
6727
7542
|
}
|
|
6728
7543
|
/**
|
|
6729
|
-
* Mark shadow canvas as dirty for next render
|
|
7544
|
+
* Mark shadow canvas as dirty for next render
|
|
6730
7545
|
*/
|
|
6731
7546
|
markShadowCanvasDirty() {
|
|
6732
7547
|
this.shadowCanvasDirty = true;
|
|
6733
7548
|
}
|
|
6734
7549
|
/**
|
|
6735
|
-
* Force shadow canvas render
|
|
7550
|
+
* Force shadow canvas render
|
|
6736
7551
|
*/
|
|
6737
7552
|
forceShadowCanvasRender() {
|
|
6738
7553
|
this.shadowCanvasDirty = true;
|
|
@@ -6740,96 +7555,53 @@ var Renderer = class {
|
|
|
6740
7555
|
this.renderShadowCanvas();
|
|
6741
7556
|
}
|
|
6742
7557
|
/**
|
|
6743
|
-
* Clear canvas context
|
|
7558
|
+
* Clear shadow canvas context
|
|
6744
7559
|
*/
|
|
6745
7560
|
clearCanvas(ctx) {
|
|
6746
|
-
if (!this.
|
|
7561
|
+
if (!this.config) return;
|
|
6747
7562
|
try {
|
|
6748
|
-
const {
|
|
7563
|
+
const { canvas } = this.config;
|
|
7564
|
+
const { width, height } = canvas;
|
|
6749
7565
|
CanvasUtils.resetTransform(ctx);
|
|
6750
7566
|
ctx.clearRect(0, 0, width, height);
|
|
6751
|
-
} catch {
|
|
6752
|
-
|
|
7567
|
+
} catch (error) {
|
|
7568
|
+
ErrorHandler.logError(error, { message: "Failed to clear shadow canvas" });
|
|
6753
7569
|
}
|
|
6754
7570
|
}
|
|
6755
7571
|
/**
|
|
6756
|
-
* Apply transform to canvas context
|
|
7572
|
+
* Apply transform to shadow canvas context
|
|
6757
7573
|
*/
|
|
6758
7574
|
applyTransform(transform2, ctx) {
|
|
6759
7575
|
try {
|
|
6760
7576
|
CanvasUtils.resetTransform(ctx);
|
|
6761
7577
|
ctx.translate(transform2.x, transform2.y);
|
|
6762
7578
|
ctx.scale(transform2.k, transform2.k);
|
|
6763
|
-
} catch {
|
|
6764
|
-
throw new RenderError("Failed to apply transform");
|
|
6765
|
-
}
|
|
6766
|
-
}
|
|
6767
|
-
/**
|
|
6768
|
-
* Get unique link ID for tracking (consistent with LinkLabelsRenderer)
|
|
6769
|
-
*/
|
|
6770
|
-
getLinkId(link) {
|
|
6771
|
-
const sourceId = typeof link.source === "string" ? link.source : link.source.id;
|
|
6772
|
-
const targetId = typeof link.target === "string" ? link.target : link.target.id;
|
|
6773
|
-
return `${sourceId}->${targetId}`;
|
|
6774
|
-
}
|
|
6775
|
-
/**
|
|
6776
|
-
* Render main canvas nodes
|
|
6777
|
-
*/
|
|
6778
|
-
renderNodes(ctx) {
|
|
6779
|
-
if (!this.config || !this.styleResolver) return;
|
|
6780
|
-
try {
|
|
6781
|
-
const { nodes } = this.config;
|
|
6782
|
-
NodesRenderer.renderWithStyleResolver(
|
|
6783
|
-
ctx,
|
|
6784
|
-
nodes,
|
|
6785
|
-
this.styleResolver,
|
|
6786
|
-
(nodeId) => this.isNodeHovered(nodeId),
|
|
6787
|
-
(nodeId) => this.isNodeSelected(nodeId),
|
|
6788
|
-
this.performanceMetrics
|
|
6789
|
-
);
|
|
6790
|
-
} catch (error) {
|
|
6791
|
-
ErrorHandler.logError(error);
|
|
6792
|
-
throw new RenderError("Failed to render nodes");
|
|
6793
|
-
}
|
|
6794
|
-
}
|
|
6795
|
-
/**
|
|
6796
|
-
* Render node labels
|
|
6797
|
-
*/
|
|
6798
|
-
renderNodeLabels(ctx) {
|
|
6799
|
-
if (!this.config || !this.styleResolver) return;
|
|
6800
|
-
try {
|
|
6801
|
-
const { nodes } = this.config;
|
|
6802
|
-
const defaultNodeStyle = this.styleResolver.resolveNodeStyle({
|
|
6803
|
-
node: { id: "temp" }
|
|
6804
|
-
});
|
|
6805
|
-
NodeLabelsRenderer.render(ctx, nodes, defaultNodeStyle.radius);
|
|
6806
7579
|
} catch (error) {
|
|
6807
|
-
ErrorHandler.logError(error);
|
|
6808
|
-
throw new RenderError("Failed to render node labels");
|
|
7580
|
+
ErrorHandler.logError(error, { message: "Failed to apply transform to shadow canvas" });
|
|
6809
7581
|
}
|
|
6810
7582
|
}
|
|
6811
7583
|
/**
|
|
6812
|
-
* Render shadow links with __indexColor
|
|
7584
|
+
* Render shadow links with __indexColor for hit detection
|
|
6813
7585
|
*/
|
|
6814
7586
|
renderShadowLinks(shadowCtx) {
|
|
6815
|
-
if (!this.config
|
|
7587
|
+
if (!this.config) return;
|
|
6816
7588
|
try {
|
|
6817
|
-
const { links } = this.config;
|
|
6818
|
-
const defaultLinkStyle =
|
|
7589
|
+
const { links, stateManager, styleResolver, linkHoverPrecision = 4 } = this.config;
|
|
7590
|
+
const defaultLinkStyle = styleResolver.resolveLinkStyle({
|
|
6819
7591
|
link: { source: "", target: "" }
|
|
6820
7592
|
});
|
|
6821
7593
|
for (const link of links) {
|
|
6822
7594
|
if (!link.__indexColor) continue;
|
|
6823
|
-
const sourceNode = typeof link.source === "string" ?
|
|
6824
|
-
const targetNode = typeof link.target === "string" ?
|
|
6825
|
-
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) {
|
|
6826
7598
|
const [r, g, b] = link.__indexColorRGB;
|
|
6827
7599
|
const rgbColor = `rgb(${r},${g},${b})`;
|
|
6828
7600
|
const dx = targetNode.x - sourceNode.x;
|
|
6829
7601
|
const dy = targetNode.y - sourceNode.y;
|
|
6830
7602
|
const length = Math.sqrt(dx * dx + dy * dy);
|
|
6831
7603
|
const angle = Math.atan2(dy, dx);
|
|
6832
|
-
const thickness = defaultLinkStyle.strokeWidth +
|
|
7604
|
+
const thickness = defaultLinkStyle.strokeWidth + linkHoverPrecision;
|
|
6833
7605
|
shadowCtx.save();
|
|
6834
7606
|
shadowCtx.translate(sourceNode.x, sourceNode.y);
|
|
6835
7607
|
shadowCtx.rotate(angle);
|
|
@@ -6846,27 +7618,30 @@ var Renderer = class {
|
|
|
6846
7618
|
* Render shadow link labels for hit detection
|
|
6847
7619
|
*/
|
|
6848
7620
|
renderShadowLinkLabels(shadowCtx) {
|
|
6849
|
-
if (!this.config
|
|
7621
|
+
if (!this.config) return;
|
|
6850
7622
|
try {
|
|
7623
|
+
const { links, styleResolver } = this.config;
|
|
6851
7624
|
const linkStateCache = /* @__PURE__ */ new Map();
|
|
6852
7625
|
const linkIdToLinkMap = /* @__PURE__ */ new Map();
|
|
6853
|
-
for (const link of
|
|
7626
|
+
for (const link of links) {
|
|
6854
7627
|
const linkId = this.getLinkId(link);
|
|
6855
7628
|
linkIdToLinkMap.set(linkId, link);
|
|
6856
7629
|
}
|
|
6857
7630
|
const labelPositions = LinkLabelsRenderer.calculateLabelPositions(
|
|
6858
|
-
|
|
7631
|
+
links,
|
|
6859
7632
|
(link) => {
|
|
6860
7633
|
const linkId = this.getLinkId(link);
|
|
6861
7634
|
let linkState = linkStateCache.get(linkId);
|
|
6862
7635
|
if (!linkState) {
|
|
6863
7636
|
linkState = {
|
|
6864
|
-
isHovered:
|
|
6865
|
-
|
|
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
|
|
6866
7641
|
};
|
|
6867
7642
|
linkStateCache.set(linkId, linkState);
|
|
6868
7643
|
}
|
|
6869
|
-
const style =
|
|
7644
|
+
const style = styleResolver.resolveLinkStyle({
|
|
6870
7645
|
link,
|
|
6871
7646
|
isHovered: linkState.isHovered,
|
|
6872
7647
|
isSelected: linkState.isSelected
|
|
@@ -6874,34 +7649,10 @@ var Renderer = class {
|
|
|
6874
7649
|
return style.label || null;
|
|
6875
7650
|
},
|
|
6876
7651
|
(link) => this.getLinkMidpoint(link),
|
|
6877
|
-
(
|
|
6878
|
-
|
|
6879
|
-
|
|
6880
|
-
|
|
6881
|
-
if (link) {
|
|
6882
|
-
linkState = {
|
|
6883
|
-
isHovered: this.isLinkHovered(link),
|
|
6884
|
-
isSelected: this.isLinkSelected(link)
|
|
6885
|
-
};
|
|
6886
|
-
linkStateCache.set(linkId, linkState);
|
|
6887
|
-
}
|
|
6888
|
-
}
|
|
6889
|
-
return linkState?.isHovered || false;
|
|
6890
|
-
},
|
|
6891
|
-
(linkId) => {
|
|
6892
|
-
let linkState = linkStateCache.get(linkId);
|
|
6893
|
-
if (!linkState) {
|
|
6894
|
-
const link = linkIdToLinkMap.get(linkId);
|
|
6895
|
-
if (link) {
|
|
6896
|
-
linkState = {
|
|
6897
|
-
isHovered: this.isLinkHovered(link),
|
|
6898
|
-
isSelected: this.isLinkSelected(link)
|
|
6899
|
-
};
|
|
6900
|
-
linkStateCache.set(linkId, linkState);
|
|
6901
|
-
}
|
|
6902
|
-
}
|
|
6903
|
-
return linkState?.isSelected || false;
|
|
6904
|
-
}
|
|
7652
|
+
(_linkId) => false,
|
|
7653
|
+
// isHovered - not needed for hit detection
|
|
7654
|
+
(_linkId) => false
|
|
7655
|
+
// isSelected - not needed for hit detection
|
|
6905
7656
|
);
|
|
6906
7657
|
for (const [linkId, position] of labelPositions) {
|
|
6907
7658
|
const link = linkIdToLinkMap.get(linkId);
|
|
@@ -6917,13 +7668,13 @@ var Renderer = class {
|
|
|
6917
7668
|
}
|
|
6918
7669
|
}
|
|
6919
7670
|
/**
|
|
6920
|
-
* Render shadow nodes with __indexColor
|
|
7671
|
+
* Render shadow nodes with __indexColor for hit detection
|
|
6921
7672
|
*/
|
|
6922
7673
|
renderShadowNodes(shadowCtx) {
|
|
6923
|
-
if (!this.config
|
|
7674
|
+
if (!this.config) return;
|
|
6924
7675
|
try {
|
|
6925
|
-
const { nodes } = this.config;
|
|
6926
|
-
const defaultNodeStyle =
|
|
7676
|
+
const { nodes, styleResolver } = this.config;
|
|
7677
|
+
const defaultNodeStyle = styleResolver.resolveNodeStyle({
|
|
6927
7678
|
node: { id: "temp" }
|
|
6928
7679
|
});
|
|
6929
7680
|
NodesRenderer.renderShadow(
|
|
@@ -6936,296 +7687,1167 @@ var Renderer = class {
|
|
|
6936
7687
|
}
|
|
6937
7688
|
}
|
|
6938
7689
|
/**
|
|
6939
|
-
* Get
|
|
7690
|
+
* Get unique link ID for tracking (delegates to StateManager)
|
|
6940
7691
|
*/
|
|
6941
|
-
|
|
6942
|
-
|
|
6943
|
-
const hoverState = this.hoverManager.getHoverState();
|
|
6944
|
-
if (!hoverState.currentHovered || hoverState.currentHovered.d.entityType !== "Node") {
|
|
6945
|
-
return null;
|
|
6946
|
-
}
|
|
6947
|
-
const hoveredNode = hoverState.currentHovered.d;
|
|
6948
|
-
return hoveredNode ? hoveredNode.id : null;
|
|
7692
|
+
getLinkId(link) {
|
|
7693
|
+
return this.stateManager.getLinkId(link);
|
|
6949
7694
|
}
|
|
6950
7695
|
/**
|
|
6951
|
-
*
|
|
7696
|
+
* Calculate midpoint of a link for label positioning (delegates to StateManager)
|
|
6952
7697
|
*/
|
|
6953
|
-
|
|
6954
|
-
|
|
6955
|
-
const selectionState = this.selectionManager.getSelectionState();
|
|
6956
|
-
return selectionState.selectedNode?.id || null;
|
|
7698
|
+
getLinkMidpoint(link) {
|
|
7699
|
+
return this.stateManager.getLinkMidpoint(link);
|
|
6957
7700
|
}
|
|
6958
7701
|
/**
|
|
6959
|
-
*
|
|
7702
|
+
* Update configuration
|
|
6960
7703
|
*/
|
|
6961
|
-
|
|
6962
|
-
if (
|
|
6963
|
-
|
|
6964
|
-
if (!hoverState.currentHovered || hoverState.currentHovered.d.entityType !== "Link") {
|
|
6965
|
-
return null;
|
|
7704
|
+
updateConfig(updates) {
|
|
7705
|
+
if (this.config) {
|
|
7706
|
+
Object.assign(this.config, updates);
|
|
6966
7707
|
}
|
|
6967
|
-
return hoverState.currentHovered.d;
|
|
6968
7708
|
}
|
|
6969
7709
|
/**
|
|
6970
|
-
*
|
|
7710
|
+
* Debug shadow canvas export
|
|
6971
7711
|
*/
|
|
6972
|
-
|
|
6973
|
-
|
|
6974
|
-
|
|
6975
|
-
|
|
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
|
+
}
|
|
6976
7724
|
}
|
|
6977
7725
|
/**
|
|
6978
|
-
*
|
|
7726
|
+
* Get hit detection stats
|
|
6979
7727
|
*/
|
|
6980
|
-
|
|
6981
|
-
|
|
6982
|
-
|
|
6983
|
-
|
|
6984
|
-
|
|
6985
|
-
|
|
6986
|
-
console.log("\u23F1\uFE0F Total render:", (this.performanceMetrics.renderTotal / frames).toFixed(2), "ms");
|
|
6987
|
-
console.log("\u{1F517} Links render:", (this.performanceMetrics.renderLinks / frames).toFixed(2), "ms");
|
|
6988
|
-
console.log("\u{1F3F7}\uFE0F Link labels:", (this.performanceMetrics.renderLinkLabels / frames).toFixed(2), "ms");
|
|
6989
|
-
console.log("\u2B55 Nodes render:", (this.performanceMetrics.renderNodes / frames).toFixed(2), "ms");
|
|
6990
|
-
console.log("\u{1F4DD} Node labels:", (this.performanceMetrics.renderNodeLabels / frames).toFixed(2), "ms");
|
|
6991
|
-
console.log("\u{1F3A8} Style resolution:", (this.performanceMetrics.styleResolution / frames).toFixed(2), "ms");
|
|
6992
|
-
console.log("\u{1F446} Hover checks:", (this.performanceMetrics.hoverChecks / frames).toFixed(2), "ms");
|
|
6993
|
-
console.log("\u{1F5BC}\uFE0F Canvas calls:", (this.performanceMetrics.canvasCalls / frames).toFixed(2), "ms");
|
|
6994
|
-
console.log("---");
|
|
7728
|
+
getStats() {
|
|
7729
|
+
return {
|
|
7730
|
+
shadowCanvasDirty: this.shadowCanvasDirty,
|
|
7731
|
+
throttleRate: this.SHADOW_RENDER_THROTTLE,
|
|
7732
|
+
lastRenderTime: this.lastShadowRenderTime
|
|
7733
|
+
};
|
|
6995
7734
|
}
|
|
6996
7735
|
/**
|
|
6997
|
-
*
|
|
7736
|
+
* Destroy and clean up
|
|
6998
7737
|
*/
|
|
6999
|
-
|
|
7000
|
-
this.
|
|
7001
|
-
|
|
7002
|
-
|
|
7003
|
-
|
|
7004
|
-
renderLinkLabels: 0,
|
|
7005
|
-
renderNodeLabels: 0,
|
|
7006
|
-
styleResolution: 0,
|
|
7007
|
-
hoverChecks: 0,
|
|
7008
|
-
canvasCalls: 0,
|
|
7009
|
-
frameCount: 0
|
|
7010
|
-
};
|
|
7738
|
+
destroy() {
|
|
7739
|
+
this.config = void 0;
|
|
7740
|
+
this.stateManager = void 0;
|
|
7741
|
+
this.shadowCanvasDirty = false;
|
|
7742
|
+
this.lastShadowRenderTime = 0;
|
|
7011
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
|
+
};
|
|
7012
7759
|
/**
|
|
7013
|
-
*
|
|
7760
|
+
* Increment frame count
|
|
7014
7761
|
*/
|
|
7015
|
-
|
|
7016
|
-
|
|
7762
|
+
incrementFrame() {
|
|
7763
|
+
this.metrics.frameCount++;
|
|
7017
7764
|
}
|
|
7018
7765
|
/**
|
|
7019
|
-
*
|
|
7766
|
+
* Add timing for a specific metric
|
|
7020
7767
|
*/
|
|
7021
|
-
|
|
7022
|
-
this.
|
|
7768
|
+
addTiming(metric, time) {
|
|
7769
|
+
this.metrics[metric] += time;
|
|
7023
7770
|
}
|
|
7024
7771
|
/**
|
|
7025
|
-
*
|
|
7772
|
+
* Get current metrics (copy to prevent mutation)
|
|
7026
7773
|
*/
|
|
7027
|
-
|
|
7028
|
-
|
|
7029
|
-
const hoverState = this.hoverManager.getHoverState();
|
|
7030
|
-
if (!hoverState.currentHovered || hoverState.currentHovered.d.entityType !== "Node") {
|
|
7031
|
-
return false;
|
|
7032
|
-
}
|
|
7033
|
-
const hoveredNode = hoverState.currentHovered.d;
|
|
7034
|
-
return hoveredNode && hoveredNode.id === nodeId;
|
|
7774
|
+
getMetrics() {
|
|
7775
|
+
return { ...this.metrics };
|
|
7035
7776
|
}
|
|
7036
7777
|
/**
|
|
7037
|
-
*
|
|
7778
|
+
* Reset all metrics
|
|
7038
7779
|
*/
|
|
7039
|
-
|
|
7040
|
-
|
|
7041
|
-
|
|
7042
|
-
|
|
7043
|
-
|
|
7044
|
-
|
|
7045
|
-
|
|
7046
|
-
|
|
7047
|
-
|
|
7048
|
-
|
|
7049
|
-
|
|
7050
|
-
|
|
7051
|
-
}
|
|
7052
|
-
if (hoverState.currentHovered.d.entityType === "Node") {
|
|
7053
|
-
const hoveredNode = hoverState.currentHovered.d;
|
|
7054
|
-
if (!hoveredNode) return false;
|
|
7055
|
-
const linkSourceId = typeof link.source === "string" ? link.source : link.source.id;
|
|
7056
|
-
const linkTargetId = typeof link.target === "string" ? link.target : link.target.id;
|
|
7057
|
-
return hoveredNode.id === linkSourceId || hoveredNode.id === linkTargetId;
|
|
7058
|
-
}
|
|
7059
|
-
return false;
|
|
7780
|
+
reset() {
|
|
7781
|
+
this.metrics = {
|
|
7782
|
+
renderTotal: 0,
|
|
7783
|
+
renderNodes: 0,
|
|
7784
|
+
renderLinks: 0,
|
|
7785
|
+
renderLinkLabels: 0,
|
|
7786
|
+
renderNodeLabels: 0,
|
|
7787
|
+
styleResolution: 0,
|
|
7788
|
+
hoverChecks: 0,
|
|
7789
|
+
canvasCalls: 0,
|
|
7790
|
+
frameCount: 0
|
|
7791
|
+
};
|
|
7060
7792
|
}
|
|
7061
7793
|
/**
|
|
7062
|
-
*
|
|
7794
|
+
* Log performance metrics for analysis
|
|
7063
7795
|
*/
|
|
7064
|
-
|
|
7065
|
-
|
|
7066
|
-
|
|
7067
|
-
|
|
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("---");
|
|
7068
7809
|
}
|
|
7069
7810
|
/**
|
|
7070
|
-
* Check if
|
|
7811
|
+
* Check if it's time to log metrics (every N frames)
|
|
7071
7812
|
*/
|
|
7072
|
-
|
|
7073
|
-
|
|
7074
|
-
const selectionState = this.selectionManager.getSelectionState();
|
|
7075
|
-
if (!selectionState.selectedLink) return false;
|
|
7076
|
-
const sourceId1 = typeof link.source === "string" ? link.source : link.source.id;
|
|
7077
|
-
const targetId1 = typeof link.target === "string" ? link.target : link.target.id;
|
|
7078
|
-
const selectedLink = selectionState.selectedLink;
|
|
7079
|
-
const sourceId2 = typeof selectedLink.source === "string" ? selectedLink.source : selectedLink.source.id;
|
|
7080
|
-
const targetId2 = typeof selectedLink.target === "string" ? selectedLink.target : selectedLink.target.id;
|
|
7081
|
-
return sourceId1 === sourceId2 && targetId1 === targetId2;
|
|
7813
|
+
shouldLogMetrics(intervalFrames = 100) {
|
|
7814
|
+
return this.metrics.frameCount % intervalFrames === 0 && this.metrics.frameCount > 0;
|
|
7082
7815
|
}
|
|
7816
|
+
};
|
|
7817
|
+
|
|
7818
|
+
// src/v2/rendering/link-renderer.ts
|
|
7819
|
+
var LinkRenderer = class {
|
|
7083
7820
|
/**
|
|
7084
|
-
*
|
|
7085
|
-
* (either the link itself is selected, or its connected node is selected)
|
|
7821
|
+
* Render a directed link with optional arrow head
|
|
7086
7822
|
*/
|
|
7087
|
-
|
|
7088
|
-
|
|
7089
|
-
|
|
7090
|
-
|
|
7091
|
-
|
|
7092
|
-
|
|
7093
|
-
|
|
7094
|
-
|
|
7095
|
-
|
|
7096
|
-
|
|
7097
|
-
|
|
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);
|
|
7098
7850
|
}
|
|
7851
|
+
ctx.globalAlpha = 1;
|
|
7852
|
+
} catch (error) {
|
|
7853
|
+
ErrorHandler.logError(error);
|
|
7099
7854
|
}
|
|
7100
|
-
if (selectionState.selectedNode) {
|
|
7101
|
-
const selectedNode = selectionState.selectedNode;
|
|
7102
|
-
const linkSourceId = typeof link.source === "string" ? link.source : link.source.id;
|
|
7103
|
-
const linkTargetId = typeof link.target === "string" ? link.target : link.target.id;
|
|
7104
|
-
return selectedNode.id === linkSourceId || selectedNode.id === linkTargetId;
|
|
7105
|
-
}
|
|
7106
|
-
return false;
|
|
7107
7855
|
}
|
|
7108
7856
|
/**
|
|
7109
|
-
*
|
|
7857
|
+
* V1-compatible link shortening for source point
|
|
7110
7858
|
*/
|
|
7111
|
-
|
|
7112
|
-
|
|
7113
|
-
const
|
|
7114
|
-
const
|
|
7115
|
-
|
|
7116
|
-
|
|
7117
|
-
|
|
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;
|
|
7118
7874
|
return {
|
|
7119
|
-
x:
|
|
7120
|
-
y:
|
|
7875
|
+
x: sourceX + dx / distance * offset,
|
|
7876
|
+
y: sourceY + dy / distance * offset
|
|
7121
7877
|
};
|
|
7122
7878
|
}
|
|
7123
7879
|
/**
|
|
7124
|
-
*
|
|
7880
|
+
* V1-compatible link shortening for target point
|
|
7125
7881
|
*/
|
|
7126
|
-
|
|
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) {
|
|
7127
7908
|
try {
|
|
7128
|
-
const
|
|
7129
|
-
const
|
|
7130
|
-
|
|
7131
|
-
|
|
7132
|
-
|
|
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";
|
|
7133
7920
|
ctx.beginPath();
|
|
7134
|
-
ctx.moveTo(
|
|
7135
|
-
ctx.lineTo(
|
|
7136
|
-
ctx.
|
|
7137
|
-
|
|
7138
|
-
|
|
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;
|
|
7992
|
+
const sourceId1 = typeof link.source === "string" ? link.source : link.source.id;
|
|
7993
|
+
const targetId1 = typeof link.target === "string" ? link.target : link.target.id;
|
|
7994
|
+
const selectedLink = selectionState.selectedLink;
|
|
7995
|
+
const sourceId2 = typeof selectedLink.source === "string" ? selectedLink.source : selectedLink.source.id;
|
|
7996
|
+
const targetId2 = typeof selectedLink.target === "string" ? selectedLink.target : selectedLink.target.id;
|
|
7997
|
+
return sourceId1 === sourceId2 && targetId1 === targetId2;
|
|
7998
|
+
}
|
|
7999
|
+
/**
|
|
8000
|
+
* Get interaction state for a node (optimized for caching)
|
|
8001
|
+
*/
|
|
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();
|
|
8058
|
+
}
|
|
8059
|
+
return obj;
|
|
8060
|
+
}
|
|
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);
|
|
8069
|
+
}
|
|
8070
|
+
}
|
|
8071
|
+
/**
|
|
8072
|
+
* Pre-warm pool with initial objects
|
|
8073
|
+
*/
|
|
8074
|
+
prewarm(count) {
|
|
8075
|
+
for (let i = 0; i < count; i++) {
|
|
8076
|
+
this.pool.push(this.createFn());
|
|
8077
|
+
}
|
|
8078
|
+
}
|
|
8079
|
+
/**
|
|
8080
|
+
* Get pool statistics
|
|
8081
|
+
*/
|
|
8082
|
+
getStats() {
|
|
8083
|
+
return {
|
|
8084
|
+
available: this.pool.length,
|
|
8085
|
+
maxSize: this.maxSize,
|
|
8086
|
+
utilization: (this.maxSize - this.pool.length) / this.maxSize
|
|
8087
|
+
};
|
|
8088
|
+
}
|
|
8089
|
+
/**
|
|
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
|
|
8214
|
+
*/
|
|
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;
|
|
8386
|
+
try {
|
|
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
|
+
}
|
|
7139
8455
|
}
|
|
7140
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);
|
|
7141
8473
|
} catch (error) {
|
|
7142
8474
|
ErrorHandler.logError(error);
|
|
7143
8475
|
}
|
|
8476
|
+
this.state.lastDragRenderTime = performance.now() - startTime;
|
|
7144
8477
|
}
|
|
7145
8478
|
/**
|
|
7146
|
-
*
|
|
8479
|
+
* Optimized label rendering with minimal object allocation
|
|
7147
8480
|
*/
|
|
7148
|
-
|
|
7149
|
-
|
|
7150
|
-
const
|
|
7151
|
-
|
|
7152
|
-
|
|
7153
|
-
|
|
7154
|
-
const
|
|
7155
|
-
|
|
7156
|
-
|
|
7157
|
-
|
|
7158
|
-
|
|
7159
|
-
|
|
7160
|
-
}
|
|
7161
|
-
|
|
7162
|
-
|
|
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();
|
|
7163
8567
|
return {
|
|
7164
|
-
|
|
7165
|
-
|
|
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
|
+
}
|
|
7166
8576
|
};
|
|
7167
8577
|
}
|
|
7168
8578
|
/**
|
|
7169
|
-
*
|
|
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
|
|
7170
8834
|
*/
|
|
7171
|
-
|
|
7172
|
-
|
|
7173
|
-
const sourceY = source.y ?? 0;
|
|
7174
|
-
const targetX = target.x ?? 0;
|
|
7175
|
-
const targetY = target.y ?? 0;
|
|
7176
|
-
const dx = targetX - sourceX;
|
|
7177
|
-
const dy = targetY - sourceY;
|
|
7178
|
-
const distance = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
7179
|
-
const targetNodeStyle = this.styleResolver.resolveNodeStyle({
|
|
7180
|
-
node: target,
|
|
7181
|
-
isHovered: this.isNodeHovered(target.id),
|
|
7182
|
-
isSelected: this.isNodeSelected(target.id)
|
|
7183
|
-
});
|
|
7184
|
-
const visualRadius = targetNodeStyle.radius + targetNodeStyle.strokeWidth / 2;
|
|
7185
|
-
const arrowLength = style.arrow?.enabled ? style.arrow.size ?? 4 : 0;
|
|
7186
|
-
const offset = style.arrow?.enabled ? visualRadius + arrowLength : visualRadius + 1;
|
|
7187
|
-
const shortenedPoint = {
|
|
7188
|
-
x: targetX - dx / distance * offset,
|
|
7189
|
-
y: targetY - dy / distance * offset
|
|
7190
|
-
};
|
|
7191
|
-
return shortenedPoint;
|
|
8835
|
+
getPerformanceMetrics() {
|
|
8836
|
+
return this.metricsManager.getMetrics();
|
|
7192
8837
|
}
|
|
7193
8838
|
/**
|
|
7194
|
-
*
|
|
8839
|
+
* Force log performance metrics immediately (for debugging)
|
|
7195
8840
|
*/
|
|
7196
|
-
|
|
7197
|
-
|
|
7198
|
-
|
|
7199
|
-
|
|
7200
|
-
const angle = Math.atan2(dy, dx);
|
|
7201
|
-
const arrowLength = arrowStyle.size ?? 4;
|
|
7202
|
-
const arrowTipX = targetPoint.x + arrowLength * Math.cos(angle);
|
|
7203
|
-
const arrowTipY = targetPoint.y + arrowLength * Math.sin(angle);
|
|
7204
|
-
const x1 = arrowTipX - arrowLength * Math.cos(angle - Math.PI / 6);
|
|
7205
|
-
const y1 = arrowTipY - arrowLength * Math.sin(angle - Math.PI / 6);
|
|
7206
|
-
const x22 = arrowTipX - arrowLength * Math.cos(angle + Math.PI / 6);
|
|
7207
|
-
const y22 = arrowTipY - arrowLength * Math.sin(angle + Math.PI / 6);
|
|
7208
|
-
ctx.fillStyle = arrowStyle.fill ?? "#000000";
|
|
7209
|
-
ctx.beginPath();
|
|
7210
|
-
ctx.moveTo(arrowTipX, arrowTipY);
|
|
7211
|
-
ctx.lineTo(x1, y1);
|
|
7212
|
-
ctx.lineTo(x22, y22);
|
|
7213
|
-
ctx.closePath();
|
|
7214
|
-
ctx.fill();
|
|
7215
|
-
} catch (error) {
|
|
7216
|
-
ErrorHandler.logError(error);
|
|
7217
|
-
}
|
|
8841
|
+
forceLogMetrics() {
|
|
8842
|
+
const nodeCount = this.config?.nodes.length || 0;
|
|
8843
|
+
const linkCount = this.config?.links.length || 0;
|
|
8844
|
+
this.metricsManager.logMetrics(nodeCount, linkCount);
|
|
7218
8845
|
}
|
|
7219
8846
|
/**
|
|
7220
|
-
*
|
|
8847
|
+
* Calculate midpoint of a link for label positioning (delegates to StateManager)
|
|
7221
8848
|
*/
|
|
7222
|
-
|
|
7223
|
-
this.
|
|
7224
|
-
ctx,
|
|
7225
|
-
{ x: source.x, y: source.y },
|
|
7226
|
-
{ x: target.x, y: target.y },
|
|
7227
|
-
arrowStyle
|
|
7228
|
-
);
|
|
8849
|
+
getLinkMidpoint(link) {
|
|
8850
|
+
return this.stateManager.getLinkMidpoint(link);
|
|
7229
8851
|
}
|
|
7230
8852
|
/**
|
|
7231
8853
|
* Initialize node positions if needed
|
|
@@ -7250,6 +8872,29 @@ var Renderer = class {
|
|
|
7250
8872
|
if (!this.config) return;
|
|
7251
8873
|
try {
|
|
7252
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
|
+
});
|
|
7253
8898
|
} catch (error) {
|
|
7254
8899
|
ErrorHandler.logError(error);
|
|
7255
8900
|
}
|
|
@@ -7272,247 +8917,22 @@ var Renderer = class {
|
|
|
7272
8917
|
};
|
|
7273
8918
|
}
|
|
7274
8919
|
/**
|
|
7275
|
-
* Debug shadow canvas export (
|
|
8920
|
+
* Debug shadow canvas export (delegates to HitDetectionRenderer)
|
|
7276
8921
|
*/
|
|
7277
8922
|
debugShadowCanvas() {
|
|
7278
|
-
|
|
7279
|
-
if (!this.canvasState) return;
|
|
7280
|
-
const { shadowCanvas } = this.canvasState;
|
|
7281
|
-
const link = document.createElement("a");
|
|
7282
|
-
link.download = "shadow-canvas-debug.png";
|
|
7283
|
-
link.href = shadowCanvas.toDataURL("image/png");
|
|
7284
|
-
link.click();
|
|
7285
|
-
} catch (error) {
|
|
7286
|
-
ErrorHandler.logError(error);
|
|
7287
|
-
}
|
|
7288
|
-
}
|
|
7289
|
-
/**
|
|
7290
|
-
* Build node index for O(1) lookups (Step 3 optimization)
|
|
7291
|
-
*/
|
|
7292
|
-
buildNodeIndex() {
|
|
7293
|
-
if (!this.config) return;
|
|
7294
|
-
try {
|
|
7295
|
-
this.nodeMap.clear();
|
|
7296
|
-
for (const node of this.config.nodes) {
|
|
7297
|
-
this.nodeMap.set(node.id, node);
|
|
7298
|
-
}
|
|
7299
|
-
for (const link of this.config.links) {
|
|
7300
|
-
if (typeof link.source === "string") {
|
|
7301
|
-
const sourceNode = this.nodeMap.get(link.source);
|
|
7302
|
-
if (sourceNode) {
|
|
7303
|
-
link.source = sourceNode;
|
|
7304
|
-
}
|
|
7305
|
-
}
|
|
7306
|
-
if (typeof link.target === "string") {
|
|
7307
|
-
const targetNode = this.nodeMap.get(link.target);
|
|
7308
|
-
if (targetNode) {
|
|
7309
|
-
link.target = targetNode;
|
|
7310
|
-
}
|
|
7311
|
-
}
|
|
7312
|
-
}
|
|
7313
|
-
} catch (error) {
|
|
7314
|
-
ErrorHandler.logError(error);
|
|
7315
|
-
}
|
|
7316
|
-
}
|
|
7317
|
-
/**
|
|
7318
|
-
* Get node by ID using O(1) lookup (Step 3 optimization)
|
|
7319
|
-
*/
|
|
7320
|
-
getNodeById(nodeId) {
|
|
7321
|
-
return this.nodeMap.get(nodeId);
|
|
7322
|
-
}
|
|
7323
|
-
/**
|
|
7324
|
-
* Render with z-index layers (for renderWithTransform)
|
|
7325
|
-
*/
|
|
7326
|
-
renderWithLayers(ctx) {
|
|
7327
|
-
if (!this.config) return;
|
|
7328
|
-
try {
|
|
7329
|
-
const hoveredNode = this.getHoveredNode();
|
|
7330
|
-
const selectedNode = this.getSelectedNode();
|
|
7331
|
-
const hoveredLink = this.getHoveredLink();
|
|
7332
|
-
const selectedLink = this.getSelectedLink();
|
|
7333
|
-
const nodeHighlightChecker = ZIndexManager.createNodeHighlightChecker(
|
|
7334
|
-
hoveredNode?.id || null,
|
|
7335
|
-
selectedNode?.id || null
|
|
7336
|
-
);
|
|
7337
|
-
const linkHighlightChecker = ZIndexManager.createLinkHighlightChecker(
|
|
7338
|
-
hoveredNode?.id || null,
|
|
7339
|
-
selectedNode?.id || null,
|
|
7340
|
-
hoveredLink ? this.getLinkId(hoveredLink) : null,
|
|
7341
|
-
selectedLink ? this.getLinkId(selectedLink) : null
|
|
7342
|
-
);
|
|
7343
|
-
const linkLayers = ZIndexManager.separateIntoLayers(this.config.links, linkHighlightChecker);
|
|
7344
|
-
const nodeLayers = ZIndexManager.separateIntoLayers(this.config.nodes, nodeHighlightChecker);
|
|
7345
|
-
this.renderLinksLayer(ctx, linkLayers.background);
|
|
7346
|
-
this.renderLinkLabelsLayer(ctx, linkLayers.background);
|
|
7347
|
-
this.renderNodesWithLabelsLayer(ctx, nodeLayers.background);
|
|
7348
|
-
this.renderLinksLayer(ctx, linkLayers.foreground);
|
|
7349
|
-
this.renderLinkLabelsLayer(ctx, linkLayers.foreground);
|
|
7350
|
-
this.renderNodesWithLabelsLayer(ctx, nodeLayers.foreground);
|
|
7351
|
-
} catch (error) {
|
|
7352
|
-
ErrorHandler.logError(error);
|
|
7353
|
-
}
|
|
7354
|
-
}
|
|
7355
|
-
/**
|
|
7356
|
-
* Render with z-index layers and performance metrics (for main render)
|
|
7357
|
-
*/
|
|
7358
|
-
renderWithLayersAndMetrics(ctx) {
|
|
7359
|
-
if (!this.config) return;
|
|
7360
|
-
try {
|
|
7361
|
-
const hoveredNode = this.getHoveredNode();
|
|
7362
|
-
const selectedNode = this.getSelectedNode();
|
|
7363
|
-
const hoveredLink = this.getHoveredLink();
|
|
7364
|
-
const selectedLink = this.getSelectedLink();
|
|
7365
|
-
const nodeHighlightChecker = ZIndexManager.createNodeHighlightChecker(
|
|
7366
|
-
hoveredNode?.id || null,
|
|
7367
|
-
selectedNode?.id || null
|
|
7368
|
-
);
|
|
7369
|
-
const linkHighlightChecker = ZIndexManager.createLinkHighlightChecker(
|
|
7370
|
-
hoveredNode?.id || null,
|
|
7371
|
-
selectedNode?.id || null,
|
|
7372
|
-
hoveredLink ? this.getLinkId(hoveredLink) : null,
|
|
7373
|
-
selectedLink ? this.getLinkId(selectedLink) : null
|
|
7374
|
-
);
|
|
7375
|
-
const linkLayers = ZIndexManager.separateIntoLayers(this.config.links, linkHighlightChecker);
|
|
7376
|
-
const nodeLayers = ZIndexManager.separateIntoLayers(this.config.nodes, nodeHighlightChecker);
|
|
7377
|
-
let stepStart = performance.now();
|
|
7378
|
-
this.renderLinksLayer(ctx, linkLayers.background);
|
|
7379
|
-
this.renderLinksLayer(ctx, linkLayers.foreground);
|
|
7380
|
-
this.performanceMetrics.renderLinks += performance.now() - stepStart;
|
|
7381
|
-
stepStart = performance.now();
|
|
7382
|
-
this.renderNodesLayer(ctx, nodeLayers.background);
|
|
7383
|
-
this.renderNodesLayer(ctx, nodeLayers.foreground);
|
|
7384
|
-
this.performanceMetrics.renderNodes += performance.now() - stepStart;
|
|
7385
|
-
stepStart = performance.now();
|
|
7386
|
-
this.renderLinkLabelsLayer(ctx, linkLayers.background);
|
|
7387
|
-
this.renderLinkLabelsLayer(ctx, linkLayers.foreground);
|
|
7388
|
-
this.performanceMetrics.renderLinkLabels += performance.now() - stepStart;
|
|
7389
|
-
stepStart = performance.now();
|
|
7390
|
-
this.renderNodeLabelsLayer(ctx, nodeLayers.background);
|
|
7391
|
-
this.renderNodeLabelsLayer(ctx, nodeLayers.foreground);
|
|
7392
|
-
this.performanceMetrics.renderNodeLabels += performance.now() - stepStart;
|
|
7393
|
-
} catch (error) {
|
|
7394
|
-
ErrorHandler.logError(error);
|
|
7395
|
-
}
|
|
7396
|
-
}
|
|
7397
|
-
/**
|
|
7398
|
-
* Helper methods for getting current interaction states
|
|
7399
|
-
*/
|
|
7400
|
-
getHoveredNode() {
|
|
7401
|
-
if (!this.hoverManager) return null;
|
|
7402
|
-
const hoverState = this.hoverManager.getHoverState();
|
|
7403
|
-
return hoverState.currentHovered?.d.entityType === "Node" ? hoverState.currentHovered.d : null;
|
|
7404
|
-
}
|
|
7405
|
-
getSelectedNode() {
|
|
7406
|
-
if (!this.selectionManager) return null;
|
|
7407
|
-
const selectionState = this.selectionManager.getSelectionState();
|
|
7408
|
-
return selectionState.selectedNode;
|
|
7409
|
-
}
|
|
7410
|
-
getHoveredLink() {
|
|
7411
|
-
if (!this.hoverManager) return null;
|
|
7412
|
-
const hoverState = this.hoverManager.getHoverState();
|
|
7413
|
-
return hoverState.currentHovered?.d.entityType === "Link" ? hoverState.currentHovered.d : null;
|
|
7414
|
-
}
|
|
7415
|
-
getSelectedLink() {
|
|
7416
|
-
if (!this.selectionManager) return null;
|
|
7417
|
-
const selectionState = this.selectionManager.getSelectionState();
|
|
7418
|
-
return selectionState.selectedLink;
|
|
7419
|
-
}
|
|
7420
|
-
/**
|
|
7421
|
-
* Layer-specific rendering methods (render subsets of entities)
|
|
7422
|
-
*/
|
|
7423
|
-
renderNodesWithLabelsLayer(ctx, nodes) {
|
|
7424
|
-
if (!this.config || !this.styleResolver) return;
|
|
7425
|
-
const nodeCount = this.config.nodes.length;
|
|
7426
|
-
const isLargeGraph = nodeCount > 1e4;
|
|
7427
|
-
let currentZoom = 1;
|
|
7428
|
-
let isZoomedOutForNodes = false;
|
|
7429
|
-
if (isLargeGraph) {
|
|
7430
|
-
currentZoom = this.canvasState ? transform(this.canvasState.canvas).k : 1;
|
|
7431
|
-
isZoomedOutForNodes = currentZoom <= 0.8;
|
|
7432
|
-
}
|
|
7433
|
-
const nodeStateCache = /* @__PURE__ */ new Map();
|
|
7434
|
-
for (const node of nodes) {
|
|
7435
|
-
if (!node.x || !node.y) continue;
|
|
7436
|
-
const nodeId = node.id;
|
|
7437
|
-
let nodeState = nodeStateCache.get(nodeId);
|
|
7438
|
-
if (!nodeState) {
|
|
7439
|
-
nodeState = {
|
|
7440
|
-
isHovered: this.isNodeHovered(nodeId),
|
|
7441
|
-
isSelected: this.isNodeSelected(nodeId)
|
|
7442
|
-
};
|
|
7443
|
-
nodeStateCache.set(nodeId, nodeState);
|
|
7444
|
-
}
|
|
7445
|
-
const nodeStyle = this.styleResolver.resolveNodeStyle({
|
|
7446
|
-
node,
|
|
7447
|
-
isHovered: nodeState.isHovered,
|
|
7448
|
-
isSelected: nodeState.isSelected
|
|
7449
|
-
});
|
|
7450
|
-
ctx.beginPath();
|
|
7451
|
-
ctx.arc(node.x, node.y, nodeStyle.radius, 0, 2 * Math.PI);
|
|
7452
|
-
ctx.fillStyle = nodeStyle.fill;
|
|
7453
|
-
ctx.fill();
|
|
7454
|
-
if (nodeStyle.stroke && nodeStyle.strokeWidth > 0) {
|
|
7455
|
-
ctx.strokeStyle = nodeStyle.stroke;
|
|
7456
|
-
ctx.lineWidth = nodeStyle.strokeWidth;
|
|
7457
|
-
ctx.stroke();
|
|
7458
|
-
}
|
|
7459
|
-
if (isLargeGraph && isZoomedOutForNodes) {
|
|
7460
|
-
continue;
|
|
7461
|
-
}
|
|
7462
|
-
const nodeStyleWithLabel = nodeStyle;
|
|
7463
|
-
if (nodeStyleWithLabel.label && !nodeStyleWithLabel.label.enabled) continue;
|
|
7464
|
-
const fullLabel = node.label || node.id;
|
|
7465
|
-
const defaultStyle = {
|
|
7466
|
-
font: "9px sans-serif",
|
|
7467
|
-
textAlign: "center",
|
|
7468
|
-
textBaseline: "middle",
|
|
7469
|
-
fillStyle: "#ffffff",
|
|
7470
|
-
offsetY: 0
|
|
7471
|
-
};
|
|
7472
|
-
if (nodeStyleWithLabel.label) {
|
|
7473
|
-
ctx.font = nodeStyleWithLabel.label.font || defaultStyle.font;
|
|
7474
|
-
ctx.textAlign = nodeStyleWithLabel.label.textAlign || defaultStyle.textAlign;
|
|
7475
|
-
ctx.textBaseline = nodeStyleWithLabel.label.textBaseline || defaultStyle.textBaseline;
|
|
7476
|
-
ctx.fillStyle = nodeStyleWithLabel.label.textColor || defaultStyle.fillStyle;
|
|
7477
|
-
} else {
|
|
7478
|
-
ctx.font = defaultStyle.font;
|
|
7479
|
-
ctx.textAlign = defaultStyle.textAlign;
|
|
7480
|
-
ctx.textBaseline = defaultStyle.textBaseline;
|
|
7481
|
-
ctx.fillStyle = defaultStyle.fillStyle;
|
|
7482
|
-
}
|
|
7483
|
-
const maxWidth = nodeStyle.radius * 2 - 6;
|
|
7484
|
-
const truncatedLabel = this.truncateLabel(ctx, fullLabel, maxWidth);
|
|
7485
|
-
const labelY = node.y + (nodeStyleWithLabel.label?.offsetY || defaultStyle.offsetY);
|
|
7486
|
-
ctx.fillText(truncatedLabel, node.x, labelY);
|
|
7487
|
-
}
|
|
7488
|
-
}
|
|
7489
|
-
/**
|
|
7490
|
-
* Helper method to truncate labels (copied from NodeLabelsRenderer)
|
|
7491
|
-
*/
|
|
7492
|
-
truncateLabel(ctx, label, maxWidth) {
|
|
7493
|
-
let truncatedLabel = label;
|
|
7494
|
-
if (ctx.measureText(truncatedLabel).width <= maxWidth) {
|
|
7495
|
-
return truncatedLabel;
|
|
7496
|
-
}
|
|
7497
|
-
while (truncatedLabel.length > 1 && ctx.measureText(`${truncatedLabel}\u2026`).width > maxWidth) {
|
|
7498
|
-
truncatedLabel = truncatedLabel.slice(0, -1);
|
|
7499
|
-
}
|
|
7500
|
-
return truncatedLabel.length < label.length ? `${truncatedLabel}\u2026` : truncatedLabel;
|
|
8923
|
+
this.hitDetectionRenderer.debugShadowCanvas();
|
|
7501
8924
|
}
|
|
7502
8925
|
renderLinksLayer(ctx, links) {
|
|
7503
8926
|
if (!this.config || !this.styleResolver) return;
|
|
7504
8927
|
const linkStateCache = /* @__PURE__ */ new Map();
|
|
7505
8928
|
for (const link of links) {
|
|
7506
|
-
const sourceNode = typeof link.source === "string" ? this.
|
|
7507
|
-
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;
|
|
7508
8931
|
if (sourceNode && targetNode && sourceNode.x && sourceNode.y && targetNode.x && targetNode.y) {
|
|
7509
8932
|
const linkId = this.getLinkId(link);
|
|
7510
8933
|
let linkState = linkStateCache.get(linkId);
|
|
7511
8934
|
if (!linkState) {
|
|
7512
|
-
linkState =
|
|
7513
|
-
isHovered: this.isLinkHovered(link),
|
|
7514
|
-
isSelected: this.isLinkSelected(link)
|
|
7515
|
-
};
|
|
8935
|
+
linkState = this.interactionResolver.getLinkState(link);
|
|
7516
8936
|
linkStateCache.set(linkId, linkState);
|
|
7517
8937
|
}
|
|
7518
8938
|
const style = this.styleResolver.resolveLinkStyle({
|
|
@@ -7520,85 +8940,18 @@ var Renderer = class {
|
|
|
7520
8940
|
isHovered: linkState.isHovered,
|
|
7521
8941
|
isSelected: linkState.isSelected
|
|
7522
8942
|
});
|
|
7523
|
-
this.
|
|
7524
|
-
|
|
7525
|
-
|
|
7526
|
-
|
|
7527
|
-
|
|
7528
|
-
|
|
7529
|
-
|
|
7530
|
-
|
|
7531
|
-
|
|
7532
|
-
|
|
7533
|
-
this.hasLoggedLargeGraphOptimization = true;
|
|
7534
|
-
}
|
|
7535
|
-
if (isLargeGraph) {
|
|
7536
|
-
const currentZoom = this.canvasState ? transform(this.canvasState.canvas).k : 1;
|
|
7537
|
-
const isZoomedOut = currentZoom <= 1;
|
|
7538
|
-
if (isZoomedOut) {
|
|
7539
|
-
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
|
+
);
|
|
7540
8953
|
}
|
|
7541
8954
|
}
|
|
7542
|
-
const linkStateCache = /* @__PURE__ */ new Map();
|
|
7543
|
-
LinkLabelsRenderer.renderWithVisibility(
|
|
7544
|
-
ctx,
|
|
7545
|
-
links,
|
|
7546
|
-
(link) => {
|
|
7547
|
-
if (!link.label) {
|
|
7548
|
-
return null;
|
|
7549
|
-
}
|
|
7550
|
-
const linkId = this.getLinkId(link);
|
|
7551
|
-
let linkState = linkStateCache.get(linkId);
|
|
7552
|
-
if (!linkState) {
|
|
7553
|
-
linkState = {
|
|
7554
|
-
isHovered: this.isLinkHovered(link),
|
|
7555
|
-
isSelected: this.isLinkSelected(link)
|
|
7556
|
-
};
|
|
7557
|
-
linkStateCache.set(linkId, linkState);
|
|
7558
|
-
}
|
|
7559
|
-
if (isLargeGraph) {
|
|
7560
|
-
const isInteractive = linkState.isHovered || linkState.isSelected;
|
|
7561
|
-
if (!isInteractive) {
|
|
7562
|
-
return null;
|
|
7563
|
-
}
|
|
7564
|
-
}
|
|
7565
|
-
const style = this.styleResolver.resolveLinkStyle({
|
|
7566
|
-
link,
|
|
7567
|
-
isHovered: linkState.isHovered,
|
|
7568
|
-
isSelected: linkState.isSelected
|
|
7569
|
-
});
|
|
7570
|
-
return style.label || null;
|
|
7571
|
-
},
|
|
7572
|
-
(link) => this.getLinkMidpoint(link),
|
|
7573
|
-
(linkId) => {
|
|
7574
|
-
let linkState = linkStateCache.get(linkId);
|
|
7575
|
-
if (!linkState) {
|
|
7576
|
-
const link = links.find((l) => this.getLinkId(l) === linkId);
|
|
7577
|
-
if (link) {
|
|
7578
|
-
linkState = {
|
|
7579
|
-
isHovered: this.isLinkHovered(link),
|
|
7580
|
-
isSelected: this.isLinkSelected(link)
|
|
7581
|
-
};
|
|
7582
|
-
linkStateCache.set(linkId, linkState);
|
|
7583
|
-
}
|
|
7584
|
-
}
|
|
7585
|
-
return linkState?.isHovered || false;
|
|
7586
|
-
},
|
|
7587
|
-
(linkId) => {
|
|
7588
|
-
let linkState = linkStateCache.get(linkId);
|
|
7589
|
-
if (!linkState) {
|
|
7590
|
-
const link = links.find((l) => this.getLinkId(l) === linkId);
|
|
7591
|
-
if (link) {
|
|
7592
|
-
linkState = {
|
|
7593
|
-
isHovered: this.isLinkHovered(link),
|
|
7594
|
-
isSelected: this.isLinkSelected(link)
|
|
7595
|
-
};
|
|
7596
|
-
linkStateCache.set(linkId, linkState);
|
|
7597
|
-
}
|
|
7598
|
-
}
|
|
7599
|
-
return linkState?.isSelected || false;
|
|
7600
|
-
}
|
|
7601
|
-
);
|
|
7602
8955
|
}
|
|
7603
8956
|
renderNodesLayer(ctx, nodes) {
|
|
7604
8957
|
if (!this.config || !this.styleResolver) return;
|
|
@@ -7610,9 +8963,11 @@ var Renderer = class {
|
|
|
7610
8963
|
(nodeId) => {
|
|
7611
8964
|
let nodeState = nodeStateCache.get(nodeId);
|
|
7612
8965
|
if (!nodeState) {
|
|
8966
|
+
const interactionState = this.interactionResolver.getNodeState(nodeId);
|
|
7613
8967
|
nodeState = {
|
|
7614
|
-
isHovered:
|
|
7615
|
-
isSelected:
|
|
8968
|
+
isHovered: interactionState.isHovered,
|
|
8969
|
+
isSelected: interactionState.isSelected,
|
|
8970
|
+
isHighlighted: this.stateManager.isNodeHighlighted(nodeId)
|
|
7616
8971
|
};
|
|
7617
8972
|
nodeStateCache.set(nodeId, nodeState);
|
|
7618
8973
|
}
|
|
@@ -7621,13 +8976,28 @@ var Renderer = class {
|
|
|
7621
8976
|
(nodeId) => {
|
|
7622
8977
|
let nodeState = nodeStateCache.get(nodeId);
|
|
7623
8978
|
if (!nodeState) {
|
|
8979
|
+
const interactionState = this.interactionResolver.getNodeState(nodeId);
|
|
7624
8980
|
nodeState = {
|
|
7625
|
-
isHovered:
|
|
7626
|
-
isSelected:
|
|
8981
|
+
isHovered: interactionState.isHovered,
|
|
8982
|
+
isSelected: interactionState.isSelected,
|
|
8983
|
+
isHighlighted: this.stateManager.isNodeHighlighted(nodeId)
|
|
7627
8984
|
};
|
|
7628
8985
|
nodeStateCache.set(nodeId, nodeState);
|
|
7629
8986
|
}
|
|
7630
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;
|
|
7631
9001
|
}
|
|
7632
9002
|
);
|
|
7633
9003
|
}
|
|
@@ -7650,10 +9020,7 @@ var Renderer = class {
|
|
|
7650
9020
|
(nodeId) => {
|
|
7651
9021
|
let nodeState = nodeStateCache.get(nodeId);
|
|
7652
9022
|
if (!nodeState) {
|
|
7653
|
-
nodeState =
|
|
7654
|
-
isHovered: this.isNodeHovered(nodeId),
|
|
7655
|
-
isSelected: this.isNodeSelected(nodeId)
|
|
7656
|
-
};
|
|
9023
|
+
nodeState = this.interactionResolver.getNodeState(nodeId);
|
|
7657
9024
|
nodeStateCache.set(nodeId, nodeState);
|
|
7658
9025
|
}
|
|
7659
9026
|
return nodeState.isHovered;
|
|
@@ -7661,28 +9028,33 @@ var Renderer = class {
|
|
|
7661
9028
|
(nodeId) => {
|
|
7662
9029
|
let nodeState = nodeStateCache.get(nodeId);
|
|
7663
9030
|
if (!nodeState) {
|
|
7664
|
-
nodeState =
|
|
7665
|
-
isHovered: this.isNodeHovered(nodeId),
|
|
7666
|
-
isSelected: this.isNodeSelected(nodeId)
|
|
7667
|
-
};
|
|
9031
|
+
nodeState = this.interactionResolver.getNodeState(nodeId);
|
|
7668
9032
|
nodeStateCache.set(nodeId, nodeState);
|
|
7669
9033
|
}
|
|
7670
9034
|
return nodeState.isSelected;
|
|
7671
9035
|
}
|
|
7672
9036
|
);
|
|
7673
9037
|
}
|
|
9038
|
+
/**
|
|
9039
|
+
* Get the state manager for highlight operations
|
|
9040
|
+
*/
|
|
9041
|
+
getStateManager() {
|
|
9042
|
+
return this.stateManager;
|
|
9043
|
+
}
|
|
7674
9044
|
/**
|
|
7675
9045
|
* Destroy renderer and clean up resources
|
|
7676
9046
|
*/
|
|
7677
9047
|
destroy() {
|
|
7678
9048
|
try {
|
|
9049
|
+
this.zIndexRenderer.destroy();
|
|
9050
|
+
this.zoomRenderer.destroy();
|
|
9051
|
+
this.hitDetectionRenderer.destroy();
|
|
9052
|
+
this.dragOptimizer.destroy();
|
|
7679
9053
|
this.config = void 0;
|
|
7680
9054
|
this.canvasState = void 0;
|
|
7681
9055
|
this.hoverManager = void 0;
|
|
7682
9056
|
this.styleResolver = void 0;
|
|
7683
|
-
this.
|
|
7684
|
-
this.shadowCanvasDirty = false;
|
|
7685
|
-
this.lastShadowRenderTime = 0;
|
|
9057
|
+
this.stateManager.destroy();
|
|
7686
9058
|
} catch (error) {
|
|
7687
9059
|
ErrorHandler.logError(error);
|
|
7688
9060
|
}
|
|
@@ -8133,13 +9505,22 @@ var V2Graph = class {
|
|
|
8133
9505
|
// pointerManager: this.pointerManager,
|
|
8134
9506
|
physicsManager: this.physicsManager,
|
|
8135
9507
|
hoverManager: this.hoverManager,
|
|
9508
|
+
renderer: this.renderer,
|
|
9509
|
+
// Pass renderer for drag state management
|
|
8136
9510
|
onRender: () => this.renderer.renderWithTransform()
|
|
8137
9511
|
});
|
|
8138
9512
|
this.coordinateDragHover();
|
|
8139
9513
|
this.zoomManager.initialize({
|
|
8140
9514
|
canvas: canvasState.canvas,
|
|
8141
9515
|
canvasManager: this.canvasManager,
|
|
8142
|
-
|
|
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
|
+
},
|
|
8143
9524
|
isOverEntity: () => {
|
|
8144
9525
|
const hoverState = this.hoverManager.getHoverState();
|
|
8145
9526
|
return hoverState.currentHovered !== null;
|
|
@@ -8651,6 +10032,76 @@ var V2Graph = class {
|
|
|
8651
10032
|
}
|
|
8652
10033
|
});
|
|
8653
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
|
+
}
|
|
8654
10105
|
/**
|
|
8655
10106
|
* Destroy the graph and clean up resources
|
|
8656
10107
|
*/
|