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.js
CHANGED
|
@@ -454,6 +454,15 @@ var DEFAULT_NODE_INTERACTION_STYLE = {
|
|
|
454
454
|
// Hover strokeWidth from KG component
|
|
455
455
|
// Selection uses radius: 24 and strokeWidth: 4 (handled separately in interaction config)
|
|
456
456
|
};
|
|
457
|
+
var DEFAULT_NODE_HIGHLIGHT_STYLE = {
|
|
458
|
+
fill: "#fbbf24",
|
|
459
|
+
// Amber highlight color
|
|
460
|
+
stroke: "#f59e0b",
|
|
461
|
+
// Darker amber border
|
|
462
|
+
strokeWidth: 2,
|
|
463
|
+
// Highlighted border
|
|
464
|
+
opacity: 1
|
|
465
|
+
};
|
|
457
466
|
var DEFAULT_LINK_LABEL_STYLE2 = {
|
|
458
467
|
enabled: true,
|
|
459
468
|
visibility: "always",
|
|
@@ -510,8 +519,8 @@ var StyleResolver = class {
|
|
|
510
519
|
/**
|
|
511
520
|
* Generate cache key for node styles (Step 4 optimization)
|
|
512
521
|
*/
|
|
513
|
-
createNodeCacheKey(node, isHovered, isSelected) {
|
|
514
|
-
return `${node.id}_${!!node.style}_${isHovered}_${isSelected}_${this.interactionConfigHash}`;
|
|
522
|
+
createNodeCacheKey(node, isHovered, isSelected, isHighlighted = false) {
|
|
523
|
+
return `${node.id}_${!!node.style}_${isHovered}_${isSelected}_${isHighlighted}_${this.interactionConfigHash}`;
|
|
515
524
|
}
|
|
516
525
|
/**
|
|
517
526
|
* Generate cache key for link styles (Step 4 optimization)
|
|
@@ -526,13 +535,13 @@ var StyleResolver = class {
|
|
|
526
535
|
* Resolve node style using V1-compatible approach with caching (Step 4 optimization)
|
|
527
536
|
*/
|
|
528
537
|
resolveNodeStyle(params) {
|
|
529
|
-
const { node, isHovered = false, isSelected = false } = params;
|
|
530
|
-
const cacheKey = this.createNodeCacheKey(node, isHovered, isSelected);
|
|
538
|
+
const { node, isHovered = false, isSelected = false, isHighlighted = false } = params;
|
|
539
|
+
const cacheKey = this.createNodeCacheKey(node, isHovered, isSelected, isHighlighted);
|
|
531
540
|
const cached = this.nodeStyleCache.get(cacheKey);
|
|
532
541
|
if (cached) {
|
|
533
542
|
return cached;
|
|
534
543
|
}
|
|
535
|
-
if (!isHovered && !isSelected) {
|
|
544
|
+
if (!isHovered && !isSelected && !isHighlighted) {
|
|
536
545
|
const result2 = node.style ? { ...DEFAULT_NODE_STYLE, ...node.style } : DEFAULT_NODE_STYLE;
|
|
537
546
|
this.nodeStyleCache.set(cacheKey, result2);
|
|
538
547
|
return result2;
|
|
@@ -547,6 +556,12 @@ var StyleResolver = class {
|
|
|
547
556
|
this.interactionConfig?.selection?.nodeStyle
|
|
548
557
|
);
|
|
549
558
|
result = { ...result, ...interactionStyle };
|
|
559
|
+
} else if (isHighlighted) {
|
|
560
|
+
const highlightStyle = this.mergeNodeStyleSmart(
|
|
561
|
+
DEFAULT_NODE_HIGHLIGHT_STYLE,
|
|
562
|
+
this.interactionConfig?.highlight?.nodeStyle
|
|
563
|
+
);
|
|
564
|
+
result = { ...result, ...highlightStyle };
|
|
550
565
|
} else if (isHovered) {
|
|
551
566
|
const interactionStyle = this.mergeNodeStyleSmart(
|
|
552
567
|
DEFAULT_NODE_INTERACTION_STYLE,
|
|
@@ -733,89 +748,6 @@ function calculateCanvasDimensions(width, height) {
|
|
|
733
748
|
};
|
|
734
749
|
}
|
|
735
750
|
|
|
736
|
-
// src/v2/utils/z-index-manager.ts
|
|
737
|
-
var ZIndexManager = class {
|
|
738
|
-
/**
|
|
739
|
-
* Separate entities into background and foreground layers based on interaction state
|
|
740
|
-
*/
|
|
741
|
-
static separateIntoLayers(entities, isHighlighted) {
|
|
742
|
-
try {
|
|
743
|
-
const background = [];
|
|
744
|
-
const foreground = [];
|
|
745
|
-
for (const entity of entities) {
|
|
746
|
-
if (isHighlighted(entity)) {
|
|
747
|
-
foreground.push(entity);
|
|
748
|
-
} else {
|
|
749
|
-
background.push(entity);
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
return { background, foreground };
|
|
753
|
-
} catch (error) {
|
|
754
|
-
ErrorHandler.logError(error, {
|
|
755
|
-
entityCount: entities.length
|
|
756
|
-
});
|
|
757
|
-
return { background: entities, foreground: [] };
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
/**
|
|
761
|
-
* Get connected entities for a node (for bringing associated elements to front)
|
|
762
|
-
*/
|
|
763
|
-
static getConnectedEntities(node, allLinks) {
|
|
764
|
-
try {
|
|
765
|
-
const connectedLinks = [];
|
|
766
|
-
const linkIds = /* @__PURE__ */ new Set();
|
|
767
|
-
for (const link of allLinks) {
|
|
768
|
-
const sourceId = typeof link.source === "string" ? link.source : link.source.id;
|
|
769
|
-
const targetId = typeof link.target === "string" ? link.target : link.target.id;
|
|
770
|
-
if (sourceId === node.id || targetId === node.id) {
|
|
771
|
-
connectedLinks.push(link);
|
|
772
|
-
linkIds.add(`${sourceId}->${targetId}`);
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
return { connectedLinks, linkIds };
|
|
776
|
-
} catch (error) {
|
|
777
|
-
ErrorHandler.logError(error, {
|
|
778
|
-
nodeId: node.id
|
|
779
|
-
});
|
|
780
|
-
return { connectedLinks: [], linkIds: /* @__PURE__ */ new Set() };
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
/**
|
|
784
|
-
* Create highlight checker for nodes (node is highlighted if hovered or selected)
|
|
785
|
-
*/
|
|
786
|
-
static createNodeHighlightChecker(hoveredNodeId, selectedNodeId) {
|
|
787
|
-
return (node) => {
|
|
788
|
-
return node.id === hoveredNodeId || node.id === selectedNodeId;
|
|
789
|
-
};
|
|
790
|
-
}
|
|
791
|
-
/**
|
|
792
|
-
* Create highlight checker for links (link is highlighted if hovered, selected, or connected to highlighted node)
|
|
793
|
-
*/
|
|
794
|
-
static createLinkHighlightChecker(hoveredNodeId, selectedNodeId, hoveredLinkId, selectedLinkId) {
|
|
795
|
-
return (link) => {
|
|
796
|
-
const sourceId = typeof link.source === "string" ? link.source : link.source.id;
|
|
797
|
-
const targetId = typeof link.target === "string" ? link.target : link.target.id;
|
|
798
|
-
const linkId = `${sourceId}->${targetId}`;
|
|
799
|
-
if (linkId === hoveredLinkId || linkId === selectedLinkId) {
|
|
800
|
-
return true;
|
|
801
|
-
}
|
|
802
|
-
if (hoveredNodeId && (sourceId === hoveredNodeId || targetId === hoveredNodeId)) {
|
|
803
|
-
return true;
|
|
804
|
-
}
|
|
805
|
-
if (selectedNodeId && (sourceId === selectedNodeId || targetId === selectedNodeId)) {
|
|
806
|
-
return true;
|
|
807
|
-
}
|
|
808
|
-
return false;
|
|
809
|
-
};
|
|
810
|
-
}
|
|
811
|
-
/**
|
|
812
|
-
* Create highlight checker for labels (same logic as their parent entities)
|
|
813
|
-
*/
|
|
814
|
-
static createLabelHighlightChecker(entityHighlightChecker) {
|
|
815
|
-
return entityHighlightChecker;
|
|
816
|
-
}
|
|
817
|
-
};
|
|
818
|
-
|
|
819
751
|
// src/v2/utils/timer-manager.ts
|
|
820
752
|
var TimerManager = class {
|
|
821
753
|
activeTimers = /* @__PURE__ */ new Map();
|
|
@@ -1991,15 +1923,278 @@ function manyBody_default() {
|
|
|
1991
1923
|
return force;
|
|
1992
1924
|
}
|
|
1993
1925
|
|
|
1926
|
+
// src/v2/core/state-manager.ts
|
|
1927
|
+
var StateManager = class {
|
|
1928
|
+
// Core entity maps for O(1) lookups
|
|
1929
|
+
nodeMap = /* @__PURE__ */ new Map();
|
|
1930
|
+
linkMap = /* @__PURE__ */ new Map();
|
|
1931
|
+
// Using linkId as key
|
|
1932
|
+
// State cache maps for performance
|
|
1933
|
+
nodeStateCache = /* @__PURE__ */ new Map();
|
|
1934
|
+
linkStateCache = /* @__PURE__ */ new Map();
|
|
1935
|
+
// Link ID to Link mapping for efficient lookups
|
|
1936
|
+
linkIdToLinkMap = /* @__PURE__ */ new Map();
|
|
1937
|
+
// Highlight state tracking
|
|
1938
|
+
highlightedNodes = /* @__PURE__ */ new Set();
|
|
1939
|
+
/**
|
|
1940
|
+
* Initialize with graph data
|
|
1941
|
+
*/
|
|
1942
|
+
initialize(state) {
|
|
1943
|
+
try {
|
|
1944
|
+
this.buildNodeMap(state.nodes);
|
|
1945
|
+
this.buildLinkMaps(state.links);
|
|
1946
|
+
this.clearStateCache();
|
|
1947
|
+
} catch (error) {
|
|
1948
|
+
ErrorHandler.logError(error);
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
/**
|
|
1952
|
+
* Update with new graph data
|
|
1953
|
+
*/
|
|
1954
|
+
updateState(state) {
|
|
1955
|
+
try {
|
|
1956
|
+
this.nodeMap.clear();
|
|
1957
|
+
this.linkMap.clear();
|
|
1958
|
+
this.linkIdToLinkMap.clear();
|
|
1959
|
+
this.buildNodeMap(state.nodes);
|
|
1960
|
+
this.buildLinkMaps(state.links);
|
|
1961
|
+
this.clearStateCache();
|
|
1962
|
+
} catch (error) {
|
|
1963
|
+
ErrorHandler.logError(error);
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
/**
|
|
1967
|
+
* Build node map for O(1) node lookups
|
|
1968
|
+
*/
|
|
1969
|
+
buildNodeMap(nodes) {
|
|
1970
|
+
for (const node of nodes) {
|
|
1971
|
+
this.nodeMap.set(node.id, node);
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
/**
|
|
1975
|
+
* Build link maps for O(1) link lookups
|
|
1976
|
+
*/
|
|
1977
|
+
buildLinkMaps(links) {
|
|
1978
|
+
for (const link of links) {
|
|
1979
|
+
const linkId = this.getLinkId(link);
|
|
1980
|
+
this.linkMap.set(linkId, link);
|
|
1981
|
+
this.linkIdToLinkMap.set(linkId, link);
|
|
1982
|
+
if (typeof link.source === "string") {
|
|
1983
|
+
const sourceNode = this.nodeMap.get(link.source);
|
|
1984
|
+
if (sourceNode) {
|
|
1985
|
+
link.source = sourceNode;
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
if (typeof link.target === "string") {
|
|
1989
|
+
const targetNode = this.nodeMap.get(link.target);
|
|
1990
|
+
if (targetNode) {
|
|
1991
|
+
link.target = targetNode;
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
/**
|
|
1997
|
+
* Get node by ID (O(1) lookup)
|
|
1998
|
+
*/
|
|
1999
|
+
getNode(nodeId) {
|
|
2000
|
+
return this.nodeMap.get(nodeId);
|
|
2001
|
+
}
|
|
2002
|
+
/**
|
|
2003
|
+
* Get link by link ID (O(1) lookup)
|
|
2004
|
+
*/
|
|
2005
|
+
getLink(linkId) {
|
|
2006
|
+
return this.linkMap.get(linkId);
|
|
2007
|
+
}
|
|
2008
|
+
/**
|
|
2009
|
+
* Get link by source/target IDs (O(1) lookup)
|
|
2010
|
+
*/
|
|
2011
|
+
getLinkByNodes(sourceId, targetId) {
|
|
2012
|
+
const linkId = `${sourceId}->${targetId}`;
|
|
2013
|
+
return this.linkMap.get(linkId);
|
|
2014
|
+
}
|
|
2015
|
+
/**
|
|
2016
|
+
* Get all nodes (returns reference to avoid copying)
|
|
2017
|
+
*/
|
|
2018
|
+
getAllNodes() {
|
|
2019
|
+
return Array.from(this.nodeMap.values());
|
|
2020
|
+
}
|
|
2021
|
+
/**
|
|
2022
|
+
* Get all links (returns reference to avoid copying)
|
|
2023
|
+
*/
|
|
2024
|
+
getAllLinks() {
|
|
2025
|
+
return Array.from(this.linkMap.values());
|
|
2026
|
+
}
|
|
2027
|
+
/**
|
|
2028
|
+
* Get node map (for renderers that need direct map access)
|
|
2029
|
+
*/
|
|
2030
|
+
getNodeMap() {
|
|
2031
|
+
return this.nodeMap;
|
|
2032
|
+
}
|
|
2033
|
+
/**
|
|
2034
|
+
* Get link ID to link map (for renderers that need direct map access)
|
|
2035
|
+
*/
|
|
2036
|
+
getLinkIdToLinkMap() {
|
|
2037
|
+
return this.linkIdToLinkMap;
|
|
2038
|
+
}
|
|
2039
|
+
/**
|
|
2040
|
+
* Cache node state for performance (avoid repeated hover/selection checks)
|
|
2041
|
+
*/
|
|
2042
|
+
cacheNodeState(nodeId, state) {
|
|
2043
|
+
this.nodeStateCache.set(nodeId, state);
|
|
2044
|
+
}
|
|
2045
|
+
/**
|
|
2046
|
+
* Get cached node state
|
|
2047
|
+
*/
|
|
2048
|
+
getCachedNodeState(nodeId) {
|
|
2049
|
+
return this.nodeStateCache.get(nodeId);
|
|
2050
|
+
}
|
|
2051
|
+
/**
|
|
2052
|
+
* Cache link state for performance (avoid repeated hover/selection checks)
|
|
2053
|
+
*/
|
|
2054
|
+
cacheLinkState(linkId, state) {
|
|
2055
|
+
this.linkStateCache.set(linkId, state);
|
|
2056
|
+
}
|
|
2057
|
+
/**
|
|
2058
|
+
* Get cached link state
|
|
2059
|
+
*/
|
|
2060
|
+
getCachedLinkState(linkId) {
|
|
2061
|
+
return this.linkStateCache.get(linkId);
|
|
2062
|
+
}
|
|
2063
|
+
/**
|
|
2064
|
+
* Clear state caches (call when hover/selection state changes)
|
|
2065
|
+
*/
|
|
2066
|
+
clearStateCache() {
|
|
2067
|
+
this.nodeStateCache.clear();
|
|
2068
|
+
this.linkStateCache.clear();
|
|
2069
|
+
}
|
|
2070
|
+
/**
|
|
2071
|
+
* Clear only node state cache
|
|
2072
|
+
*/
|
|
2073
|
+
clearNodeStateCache() {
|
|
2074
|
+
this.nodeStateCache.clear();
|
|
2075
|
+
}
|
|
2076
|
+
/**
|
|
2077
|
+
* Clear only link state cache
|
|
2078
|
+
*/
|
|
2079
|
+
clearLinkStateCache() {
|
|
2080
|
+
this.linkStateCache.clear();
|
|
2081
|
+
}
|
|
2082
|
+
/**
|
|
2083
|
+
* Generate consistent link ID
|
|
2084
|
+
*/
|
|
2085
|
+
getLinkId(link) {
|
|
2086
|
+
const sourceId = typeof link.source === "string" ? link.source : link.source.id;
|
|
2087
|
+
const targetId = typeof link.target === "string" ? link.target : link.target.id;
|
|
2088
|
+
return `${sourceId}->${targetId}`;
|
|
2089
|
+
}
|
|
2090
|
+
/**
|
|
2091
|
+
* Calculate link midpoint (common utility)
|
|
2092
|
+
*/
|
|
2093
|
+
getLinkMidpoint(link) {
|
|
2094
|
+
const sourceNode = typeof link.source === "string" ? this.nodeMap.get(link.source) : link.source;
|
|
2095
|
+
const targetNode = typeof link.target === "string" ? this.nodeMap.get(link.target) : link.target;
|
|
2096
|
+
if (!sourceNode || !targetNode || sourceNode.x === void 0 || sourceNode.y === void 0 || targetNode.x === void 0 || targetNode.y === void 0) {
|
|
2097
|
+
return null;
|
|
2098
|
+
}
|
|
2099
|
+
return {
|
|
2100
|
+
x: (sourceNode.x + targetNode.x) / 2,
|
|
2101
|
+
y: (sourceNode.y + targetNode.y) / 2
|
|
2102
|
+
};
|
|
2103
|
+
}
|
|
2104
|
+
/**
|
|
2105
|
+
* Highlight a node by ID
|
|
2106
|
+
*/
|
|
2107
|
+
highlightNode(nodeId) {
|
|
2108
|
+
if (this.nodeMap.has(nodeId)) {
|
|
2109
|
+
this.highlightedNodes.add(nodeId);
|
|
2110
|
+
this.clearSingleNodeStateCache(nodeId);
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
/**
|
|
2114
|
+
* Highlight multiple nodes by IDs
|
|
2115
|
+
*/
|
|
2116
|
+
highlightNodes(nodeIds) {
|
|
2117
|
+
for (const nodeId of nodeIds) {
|
|
2118
|
+
if (this.nodeMap.has(nodeId)) {
|
|
2119
|
+
this.highlightedNodes.add(nodeId);
|
|
2120
|
+
this.clearSingleNodeStateCache(nodeId);
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
/**
|
|
2125
|
+
* Remove highlight from a node
|
|
2126
|
+
*/
|
|
2127
|
+
unhighlightNode(nodeId) {
|
|
2128
|
+
if (this.highlightedNodes.has(nodeId)) {
|
|
2129
|
+
this.highlightedNodes.delete(nodeId);
|
|
2130
|
+
this.clearSingleNodeStateCache(nodeId);
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
/**
|
|
2134
|
+
* Clear all node highlights
|
|
2135
|
+
*/
|
|
2136
|
+
clearHighlights() {
|
|
2137
|
+
const highlightedIds = Array.from(this.highlightedNodes);
|
|
2138
|
+
this.highlightedNodes.clear();
|
|
2139
|
+
for (const nodeId of highlightedIds) {
|
|
2140
|
+
this.clearSingleNodeStateCache(nodeId);
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
/**
|
|
2144
|
+
* Check if a node is highlighted
|
|
2145
|
+
*/
|
|
2146
|
+
isNodeHighlighted(nodeId) {
|
|
2147
|
+
return this.highlightedNodes.has(nodeId);
|
|
2148
|
+
}
|
|
2149
|
+
/**
|
|
2150
|
+
* Get all highlighted node IDs
|
|
2151
|
+
*/
|
|
2152
|
+
getHighlightedNodes() {
|
|
2153
|
+
return new Set(this.highlightedNodes);
|
|
2154
|
+
}
|
|
2155
|
+
/**
|
|
2156
|
+
* Clear state cache for a specific node
|
|
2157
|
+
*/
|
|
2158
|
+
clearSingleNodeStateCache(nodeId) {
|
|
2159
|
+
this.nodeStateCache.delete(nodeId);
|
|
2160
|
+
}
|
|
2161
|
+
/**
|
|
2162
|
+
* Get statistics about cached state
|
|
2163
|
+
*/
|
|
2164
|
+
getStats() {
|
|
2165
|
+
return {
|
|
2166
|
+
nodeCount: this.nodeMap.size,
|
|
2167
|
+
linkCount: this.linkMap.size,
|
|
2168
|
+
cachedNodeStates: this.nodeStateCache.size,
|
|
2169
|
+
cachedLinkStates: this.linkStateCache.size,
|
|
2170
|
+
highlightedNodes: this.highlightedNodes.size
|
|
2171
|
+
};
|
|
2172
|
+
}
|
|
2173
|
+
/**
|
|
2174
|
+
* Destroy and clean up all maps
|
|
2175
|
+
*/
|
|
2176
|
+
destroy() {
|
|
2177
|
+
this.nodeMap.clear();
|
|
2178
|
+
this.linkMap.clear();
|
|
2179
|
+
this.linkIdToLinkMap.clear();
|
|
2180
|
+
this.nodeStateCache.clear();
|
|
2181
|
+
this.linkStateCache.clear();
|
|
2182
|
+
this.highlightedNodes.clear();
|
|
2183
|
+
}
|
|
2184
|
+
};
|
|
2185
|
+
|
|
1994
2186
|
// src/v2/core/physics-manager.ts
|
|
1995
2187
|
var PhysicsManager = class {
|
|
1996
2188
|
simulation;
|
|
1997
2189
|
config;
|
|
1998
2190
|
simulationStartTime;
|
|
1999
2191
|
simulationEndTime;
|
|
2000
|
-
cooldownTimer;
|
|
2001
2192
|
hasInitialAutoFitCompleted = false;
|
|
2002
2193
|
timerManager;
|
|
2194
|
+
isVisibilityListenerAttached = false;
|
|
2195
|
+
stateManager = new StateManager();
|
|
2196
|
+
isWarmingUp = false;
|
|
2197
|
+
warmupSteps = 0;
|
|
2003
2198
|
constructor(timerManager) {
|
|
2004
2199
|
this.timerManager = timerManager;
|
|
2005
2200
|
}
|
|
@@ -2013,34 +2208,52 @@ var PhysicsManager = class {
|
|
|
2013
2208
|
if (typeof config.onTick !== "function") {
|
|
2014
2209
|
throw new ValidationError("onTick callback is required and must be a function");
|
|
2015
2210
|
}
|
|
2211
|
+
if (!this.isVisibilityListenerAttached) {
|
|
2212
|
+
document.addEventListener("visibilitychange", this.handleVisibilityChange);
|
|
2213
|
+
this.isVisibilityListenerAttached = true;
|
|
2214
|
+
}
|
|
2016
2215
|
this.config = config;
|
|
2017
2216
|
this.simulationStartTime = performance.now();
|
|
2018
2217
|
const nodeCount = config.nodes.length;
|
|
2019
|
-
const graphArea = config.width * config.height;
|
|
2218
|
+
const graphArea = Math.max(config.width * config.height, 1);
|
|
2020
2219
|
const nodeDensity = nodeCount / (graphArea / 1e5);
|
|
2021
2220
|
const densityFactor = Math.min(nodeDensity, 2);
|
|
2022
2221
|
const baseVelocityDecay = 0.4;
|
|
2023
2222
|
const adaptiveVelocityDecay = Math.min(baseVelocityDecay + densityFactor * 0.2, 0.8);
|
|
2024
2223
|
const baseAlphaDecay = 0.02;
|
|
2025
2224
|
const adaptiveAlphaDecay = Math.min(baseAlphaDecay + densityFactor * 0.01, 0.05);
|
|
2225
|
+
if (this.simulation) {
|
|
2226
|
+
this.simulation.stop();
|
|
2227
|
+
}
|
|
2228
|
+
this.stateManager.initialize({ nodes: config.nodes, links: config.links });
|
|
2229
|
+
const linkDistance = nodeCount > 1e4 ? 400 : nodeCount > 6e3 ? 350 : nodeCount > 2e3 ? 300 : 250;
|
|
2230
|
+
const linkStrength = nodeCount > 1e4 ? 0.08 : nodeCount > 6e3 ? 0.12 : nodeCount > 2e3 ? 0.2 : 0.4;
|
|
2231
|
+
const chargeStrength = nodeCount > 1e4 ? -800 : nodeCount > 6e3 ? -700 : nodeCount > 2e3 ? -600 : -500;
|
|
2232
|
+
const collisionRadius = nodeCount > 1e4 ? 5 : nodeCount > 6e3 ? 4 : 3;
|
|
2233
|
+
const collisionIterations = nodeCount > 1e4 ? 2 : nodeCount > 6e3 ? 2 : 2;
|
|
2234
|
+
const centerStrength = nodeCount > 6e3 ? 0.05 : 0.3;
|
|
2026
2235
|
this.simulation = simulation_default(config.nodes).force(
|
|
2027
2236
|
"link",
|
|
2028
|
-
link_default(config.links).id((d) => d.id).distance(
|
|
2029
|
-
// Much weaker link strength to allow repulsion to work
|
|
2237
|
+
link_default(config.links).id((d) => d.id).distance(linkDistance).strength(linkStrength).iterations(1)
|
|
2030
2238
|
).force(
|
|
2031
2239
|
"charge",
|
|
2032
|
-
manyBody_default().strength(
|
|
2033
|
-
// Adaptive repulsion strength
|
|
2034
|
-
// .distanceMin(1) // Minimum distance for repulsion
|
|
2035
|
-
// .distanceMax(Math.max(300, 600 - densityFactor * 100)) // Reduce max distance for dense graphs
|
|
2240
|
+
manyBody_default().strength(chargeStrength).theta(nodeCount > 6e3 ? 0.8 : 0.9).distanceMax(nodeCount > 1e4 ? 800 : nodeCount > 6e3 ? 700 : 1e3)
|
|
2036
2241
|
).force(
|
|
2037
2242
|
"collision",
|
|
2038
|
-
collide_default().radius((node) =>
|
|
2243
|
+
collide_default().radius((node) => (node.style?.radius ?? 20) + collisionRadius).strength(1).iterations(collisionIterations)
|
|
2039
2244
|
).force(
|
|
2040
2245
|
"center",
|
|
2041
|
-
center_default(0, 0).strength(
|
|
2042
|
-
|
|
2043
|
-
|
|
2246
|
+
center_default(0, 0).strength(centerStrength)
|
|
2247
|
+
).velocityDecay(
|
|
2248
|
+
nodeCount > 1e4 ? 0.4 : nodeCount > 6e3 ? 0.5 : adaptiveVelocityDecay
|
|
2249
|
+
).alphaDecay(
|
|
2250
|
+
nodeCount > 1e4 ? 0.01 : nodeCount > 6e3 ? 0.02 : adaptiveAlphaDecay
|
|
2251
|
+
).alphaMin(
|
|
2252
|
+
nodeCount > 1e4 ? 0.01 : nodeCount > 6e3 ? 0.02 : 1e-3
|
|
2253
|
+
).on("tick", () => this.handleTick(config.onTick)).on("end", () => this.handleSimulationEnd());
|
|
2254
|
+
if (nodeCount > 1e3) {
|
|
2255
|
+
this.startSmoothWarmup();
|
|
2256
|
+
}
|
|
2044
2257
|
if (config.cooldownTime) {
|
|
2045
2258
|
this.setupCooldownTimer(config.cooldownTime);
|
|
2046
2259
|
}
|
|
@@ -2053,12 +2266,64 @@ var PhysicsManager = class {
|
|
|
2053
2266
|
throw error;
|
|
2054
2267
|
}
|
|
2055
2268
|
}
|
|
2269
|
+
/**
|
|
2270
|
+
* Handle tick with smooth warmup progression
|
|
2271
|
+
*/
|
|
2272
|
+
handleTick(originalOnTick) {
|
|
2273
|
+
if (this.isWarmingUp) {
|
|
2274
|
+
this.progressWarmup();
|
|
2275
|
+
}
|
|
2276
|
+
originalOnTick();
|
|
2277
|
+
}
|
|
2278
|
+
/**
|
|
2279
|
+
* Start smooth warmup process for large graphs
|
|
2280
|
+
*/
|
|
2281
|
+
startSmoothWarmup() {
|
|
2282
|
+
if (!this.simulation || !this.config) return;
|
|
2283
|
+
this.isWarmingUp = true;
|
|
2284
|
+
this.warmupSteps = 0;
|
|
2285
|
+
this.applyWarmupForces(0.2);
|
|
2286
|
+
}
|
|
2287
|
+
/**
|
|
2288
|
+
* Progress warmup over time for smoother initial animation
|
|
2289
|
+
*/
|
|
2290
|
+
progressWarmup() {
|
|
2291
|
+
if (!this.simulation || !this.config) return;
|
|
2292
|
+
this.warmupSteps++;
|
|
2293
|
+
const maxWarmupSteps = 60;
|
|
2294
|
+
const progress = Math.min(this.warmupSteps / maxWarmupSteps, 1);
|
|
2295
|
+
if (progress >= 1) {
|
|
2296
|
+
this.isWarmingUp = false;
|
|
2297
|
+
this.applyWarmupForces(1);
|
|
2298
|
+
} else {
|
|
2299
|
+
const forceMultiplier = 0.2 + progress * 0.8;
|
|
2300
|
+
this.applyWarmupForces(forceMultiplier);
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
/**
|
|
2304
|
+
* Apply scaled forces during warmup
|
|
2305
|
+
*/
|
|
2306
|
+
applyWarmupForces(multiplier) {
|
|
2307
|
+
if (!this.simulation || !this.config) return;
|
|
2308
|
+
const nodeCount = this.config.nodes.length;
|
|
2309
|
+
const linkStrength = (nodeCount > 1e4 ? 0.08 : nodeCount > 6e3 ? 0.12 : nodeCount > 2e3 ? 0.2 : 0.4) * multiplier;
|
|
2310
|
+
const chargeStrength = (nodeCount > 1e4 ? -800 : nodeCount > 6e3 ? -700 : nodeCount > 2e3 ? -600 : -500) * multiplier;
|
|
2311
|
+
const linkForce = this.simulation.force("link");
|
|
2312
|
+
const chargeForce = this.simulation.force("charge");
|
|
2313
|
+
if (linkForce) {
|
|
2314
|
+
linkForce.strength(linkStrength);
|
|
2315
|
+
}
|
|
2316
|
+
if (chargeForce) {
|
|
2317
|
+
chargeForce.strength(chargeStrength);
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2056
2320
|
/**
|
|
2057
2321
|
* Handle simulation end
|
|
2058
2322
|
*/
|
|
2059
2323
|
handleSimulationEnd() {
|
|
2060
2324
|
try {
|
|
2061
2325
|
this.simulationEndTime = performance.now();
|
|
2326
|
+
this.isWarmingUp = false;
|
|
2062
2327
|
if (this.config?.onEnd) {
|
|
2063
2328
|
this.config.onEnd();
|
|
2064
2329
|
}
|
|
@@ -2105,11 +2370,15 @@ var PhysicsManager = class {
|
|
|
2105
2370
|
/**
|
|
2106
2371
|
* Reheat simulation for drag interactions
|
|
2107
2372
|
*/
|
|
2108
|
-
reheat(alphaTarget
|
|
2109
|
-
if (!this.simulation)
|
|
2373
|
+
reheat(alphaTarget) {
|
|
2374
|
+
if (!this.simulation || !this.config) {
|
|
2375
|
+
return;
|
|
2376
|
+
}
|
|
2377
|
+
const nodeCount = this.config.nodes.length;
|
|
2378
|
+
const effectiveAlpha = alphaTarget ?? (nodeCount > 1e4 ? 0.03 : nodeCount > 5e3 ? 0.05 : nodeCount > 2e3 ? 0.08 : 0.15);
|
|
2110
2379
|
try {
|
|
2111
|
-
this.simulation.alphaTarget(
|
|
2112
|
-
if (this.config
|
|
2380
|
+
this.simulation.alphaTarget(effectiveAlpha).restart();
|
|
2381
|
+
if (this.config.cooldownTime) {
|
|
2113
2382
|
this.setupCooldownTimer(this.config.cooldownTime);
|
|
2114
2383
|
}
|
|
2115
2384
|
} catch (error) {
|
|
@@ -2179,14 +2448,14 @@ var PhysicsManager = class {
|
|
|
2179
2448
|
adjustLinkDistancesForVisualShortening() {
|
|
2180
2449
|
if (!this.simulation || !this.config) return;
|
|
2181
2450
|
try {
|
|
2451
|
+
const baseDistance = this.calculateBaseDistance();
|
|
2182
2452
|
const linkForce = this.simulation.force("link");
|
|
2183
2453
|
if (linkForce) {
|
|
2184
2454
|
linkForce.distance((link) => {
|
|
2185
|
-
const
|
|
2186
|
-
const
|
|
2187
|
-
const
|
|
2188
|
-
const
|
|
2189
|
-
const targetRadius = this.getNodeRadius(targetNode);
|
|
2455
|
+
const sourceNode = typeof link.source === "string" ? this.stateManager.getNode(link.source) : link.source;
|
|
2456
|
+
const targetNode = typeof link.target === "string" ? this.stateManager.getNode(link.target) : link.target;
|
|
2457
|
+
const sourceRadius = sourceNode?.style?.radius ?? 20;
|
|
2458
|
+
const targetRadius = targetNode?.style?.radius ?? 20;
|
|
2190
2459
|
const arrowLength = this.getLinkArrowLength(link);
|
|
2191
2460
|
const visualCompensation = sourceRadius + targetRadius + arrowLength;
|
|
2192
2461
|
const spacingBuffer = Math.max(20, (sourceRadius + targetRadius) * 0.5);
|
|
@@ -2202,24 +2471,13 @@ var PhysicsManager = class {
|
|
|
2202
2471
|
* Calculate base distance based on graph size and node count
|
|
2203
2472
|
*/
|
|
2204
2473
|
calculateBaseDistance() {
|
|
2205
|
-
if (!this.config) return
|
|
2474
|
+
if (!this.config) return 150;
|
|
2206
2475
|
const nodeCount = this.config.nodes.length;
|
|
2207
|
-
const graphArea = this.config.width * this.config.height;
|
|
2476
|
+
const graphArea = Math.max(this.config.width * this.config.height, 1);
|
|
2208
2477
|
const nodeAreaRatio = nodeCount / (graphArea / 1e4);
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
* Find node by ID
|
|
2213
|
-
*/
|
|
2214
|
-
findNodeById(id2) {
|
|
2215
|
-
return this.config?.nodes.find((node) => node.id === id2);
|
|
2216
|
-
}
|
|
2217
|
-
/**
|
|
2218
|
-
* Get node radius from style or default
|
|
2219
|
-
*/
|
|
2220
|
-
getNodeRadius(node) {
|
|
2221
|
-
if (!node) return 20;
|
|
2222
|
-
return node.style?.radius ?? 20;
|
|
2478
|
+
const baseDistance = nodeCount > 1e4 ? 300 : nodeCount > 6e3 ? 250 : nodeCount > 2e3 ? 200 : 150;
|
|
2479
|
+
const areaAdjustment = Math.min(100, nodeAreaRatio * 30);
|
|
2480
|
+
return Math.max(baseDistance, baseDistance + areaAdjustment);
|
|
2223
2481
|
}
|
|
2224
2482
|
/**
|
|
2225
2483
|
* Get arrow length from link style or default
|
|
@@ -2231,15 +2489,38 @@ var PhysicsManager = class {
|
|
|
2231
2489
|
return linkStyle?.arrow?.size ?? 8;
|
|
2232
2490
|
}
|
|
2233
2491
|
/**
|
|
2234
|
-
* Initialize node positions
|
|
2492
|
+
* Initialize node positions with improved distribution for smoother startup
|
|
2235
2493
|
*/
|
|
2236
2494
|
initializePositions() {
|
|
2237
2495
|
if (!this.config) return;
|
|
2238
2496
|
try {
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2497
|
+
const nodeCount = this.config.nodes.length;
|
|
2498
|
+
const centerX = this.config.width / 2;
|
|
2499
|
+
const centerY = this.config.height / 2;
|
|
2500
|
+
const radius = Math.min(this.config.width, this.config.height) * 0.3;
|
|
2501
|
+
for (let i = 0; i < this.config.nodes.length; i++) {
|
|
2502
|
+
const node = this.config.nodes[i];
|
|
2503
|
+
if (!node) continue;
|
|
2504
|
+
if (node.x == null || node.y == null) {
|
|
2505
|
+
if (nodeCount === 1) {
|
|
2506
|
+
node.x = centerX;
|
|
2507
|
+
node.y = centerY;
|
|
2508
|
+
} else if (nodeCount <= 10) {
|
|
2509
|
+
const angle = i / nodeCount * 2 * Math.PI;
|
|
2510
|
+
node.x = centerX + Math.cos(angle) * radius * 0.5;
|
|
2511
|
+
node.y = centerY + Math.sin(angle) * radius * 0.5;
|
|
2512
|
+
} else if (nodeCount <= 100) {
|
|
2513
|
+
const angle = i * 0.5;
|
|
2514
|
+
const spiralRadius = i / nodeCount * radius;
|
|
2515
|
+
node.x = centerX + Math.cos(angle) * spiralRadius;
|
|
2516
|
+
node.y = centerY + Math.sin(angle) * spiralRadius;
|
|
2517
|
+
} else {
|
|
2518
|
+
const clusterRadius = radius * 0.6;
|
|
2519
|
+
const angle = Math.random() * 2 * Math.PI;
|
|
2520
|
+
const distance = Math.random() * clusterRadius;
|
|
2521
|
+
node.x = centerX + Math.cos(angle) * distance;
|
|
2522
|
+
node.y = centerY + Math.sin(angle) * distance;
|
|
2523
|
+
}
|
|
2243
2524
|
}
|
|
2244
2525
|
}
|
|
2245
2526
|
} catch (error) {
|
|
@@ -2285,33 +2566,57 @@ var PhysicsManager = class {
|
|
|
2285
2566
|
* Pause the simulation
|
|
2286
2567
|
*/
|
|
2287
2568
|
pause() {
|
|
2288
|
-
if (this.simulation) {
|
|
2289
|
-
|
|
2569
|
+
if (!this.simulation) {
|
|
2570
|
+
return;
|
|
2290
2571
|
}
|
|
2572
|
+
this.timerManager.clearTimer("simulationCooldown");
|
|
2573
|
+
this.simulation.stop();
|
|
2291
2574
|
}
|
|
2292
2575
|
/**
|
|
2293
2576
|
* Resume the simulation
|
|
2294
2577
|
*/
|
|
2295
2578
|
resume() {
|
|
2296
|
-
if (this.simulation) {
|
|
2297
|
-
|
|
2579
|
+
if (!this.simulation) {
|
|
2580
|
+
return;
|
|
2581
|
+
}
|
|
2582
|
+
const nodeCount = this.config?.nodes.length ?? 0;
|
|
2583
|
+
const alpha = nodeCount > 1e4 ? 0.05 : nodeCount > 5e3 ? 0.08 : nodeCount > 2e3 ? 0.12 : 0.3;
|
|
2584
|
+
this.simulation.alpha(alpha).alphaTarget(0).restart();
|
|
2585
|
+
if (this.config?.cooldownTime) {
|
|
2586
|
+
this.setupCooldownTimer(this.config.cooldownTime);
|
|
2298
2587
|
}
|
|
2299
2588
|
}
|
|
2589
|
+
handleVisibilityChange = () => {
|
|
2590
|
+
if (document.visibilityState !== "visible") {
|
|
2591
|
+
this.pause();
|
|
2592
|
+
} else {
|
|
2593
|
+
this.resume();
|
|
2594
|
+
if (this.hasInitialAutoFitCompleted && this.simulation && this.simulation.alpha() > 0) {
|
|
2595
|
+
this.hasInitialAutoFitCompleted = false;
|
|
2596
|
+
}
|
|
2597
|
+
}
|
|
2598
|
+
};
|
|
2300
2599
|
/**
|
|
2301
2600
|
* Destroy physics simulation
|
|
2302
2601
|
*/
|
|
2303
2602
|
destroy() {
|
|
2304
2603
|
try {
|
|
2604
|
+
if (this.isVisibilityListenerAttached) {
|
|
2605
|
+
document.removeEventListener("visibilitychange", this.handleVisibilityChange);
|
|
2606
|
+
this.isVisibilityListenerAttached = false;
|
|
2607
|
+
}
|
|
2305
2608
|
this.timerManager.clearTimer("simulationCooldown");
|
|
2306
|
-
this.cooldownTimer = void 0;
|
|
2307
2609
|
if (this.simulation) {
|
|
2308
2610
|
this.simulation.stop();
|
|
2309
2611
|
this.simulation = void 0;
|
|
2310
2612
|
}
|
|
2311
2613
|
this.config = void 0;
|
|
2614
|
+
this.stateManager.destroy();
|
|
2312
2615
|
this.simulationStartTime = void 0;
|
|
2313
2616
|
this.simulationEndTime = void 0;
|
|
2314
2617
|
this.hasInitialAutoFitCompleted = false;
|
|
2618
|
+
this.isWarmingUp = false;
|
|
2619
|
+
this.warmupSteps = 0;
|
|
2315
2620
|
} catch (error) {
|
|
2316
2621
|
ErrorHandler.logError(error);
|
|
2317
2622
|
}
|
|
@@ -5097,6 +5402,10 @@ var DragManager = class {
|
|
|
5097
5402
|
};
|
|
5098
5403
|
DRAG_CLICK_TOLERANCE_PX = 3;
|
|
5099
5404
|
// Force-graph constant
|
|
5405
|
+
// RAF throttling for smooth drag performance
|
|
5406
|
+
dragRenderPending = false;
|
|
5407
|
+
lastDragRenderTime = 0;
|
|
5408
|
+
pendingAnimationFrame;
|
|
5100
5409
|
/**
|
|
5101
5410
|
* Initialize drag behavior
|
|
5102
5411
|
*/
|
|
@@ -5141,11 +5450,15 @@ var DragManager = class {
|
|
|
5141
5450
|
fy: obj.fy
|
|
5142
5451
|
};
|
|
5143
5452
|
if (!event.active) {
|
|
5144
|
-
this.config.physicsManager.reheat(
|
|
5453
|
+
this.config.physicsManager.reheat();
|
|
5145
5454
|
obj.fx = obj.x;
|
|
5146
5455
|
obj.fy = obj.y;
|
|
5147
5456
|
}
|
|
5148
5457
|
this.config.canvas.classList.add("grabbable");
|
|
5458
|
+
if (this.config.renderer) {
|
|
5459
|
+
this.config.renderer.setDraggedNode(obj);
|
|
5460
|
+
this.config.renderer.setDragState(true);
|
|
5461
|
+
}
|
|
5149
5462
|
} catch (error) {
|
|
5150
5463
|
ErrorHandler.logError(error, {
|
|
5151
5464
|
nodeId: obj?.id,
|
|
@@ -5178,7 +5491,7 @@ var DragManager = class {
|
|
|
5178
5491
|
this.state.isDragging = true;
|
|
5179
5492
|
this.state.isPointerDragging = true;
|
|
5180
5493
|
obj.__dragged = true;
|
|
5181
|
-
this.
|
|
5494
|
+
this.throttledDragRender();
|
|
5182
5495
|
} catch (error) {
|
|
5183
5496
|
ErrorHandler.logError(error, {
|
|
5184
5497
|
nodeId: obj?.id,
|
|
@@ -5186,6 +5499,24 @@ var DragManager = class {
|
|
|
5186
5499
|
});
|
|
5187
5500
|
}
|
|
5188
5501
|
}
|
|
5502
|
+
/**
|
|
5503
|
+
* RAF-throttled render during drag (like ZoomManager pattern)
|
|
5504
|
+
*/
|
|
5505
|
+
throttledDragRender() {
|
|
5506
|
+
if (!this.config) return;
|
|
5507
|
+
const now2 = performance.now();
|
|
5508
|
+
if (now2 - this.lastDragRenderTime < 16) return;
|
|
5509
|
+
if (this.dragRenderPending) return;
|
|
5510
|
+
this.dragRenderPending = true;
|
|
5511
|
+
this.pendingAnimationFrame = requestAnimationFrame(() => {
|
|
5512
|
+
this.pendingAnimationFrame = void 0;
|
|
5513
|
+
this.dragRenderPending = false;
|
|
5514
|
+
this.lastDragRenderTime = performance.now();
|
|
5515
|
+
if (this.config && this.state.isPointerDragging) {
|
|
5516
|
+
this.config.onRender();
|
|
5517
|
+
}
|
|
5518
|
+
});
|
|
5519
|
+
}
|
|
5189
5520
|
/**
|
|
5190
5521
|
* Handle drag end
|
|
5191
5522
|
*/
|
|
@@ -5210,6 +5541,9 @@ var DragManager = class {
|
|
|
5210
5541
|
this.config.canvas.classList.remove("grabbable");
|
|
5211
5542
|
this.state.isDragging = false;
|
|
5212
5543
|
this.state.isPointerDragging = false;
|
|
5544
|
+
if (this.config.renderer) {
|
|
5545
|
+
this.config.renderer.setDragState(false);
|
|
5546
|
+
}
|
|
5213
5547
|
if (obj.__dragged) {
|
|
5214
5548
|
delete obj.__dragged;
|
|
5215
5549
|
this.config.onRender();
|
|
@@ -5244,6 +5578,10 @@ var DragManager = class {
|
|
|
5244
5578
|
*/
|
|
5245
5579
|
destroy() {
|
|
5246
5580
|
try {
|
|
5581
|
+
if (this.pendingAnimationFrame !== void 0) {
|
|
5582
|
+
cancelAnimationFrame(this.pendingAnimationFrame);
|
|
5583
|
+
this.pendingAnimationFrame = void 0;
|
|
5584
|
+
}
|
|
5247
5585
|
if (this.config?.canvas) {
|
|
5248
5586
|
select_default2(this.config.canvas).on(".drag", null);
|
|
5249
5587
|
this.config.canvas.classList.remove("grabbable");
|
|
@@ -5263,6 +5601,10 @@ var DragManager = class {
|
|
|
5263
5601
|
var ZoomManager = class {
|
|
5264
5602
|
config;
|
|
5265
5603
|
zoomBehavior;
|
|
5604
|
+
isZooming = false;
|
|
5605
|
+
zoomRenderPending = false;
|
|
5606
|
+
isProgrammaticZoom = false;
|
|
5607
|
+
// Flag for programmatic zoom operations
|
|
5266
5608
|
/**
|
|
5267
5609
|
* Initialize zoom behavior
|
|
5268
5610
|
*/
|
|
@@ -5310,49 +5652,69 @@ var ZoomManager = class {
|
|
|
5310
5652
|
* Handle zoom start (when panning begins)
|
|
5311
5653
|
*/
|
|
5312
5654
|
handleZoomStart(event) {
|
|
5655
|
+
this.isZooming = true;
|
|
5313
5656
|
if (!this.config) return;
|
|
5314
5657
|
try {
|
|
5315
5658
|
event.sourceEvent?.stopPropagation();
|
|
5316
5659
|
event.sourceEvent?.preventDefault();
|
|
5317
|
-
this.config.
|
|
5660
|
+
if (this.config.renderer && !this.isProgrammaticZoom) {
|
|
5661
|
+
this.config.renderer.setZoomState(true);
|
|
5662
|
+
}
|
|
5663
|
+
if (!this.isProgrammaticZoom) {
|
|
5664
|
+
this.config.canvas.style.cursor = "grabbing";
|
|
5665
|
+
}
|
|
5318
5666
|
} catch (error) {
|
|
5319
5667
|
ErrorHandler.logError(error);
|
|
5320
5668
|
}
|
|
5321
5669
|
}
|
|
5322
5670
|
/**
|
|
5323
|
-
* Handle zoom events
|
|
5671
|
+
* Handle zoom events with RAF throttling for smooth performance
|
|
5324
5672
|
*/
|
|
5325
5673
|
handleZoom(event) {
|
|
5326
5674
|
if (!this.config) return;
|
|
5327
|
-
|
|
5328
|
-
|
|
5329
|
-
|
|
5330
|
-
|
|
5331
|
-
this.config.canvasManager.clear();
|
|
5332
|
-
this.config.canvasManager.applyTransform(transform2);
|
|
5333
|
-
this.config.onRender();
|
|
5334
|
-
} catch (error) {
|
|
5335
|
-
ErrorHandler.logError(error, {
|
|
5336
|
-
transform: event.transform
|
|
5337
|
-
});
|
|
5675
|
+
event.sourceEvent?.stopPropagation();
|
|
5676
|
+
event.sourceEvent?.preventDefault();
|
|
5677
|
+
if (this.zoomRenderPending) {
|
|
5678
|
+
return;
|
|
5338
5679
|
}
|
|
5680
|
+
this.zoomRenderPending = true;
|
|
5681
|
+
requestAnimationFrame(() => {
|
|
5682
|
+
this.zoomRenderPending = false;
|
|
5683
|
+
if (!this.config) {
|
|
5684
|
+
return;
|
|
5685
|
+
}
|
|
5686
|
+
this.config.onRender();
|
|
5687
|
+
});
|
|
5339
5688
|
}
|
|
5340
5689
|
/**
|
|
5341
5690
|
* Handle zoom end (when panning ends)
|
|
5342
5691
|
*/
|
|
5343
5692
|
handleZoomEnd(event) {
|
|
5693
|
+
this.isZooming = false;
|
|
5344
5694
|
if (!this.config) return;
|
|
5345
5695
|
try {
|
|
5346
5696
|
event.sourceEvent?.stopPropagation();
|
|
5347
5697
|
event.sourceEvent?.preventDefault();
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
this.config.canvas.style.cursor = "grab";
|
|
5698
|
+
if (this.config.renderer && !this.isProgrammaticZoom) {
|
|
5699
|
+
this.config.renderer.setZoomState(false);
|
|
5351
5700
|
}
|
|
5701
|
+
if (!this.isProgrammaticZoom) {
|
|
5702
|
+
const currentCursor = this.config.canvas.style.cursor;
|
|
5703
|
+
if (currentCursor === "grabbing") {
|
|
5704
|
+
this.config.canvas.style.cursor = "grab";
|
|
5705
|
+
}
|
|
5706
|
+
}
|
|
5707
|
+
if (this.config.onZoomEnd) {
|
|
5708
|
+
this.config.onZoomEnd();
|
|
5709
|
+
}
|
|
5710
|
+
this.isProgrammaticZoom = false;
|
|
5352
5711
|
} catch (error) {
|
|
5353
5712
|
ErrorHandler.logError(error);
|
|
5354
5713
|
}
|
|
5355
5714
|
}
|
|
5715
|
+
isCurrentlyZooming() {
|
|
5716
|
+
return this.isZooming;
|
|
5717
|
+
}
|
|
5356
5718
|
/**
|
|
5357
5719
|
* Get zoom behavior instance
|
|
5358
5720
|
*/
|
|
@@ -5384,6 +5746,7 @@ var ZoomManager = class {
|
|
|
5384
5746
|
zoomIn(factor = 1.5, center) {
|
|
5385
5747
|
if (!this.config?.canvas || !this.zoomBehavior) return;
|
|
5386
5748
|
try {
|
|
5749
|
+
this.isProgrammaticZoom = true;
|
|
5387
5750
|
const canvas = this.config.canvas;
|
|
5388
5751
|
if (center) {
|
|
5389
5752
|
select_default2(canvas).transition().duration(300).call(this.zoomBehavior.scaleBy, factor, center);
|
|
@@ -5406,6 +5769,7 @@ var ZoomManager = class {
|
|
|
5406
5769
|
resetZoom(duration = 500) {
|
|
5407
5770
|
if (!this.config?.canvas || !this.zoomBehavior) return;
|
|
5408
5771
|
try {
|
|
5772
|
+
this.isProgrammaticZoom = true;
|
|
5409
5773
|
const canvasDims = this.config.canvasManager.getDimensions();
|
|
5410
5774
|
const transform2 = identity2.translate(canvasDims.width / 2, canvasDims.height / 2);
|
|
5411
5775
|
select_default2(this.config.canvas).transition().duration(duration).call(this.zoomBehavior.transform, transform2);
|
|
@@ -5419,6 +5783,7 @@ var ZoomManager = class {
|
|
|
5419
5783
|
setTransform(transform2, duration = 0) {
|
|
5420
5784
|
if (!this.config?.canvas || !this.zoomBehavior) return;
|
|
5421
5785
|
try {
|
|
5786
|
+
this.isProgrammaticZoom = true;
|
|
5422
5787
|
const selection2 = select_default2(this.config.canvas);
|
|
5423
5788
|
const zoomTransform = identity2.translate(transform2.x, transform2.y).scale(transform2.k);
|
|
5424
5789
|
if (duration > 0) {
|
|
@@ -5440,6 +5805,7 @@ var ZoomManager = class {
|
|
|
5440
5805
|
return;
|
|
5441
5806
|
}
|
|
5442
5807
|
try {
|
|
5808
|
+
this.isProgrammaticZoom = true;
|
|
5443
5809
|
const canvas = this.config.canvas;
|
|
5444
5810
|
const transitionDuration = 750;
|
|
5445
5811
|
const canvasDims = this.config.canvasManager.getDimensions();
|
|
@@ -5528,6 +5894,8 @@ var HoverManager = class {
|
|
|
5528
5894
|
flushShadowCanvas;
|
|
5529
5895
|
hasValidPointerPosition = false;
|
|
5530
5896
|
containerWarningLogged = false;
|
|
5897
|
+
// Store bound handlers for proper cleanup
|
|
5898
|
+
boundHandlers = /* @__PURE__ */ new Map();
|
|
5531
5899
|
/**
|
|
5532
5900
|
* Initialize hover manager with force-graph pattern
|
|
5533
5901
|
*/
|
|
@@ -5559,7 +5927,7 @@ var HoverManager = class {
|
|
|
5559
5927
|
console.error("Cannot add pointer event listeners - container is null");
|
|
5560
5928
|
return;
|
|
5561
5929
|
}
|
|
5562
|
-
|
|
5930
|
+
const eventHandler = (ev) => {
|
|
5563
5931
|
const pointerEvent = ev;
|
|
5564
5932
|
const container = this.container;
|
|
5565
5933
|
if (!container) {
|
|
@@ -5584,7 +5952,9 @@ var HoverManager = class {
|
|
|
5584
5952
|
this.containerWarningLogged = true;
|
|
5585
5953
|
}
|
|
5586
5954
|
}
|
|
5587
|
-
}
|
|
5955
|
+
};
|
|
5956
|
+
this.boundHandlers.set(evType, eventHandler);
|
|
5957
|
+
this.container.addEventListener(evType, eventHandler, { passive: true });
|
|
5588
5958
|
});
|
|
5589
5959
|
}
|
|
5590
5960
|
/**
|
|
@@ -5805,6 +6175,12 @@ var HoverManager = class {
|
|
|
5805
6175
|
*/
|
|
5806
6176
|
destroy() {
|
|
5807
6177
|
try {
|
|
6178
|
+
if (this.container) {
|
|
6179
|
+
this.boundHandlers.forEach((handler, eventType) => {
|
|
6180
|
+
this.container.removeEventListener(eventType, handler);
|
|
6181
|
+
});
|
|
6182
|
+
}
|
|
6183
|
+
this.boundHandlers.clear();
|
|
5808
6184
|
this.eventHandlers.clear();
|
|
5809
6185
|
this.hoverState = {
|
|
5810
6186
|
currentHovered: null,
|
|
@@ -5833,6 +6209,8 @@ var SelectionManager = class {
|
|
|
5833
6209
|
};
|
|
5834
6210
|
eventHandlers = /* @__PURE__ */ new Map();
|
|
5835
6211
|
container;
|
|
6212
|
+
// Store bound handlers for proper cleanup
|
|
6213
|
+
boundHandlers = /* @__PURE__ */ new Map();
|
|
5836
6214
|
/**
|
|
5837
6215
|
* Initialize selection manager
|
|
5838
6216
|
*/
|
|
@@ -5854,14 +6232,19 @@ var SelectionManager = class {
|
|
|
5854
6232
|
*/
|
|
5855
6233
|
setupSelectionListeners() {
|
|
5856
6234
|
if (!this.container || !this.canvasState) return;
|
|
5857
|
-
|
|
6235
|
+
const clickHandler = (event) => {
|
|
5858
6236
|
this.handleSelectionClick(event);
|
|
5859
|
-
}
|
|
5860
|
-
|
|
5861
|
-
|
|
6237
|
+
};
|
|
6238
|
+
this.boundHandlers.set("click", clickHandler);
|
|
6239
|
+
this.container.addEventListener("click", clickHandler, { passive: false });
|
|
6240
|
+
const keydownHandler = (event) => {
|
|
6241
|
+
const keyboardEvent = event;
|
|
6242
|
+
if (keyboardEvent.key === "Escape") {
|
|
5862
6243
|
this.clearSelection();
|
|
5863
6244
|
}
|
|
5864
|
-
}
|
|
6245
|
+
};
|
|
6246
|
+
this.boundHandlers.set("keydown", keydownHandler);
|
|
6247
|
+
document.addEventListener("keydown", keydownHandler);
|
|
5865
6248
|
}
|
|
5866
6249
|
/**
|
|
5867
6250
|
* Handle selection click
|
|
@@ -6093,14 +6476,13 @@ var SelectionManager = class {
|
|
|
6093
6476
|
*/
|
|
6094
6477
|
destroy() {
|
|
6095
6478
|
try {
|
|
6096
|
-
if (this.container) {
|
|
6097
|
-
this.container.removeEventListener("click", this.
|
|
6479
|
+
if (this.container && this.boundHandlers.has("click")) {
|
|
6480
|
+
this.container.removeEventListener("click", this.boundHandlers.get("click"));
|
|
6098
6481
|
}
|
|
6099
|
-
|
|
6100
|
-
|
|
6101
|
-
|
|
6102
|
-
|
|
6103
|
-
});
|
|
6482
|
+
if (this.boundHandlers.has("keydown")) {
|
|
6483
|
+
document.removeEventListener("keydown", this.boundHandlers.get("keydown"));
|
|
6484
|
+
}
|
|
6485
|
+
this.boundHandlers.clear();
|
|
6104
6486
|
this.eventHandlers.clear();
|
|
6105
6487
|
this.selectionState = {
|
|
6106
6488
|
selectedNode: null,
|
|
@@ -6178,7 +6560,7 @@ var NodesRenderer = class {
|
|
|
6178
6560
|
/**
|
|
6179
6561
|
* Render nodes to canvas using StyleResolver with performance metrics
|
|
6180
6562
|
*/
|
|
6181
|
-
static renderWithStyleResolver(ctx, nodes, styleResolver, isNodeHovered, isNodeSelected, performanceMetrics) {
|
|
6563
|
+
static renderWithStyleResolver(ctx, nodes, styleResolver, isNodeHovered, isNodeSelected, isNodeHighlighted, performanceMetrics) {
|
|
6182
6564
|
try {
|
|
6183
6565
|
for (const node of nodes) {
|
|
6184
6566
|
const x3 = node.x;
|
|
@@ -6186,6 +6568,7 @@ var NodesRenderer = class {
|
|
|
6186
6568
|
const hoverStart = performance.now();
|
|
6187
6569
|
const isHovered = isNodeHovered(node.id);
|
|
6188
6570
|
const isSelected = isNodeSelected ? isNodeSelected(node.id) : false;
|
|
6571
|
+
const isHighlighted = isNodeHighlighted ? isNodeHighlighted(node.id) : false;
|
|
6189
6572
|
if (performanceMetrics) {
|
|
6190
6573
|
performanceMetrics.hoverChecks += performance.now() - hoverStart;
|
|
6191
6574
|
}
|
|
@@ -6193,7 +6576,8 @@ var NodesRenderer = class {
|
|
|
6193
6576
|
const style = styleResolver.resolveNodeStyle({
|
|
6194
6577
|
node,
|
|
6195
6578
|
isHovered,
|
|
6196
|
-
isSelected
|
|
6579
|
+
isSelected,
|
|
6580
|
+
isHighlighted
|
|
6197
6581
|
});
|
|
6198
6582
|
if (performanceMetrics) {
|
|
6199
6583
|
performanceMetrics.styleResolution += performance.now() - styleStart;
|
|
@@ -6343,6 +6727,514 @@ var NodeLabelsRenderer = class {
|
|
|
6343
6727
|
}
|
|
6344
6728
|
};
|
|
6345
6729
|
|
|
6730
|
+
// src/v2/rendering/z-index-renderer.ts
|
|
6731
|
+
var OptimizedZIndexRenderer = class _OptimizedZIndexRenderer {
|
|
6732
|
+
config;
|
|
6733
|
+
styleResolver;
|
|
6734
|
+
// Pre-computed adjacency maps for O(1) lookups
|
|
6735
|
+
nodeToLinksMap = /* @__PURE__ */ new Map();
|
|
6736
|
+
linkToNodesMap = /* @__PURE__ */ new Map();
|
|
6737
|
+
// Reusable arrays to minimize garbage collection
|
|
6738
|
+
reusableArrays = {
|
|
6739
|
+
backgroundNodes: [],
|
|
6740
|
+
foregroundNodes: [],
|
|
6741
|
+
backgroundLinks: [],
|
|
6742
|
+
foregroundLinks: []
|
|
6743
|
+
};
|
|
6744
|
+
// Pre-sorted arrays to avoid redundant sorting
|
|
6745
|
+
sortedBackgroundNodes = [];
|
|
6746
|
+
sortedForegroundNodes = [];
|
|
6747
|
+
// State checkers (injected from parent renderer)
|
|
6748
|
+
isNodeHovered = () => false;
|
|
6749
|
+
isNodeSelected = () => false;
|
|
6750
|
+
isLinkHovered = () => false;
|
|
6751
|
+
isLinkSelected = () => false;
|
|
6752
|
+
getLinkId = () => "";
|
|
6753
|
+
getLinkMidpoint = () => null;
|
|
6754
|
+
// Component renderers (injected)
|
|
6755
|
+
renderNodes;
|
|
6756
|
+
renderLinks;
|
|
6757
|
+
renderNodeLabels;
|
|
6758
|
+
/**
|
|
6759
|
+
* Initialize the optimized z-index renderer
|
|
6760
|
+
*/
|
|
6761
|
+
initialize(config) {
|
|
6762
|
+
this.config = { nodes: config.nodes, links: config.links };
|
|
6763
|
+
this.styleResolver = config.styleResolver;
|
|
6764
|
+
this.isNodeHovered = config.isNodeHovered;
|
|
6765
|
+
this.isNodeSelected = config.isNodeSelected;
|
|
6766
|
+
this.isLinkHovered = config.isLinkHovered;
|
|
6767
|
+
this.isLinkSelected = config.isLinkSelected;
|
|
6768
|
+
this.getLinkId = config.getLinkId;
|
|
6769
|
+
this.getLinkMidpoint = config.getLinkMidpoint;
|
|
6770
|
+
this.renderNodes = config.renderNodes;
|
|
6771
|
+
this.renderLinks = config.renderLinks;
|
|
6772
|
+
this.renderNodeLabels = config.renderNodeLabels;
|
|
6773
|
+
this.buildAdjacencyMaps();
|
|
6774
|
+
}
|
|
6775
|
+
/**
|
|
6776
|
+
* Build adjacency maps once at initialization - O(n) complexity
|
|
6777
|
+
*/
|
|
6778
|
+
buildAdjacencyMaps() {
|
|
6779
|
+
if (!this.config) return;
|
|
6780
|
+
this.nodeToLinksMap.clear();
|
|
6781
|
+
this.linkToNodesMap.clear();
|
|
6782
|
+
try {
|
|
6783
|
+
for (const link of this.config.links) {
|
|
6784
|
+
const sourceId = typeof link.source === "string" ? link.source : link.source.id;
|
|
6785
|
+
const targetId = typeof link.target === "string" ? link.target : link.target.id;
|
|
6786
|
+
const linkId = this.getLinkId(link);
|
|
6787
|
+
this.linkToNodesMap.set(linkId, [sourceId, targetId]);
|
|
6788
|
+
if (!this.nodeToLinksMap.has(sourceId)) {
|
|
6789
|
+
this.nodeToLinksMap.set(sourceId, /* @__PURE__ */ new Set());
|
|
6790
|
+
}
|
|
6791
|
+
if (!this.nodeToLinksMap.has(targetId)) {
|
|
6792
|
+
this.nodeToLinksMap.set(targetId, /* @__PURE__ */ new Set());
|
|
6793
|
+
}
|
|
6794
|
+
this.nodeToLinksMap.get(sourceId).add(linkId);
|
|
6795
|
+
this.nodeToLinksMap.get(targetId).add(linkId);
|
|
6796
|
+
}
|
|
6797
|
+
} catch (error) {
|
|
6798
|
+
ErrorHandler.logError(error);
|
|
6799
|
+
}
|
|
6800
|
+
}
|
|
6801
|
+
/**
|
|
6802
|
+
* Determine interactive entities - O(n) total complexity
|
|
6803
|
+
*/
|
|
6804
|
+
getInteractiveEntities() {
|
|
6805
|
+
if (!this.config) {
|
|
6806
|
+
return { interactiveNodes: /* @__PURE__ */ new Set(), interactiveLinks: /* @__PURE__ */ new Set() };
|
|
6807
|
+
}
|
|
6808
|
+
const interactiveNodes = /* @__PURE__ */ new Set();
|
|
6809
|
+
const interactiveLinks = /* @__PURE__ */ new Set();
|
|
6810
|
+
try {
|
|
6811
|
+
for (const node of this.config.nodes) {
|
|
6812
|
+
if (this.isNodeHovered(node.id) || this.isNodeSelected(node.id)) {
|
|
6813
|
+
interactiveNodes.add(node.id);
|
|
6814
|
+
const connectedLinks = this.nodeToLinksMap.get(node.id);
|
|
6815
|
+
if (connectedLinks) {
|
|
6816
|
+
connectedLinks.forEach((linkId) => interactiveLinks.add(linkId));
|
|
6817
|
+
}
|
|
6818
|
+
}
|
|
6819
|
+
}
|
|
6820
|
+
for (const link of this.config.links) {
|
|
6821
|
+
const linkId = this.getLinkId(link);
|
|
6822
|
+
if (this.isLinkHovered(link) || this.isLinkSelected(link)) {
|
|
6823
|
+
interactiveLinks.add(linkId);
|
|
6824
|
+
const connectedNodes = this.linkToNodesMap.get(linkId);
|
|
6825
|
+
if (connectedNodes) {
|
|
6826
|
+
interactiveNodes.add(connectedNodes[0]);
|
|
6827
|
+
interactiveNodes.add(connectedNodes[1]);
|
|
6828
|
+
}
|
|
6829
|
+
}
|
|
6830
|
+
}
|
|
6831
|
+
} catch (error) {
|
|
6832
|
+
ErrorHandler.logError(error);
|
|
6833
|
+
}
|
|
6834
|
+
return { interactiveNodes, interactiveLinks };
|
|
6835
|
+
}
|
|
6836
|
+
/**
|
|
6837
|
+
* Clear and prepare reusable arrays to avoid garbage collection
|
|
6838
|
+
*/
|
|
6839
|
+
clearReusableArrays() {
|
|
6840
|
+
this.reusableArrays.backgroundNodes.length = 0;
|
|
6841
|
+
this.reusableArrays.foregroundNodes.length = 0;
|
|
6842
|
+
this.reusableArrays.backgroundLinks.length = 0;
|
|
6843
|
+
this.reusableArrays.foregroundLinks.length = 0;
|
|
6844
|
+
this.sortedBackgroundNodes.length = 0;
|
|
6845
|
+
this.sortedForegroundNodes.length = 0;
|
|
6846
|
+
}
|
|
6847
|
+
/**
|
|
6848
|
+
* Main render with optimized layering - O(n) total complexity
|
|
6849
|
+
*/
|
|
6850
|
+
render(ctx, performanceMetrics) {
|
|
6851
|
+
if (!this.config || !this.styleResolver) {
|
|
6852
|
+
throw new RenderError("Z-Index renderer not initialized");
|
|
6853
|
+
}
|
|
6854
|
+
const startTime = performance.now();
|
|
6855
|
+
try {
|
|
6856
|
+
const { interactiveNodes, interactiveLinks } = this.getInteractiveEntities();
|
|
6857
|
+
this.clearReusableArrays();
|
|
6858
|
+
this.separateEntitiesIntoLayers(interactiveNodes, interactiveLinks);
|
|
6859
|
+
this.renderAllComponentsInCorrectOrder(ctx);
|
|
6860
|
+
if (performanceMetrics) {
|
|
6861
|
+
performanceMetrics.renderTotal += performance.now() - startTime;
|
|
6862
|
+
}
|
|
6863
|
+
} catch (error) {
|
|
6864
|
+
ErrorHandler.logError(error);
|
|
6865
|
+
throw new RenderError("Failed to render with z-index optimization");
|
|
6866
|
+
}
|
|
6867
|
+
}
|
|
6868
|
+
/**
|
|
6869
|
+
* Separate entities into background/foreground layers - O(n) - OPTIMIZED
|
|
6870
|
+
* CRITICAL: Ensure entities appear in ONLY ONE layer to prevent overlapping labels
|
|
6871
|
+
* OPTIMIZED: Combined with sorting to reduce iterations
|
|
6872
|
+
*/
|
|
6873
|
+
separateEntitiesIntoLayers(interactiveNodes, interactiveLinks) {
|
|
6874
|
+
if (!this.config) return;
|
|
6875
|
+
for (const node of this.config.nodes) {
|
|
6876
|
+
if (interactiveNodes.has(node.id)) {
|
|
6877
|
+
this.reusableArrays.foregroundNodes.push(node);
|
|
6878
|
+
} else {
|
|
6879
|
+
this.reusableArrays.backgroundNodes.push(node);
|
|
6880
|
+
}
|
|
6881
|
+
}
|
|
6882
|
+
for (const link of this.config.links) {
|
|
6883
|
+
const linkId = this.getLinkId(link);
|
|
6884
|
+
if (interactiveLinks.has(linkId)) {
|
|
6885
|
+
this.reusableArrays.foregroundLinks.push(link);
|
|
6886
|
+
} else {
|
|
6887
|
+
this.reusableArrays.backgroundLinks.push(link);
|
|
6888
|
+
}
|
|
6889
|
+
}
|
|
6890
|
+
this.sortNodeArrays();
|
|
6891
|
+
}
|
|
6892
|
+
/**
|
|
6893
|
+
* Sort node arrays once after separation - O(n log n) but only once per render
|
|
6894
|
+
*/
|
|
6895
|
+
sortNodeArrays() {
|
|
6896
|
+
this.sortedBackgroundNodes = this.sortNodesForSubLayering(this.reusableArrays.backgroundNodes);
|
|
6897
|
+
this.sortedForegroundNodes = this.sortNodesForSubLayering(this.reusableArrays.foregroundNodes);
|
|
6898
|
+
}
|
|
6899
|
+
/**
|
|
6900
|
+
* Render all components in correct z-order across all layers
|
|
6901
|
+
* PROPERLY FIXED: Atomic node+label rendering for correct sub-layering
|
|
6902
|
+
*/
|
|
6903
|
+
renderAllComponentsInCorrectOrder(ctx) {
|
|
6904
|
+
if (this.renderLinks) {
|
|
6905
|
+
this.renderLinks(ctx, this.reusableArrays.backgroundLinks);
|
|
6906
|
+
}
|
|
6907
|
+
this.renderLinkLabels(ctx, this.reusableArrays.backgroundLinks);
|
|
6908
|
+
this.renderNodesWithLabelsAtomically(ctx, this.sortedBackgroundNodes);
|
|
6909
|
+
if (this.renderLinks) {
|
|
6910
|
+
this.renderLinks(ctx, this.reusableArrays.foregroundLinks);
|
|
6911
|
+
}
|
|
6912
|
+
this.renderLinkLabels(ctx, this.reusableArrays.foregroundLinks);
|
|
6913
|
+
this.renderNodesWithLabelsAtomically(ctx, this.sortedForegroundNodes);
|
|
6914
|
+
}
|
|
6915
|
+
/**
|
|
6916
|
+
* Render nodes with atomic circle+label rendering for proper sub-layering
|
|
6917
|
+
* OPTIMIZED: Uses pre-sorted nodes and batch rendering when possible
|
|
6918
|
+
*/
|
|
6919
|
+
renderNodesWithLabelsAtomically(ctx, sortedNodes) {
|
|
6920
|
+
if (sortedNodes.length === 0) return;
|
|
6921
|
+
try {
|
|
6922
|
+
for (const node of sortedNodes) {
|
|
6923
|
+
if (this.renderNodes) {
|
|
6924
|
+
this.renderNodes(ctx, [node]);
|
|
6925
|
+
}
|
|
6926
|
+
if (this.renderNodeLabels) {
|
|
6927
|
+
this.renderNodeLabels(ctx, [node]);
|
|
6928
|
+
}
|
|
6929
|
+
}
|
|
6930
|
+
} catch (error) {
|
|
6931
|
+
ErrorHandler.logError(error, { nodeCount: sortedNodes.length });
|
|
6932
|
+
}
|
|
6933
|
+
}
|
|
6934
|
+
// Static type order for performance (avoid object creation in hot path)
|
|
6935
|
+
static TYPE_ORDER = {
|
|
6936
|
+
"Server": 1,
|
|
6937
|
+
"Database": 2,
|
|
6938
|
+
"Service": 3,
|
|
6939
|
+
"Client": 4,
|
|
6940
|
+
"Gateway": 5
|
|
6941
|
+
};
|
|
6942
|
+
/**
|
|
6943
|
+
* Sort nodes for consistent sub-layer ordering within the same layer
|
|
6944
|
+
* OPTIMIZED: Reduced object allocations and string operations
|
|
6945
|
+
*/
|
|
6946
|
+
sortNodesForSubLayering(nodes) {
|
|
6947
|
+
if (nodes.length <= 1) return [...nodes];
|
|
6948
|
+
return [...nodes].sort((a2, b) => {
|
|
6949
|
+
const aPriority = _OptimizedZIndexRenderer.TYPE_ORDER[a2.type] ?? 999;
|
|
6950
|
+
const bPriority = _OptimizedZIndexRenderer.TYPE_ORDER[b.type] ?? 999;
|
|
6951
|
+
if (aPriority !== bPriority) return aPriority - bPriority;
|
|
6952
|
+
const aRadius = a2.style?.radius ?? 20;
|
|
6953
|
+
const bRadius = b.style?.radius ?? 20;
|
|
6954
|
+
if (aRadius !== bRadius) return bRadius - aRadius;
|
|
6955
|
+
const aY = a2.y ?? 0;
|
|
6956
|
+
const bY = b.y ?? 0;
|
|
6957
|
+
if (aY !== bY) return aY - bY;
|
|
6958
|
+
const aX = a2.x ?? 0;
|
|
6959
|
+
const bX = b.x ?? 0;
|
|
6960
|
+
if (aX !== bX) return aX - bX;
|
|
6961
|
+
return a2.id < b.id ? -1 : a2.id > b.id ? 1 : 0;
|
|
6962
|
+
});
|
|
6963
|
+
}
|
|
6964
|
+
/**
|
|
6965
|
+
* Optimized link label rendering with visibility rules
|
|
6966
|
+
*/
|
|
6967
|
+
renderLinkLabels(ctx, links) {
|
|
6968
|
+
if (!this.styleResolver) return;
|
|
6969
|
+
for (const link of links) {
|
|
6970
|
+
if (!link.label) continue;
|
|
6971
|
+
try {
|
|
6972
|
+
const style = this.styleResolver.resolveLinkStyle({
|
|
6973
|
+
link,
|
|
6974
|
+
isHovered: this.isLinkHovered(link),
|
|
6975
|
+
isSelected: this.isLinkSelected(link)
|
|
6976
|
+
});
|
|
6977
|
+
if (!this.shouldShowLinkLabel(style.label || null, link)) continue;
|
|
6978
|
+
const midpoint = this.getLinkMidpoint(link);
|
|
6979
|
+
if (midpoint) {
|
|
6980
|
+
this.renderSingleLinkLabel(ctx, link.label, midpoint.x, midpoint.y, style.label);
|
|
6981
|
+
}
|
|
6982
|
+
} catch (error) {
|
|
6983
|
+
ErrorHandler.logError(error, { linkId: this.getLinkId(link) });
|
|
6984
|
+
}
|
|
6985
|
+
}
|
|
6986
|
+
}
|
|
6987
|
+
/**
|
|
6988
|
+
* Visibility logic for link labels (matches requirements)
|
|
6989
|
+
*/
|
|
6990
|
+
shouldShowLinkLabel(labelStyle, link) {
|
|
6991
|
+
if (!labelStyle?.enabled) return false;
|
|
6992
|
+
const isHovered = this.isLinkHovered(link);
|
|
6993
|
+
const isSelected = this.isLinkSelected(link);
|
|
6994
|
+
if (isSelected) return true;
|
|
6995
|
+
switch (labelStyle.visibility) {
|
|
6996
|
+
case "always":
|
|
6997
|
+
return true;
|
|
6998
|
+
case "hover":
|
|
6999
|
+
return isHovered;
|
|
7000
|
+
case "selection":
|
|
7001
|
+
return isSelected;
|
|
7002
|
+
// Already handled above
|
|
7003
|
+
default:
|
|
7004
|
+
return true;
|
|
7005
|
+
}
|
|
7006
|
+
}
|
|
7007
|
+
/**
|
|
7008
|
+
* Render a single link label at given coordinates
|
|
7009
|
+
*/
|
|
7010
|
+
renderSingleLinkLabel(ctx, text, x3, y3, style) {
|
|
7011
|
+
try {
|
|
7012
|
+
ctx.font = style.font ?? "10px Arial";
|
|
7013
|
+
const metrics = ctx.measureText(text);
|
|
7014
|
+
const textWidth = metrics.width;
|
|
7015
|
+
const textHeight = (metrics.actualBoundingBoxAscent || 10) + (metrics.actualBoundingBoxDescent || 4);
|
|
7016
|
+
const rectWidth = textWidth + (style.paddingX ?? 8) * 2;
|
|
7017
|
+
const rectHeight = textHeight + (style.paddingY ?? 4) * 2;
|
|
7018
|
+
const rectX = x3 - rectWidth / 2;
|
|
7019
|
+
const rectY = y3 - rectHeight / 2;
|
|
7020
|
+
if (style.backgroundColor && style.backgroundColor !== "transparent") {
|
|
7021
|
+
ctx.fillStyle = style.backgroundColor;
|
|
7022
|
+
this.roundRect(ctx, rectX, rectY, rectWidth, rectHeight, style.borderRadius ?? 4);
|
|
7023
|
+
ctx.fill();
|
|
7024
|
+
}
|
|
7025
|
+
if ((style.borderWidth ?? 0) > 0 && style.borderColor && style.borderColor !== "transparent") {
|
|
7026
|
+
ctx.strokeStyle = style.borderColor;
|
|
7027
|
+
ctx.lineWidth = style.borderWidth ?? 1;
|
|
7028
|
+
this.roundRect(ctx, rectX, rectY, rectWidth, rectHeight, style.borderRadius ?? 4);
|
|
7029
|
+
ctx.stroke();
|
|
7030
|
+
}
|
|
7031
|
+
ctx.fillStyle = style.textColor ?? "#000000";
|
|
7032
|
+
ctx.textAlign = "center";
|
|
7033
|
+
ctx.textBaseline = "middle";
|
|
7034
|
+
ctx.fillText(text, x3, y3);
|
|
7035
|
+
} catch (error) {
|
|
7036
|
+
ErrorHandler.logError(error);
|
|
7037
|
+
}
|
|
7038
|
+
}
|
|
7039
|
+
/**
|
|
7040
|
+
* Helper to draw rounded rectangle
|
|
7041
|
+
*/
|
|
7042
|
+
roundRect(ctx, x3, y3, width, height, radius) {
|
|
7043
|
+
if (radius === 0) {
|
|
7044
|
+
ctx.rect(x3, y3, width, height);
|
|
7045
|
+
return;
|
|
7046
|
+
}
|
|
7047
|
+
ctx.beginPath();
|
|
7048
|
+
ctx.moveTo(x3 + radius, y3);
|
|
7049
|
+
ctx.lineTo(x3 + width - radius, y3);
|
|
7050
|
+
ctx.quadraticCurveTo(x3 + width, y3, x3 + width, y3 + radius);
|
|
7051
|
+
ctx.lineTo(x3 + width, y3 + height - radius);
|
|
7052
|
+
ctx.quadraticCurveTo(x3 + width, y3 + height, x3 + width - radius, y3 + height);
|
|
7053
|
+
ctx.lineTo(x3 + radius, y3 + height);
|
|
7054
|
+
ctx.quadraticCurveTo(x3, y3 + height, x3, y3 + height - radius);
|
|
7055
|
+
ctx.lineTo(x3, y3 + radius);
|
|
7056
|
+
ctx.quadraticCurveTo(x3, y3, x3 + radius, y3);
|
|
7057
|
+
ctx.closePath();
|
|
7058
|
+
}
|
|
7059
|
+
/**
|
|
7060
|
+
* Update configuration and rebuild adjacency maps
|
|
7061
|
+
*/
|
|
7062
|
+
updateConfig(config) {
|
|
7063
|
+
this.config = config;
|
|
7064
|
+
this.buildAdjacencyMaps();
|
|
7065
|
+
this.sortedBackgroundNodes.length = 0;
|
|
7066
|
+
this.sortedForegroundNodes.length = 0;
|
|
7067
|
+
}
|
|
7068
|
+
/**
|
|
7069
|
+
* Destroy and clean up resources
|
|
7070
|
+
*/
|
|
7071
|
+
destroy() {
|
|
7072
|
+
this.config = void 0;
|
|
7073
|
+
this.styleResolver = void 0;
|
|
7074
|
+
this.nodeToLinksMap.clear();
|
|
7075
|
+
this.linkToNodesMap.clear();
|
|
7076
|
+
this.clearReusableArrays();
|
|
7077
|
+
}
|
|
7078
|
+
};
|
|
7079
|
+
|
|
7080
|
+
// src/v2/rendering/zoom-renderer.ts
|
|
7081
|
+
var ZoomRenderer = class {
|
|
7082
|
+
config;
|
|
7083
|
+
stateManager;
|
|
7084
|
+
isZooming = false;
|
|
7085
|
+
// Fast zoom cache for O(1) lookups - everything pre-computed
|
|
7086
|
+
fastZoomNodeCache = /* @__PURE__ */ new Map();
|
|
7087
|
+
/**
|
|
7088
|
+
* Initialize zoom renderer
|
|
7089
|
+
*/
|
|
7090
|
+
initialize(config) {
|
|
7091
|
+
this.config = config;
|
|
7092
|
+
this.stateManager = config.stateManager;
|
|
7093
|
+
this.buildFastZoomCache();
|
|
7094
|
+
}
|
|
7095
|
+
/**
|
|
7096
|
+
* Render with zoom optimization
|
|
7097
|
+
*/
|
|
7098
|
+
render(ctx, zIndexRenderer) {
|
|
7099
|
+
if (!this.config) return;
|
|
7100
|
+
if (this.isZooming) {
|
|
7101
|
+
this.renderFastZoom(ctx);
|
|
7102
|
+
} else {
|
|
7103
|
+
zIndexRenderer.render(ctx);
|
|
7104
|
+
}
|
|
7105
|
+
}
|
|
7106
|
+
/**
|
|
7107
|
+
* Fast rendering during zoom (O(1) cached properties)
|
|
7108
|
+
*/
|
|
7109
|
+
renderFastZoom(ctx) {
|
|
7110
|
+
if (!this.config) return;
|
|
7111
|
+
const { nodes, links } = this.config;
|
|
7112
|
+
ctx.strokeStyle = "#999";
|
|
7113
|
+
ctx.lineWidth = 1;
|
|
7114
|
+
for (const link of links) {
|
|
7115
|
+
const sourceNode = typeof link.source === "string" ? this.stateManager.getNode(link.source) : link.source;
|
|
7116
|
+
const targetNode = typeof link.target === "string" ? this.stateManager.getNode(link.target) : link.target;
|
|
7117
|
+
if (sourceNode && targetNode && sourceNode.x && sourceNode.y && targetNode.x && targetNode.y) {
|
|
7118
|
+
ctx.beginPath();
|
|
7119
|
+
ctx.moveTo(sourceNode.x, sourceNode.y);
|
|
7120
|
+
ctx.lineTo(targetNode.x, targetNode.y);
|
|
7121
|
+
ctx.stroke();
|
|
7122
|
+
}
|
|
7123
|
+
}
|
|
7124
|
+
for (const node of nodes) {
|
|
7125
|
+
if (!node.x || !node.y) continue;
|
|
7126
|
+
const cachedProps = this.fastZoomNodeCache.get(node.id);
|
|
7127
|
+
if (!cachedProps) continue;
|
|
7128
|
+
ctx.beginPath();
|
|
7129
|
+
ctx.arc(node.x, node.y, cachedProps.radius, 0, 2 * Math.PI);
|
|
7130
|
+
ctx.fillStyle = cachedProps.fill;
|
|
7131
|
+
ctx.fill();
|
|
7132
|
+
}
|
|
7133
|
+
this.renderFastZoomLabels(ctx);
|
|
7134
|
+
}
|
|
7135
|
+
/**
|
|
7136
|
+
* Render pre-computed node labels during zoom (true O(1) lookups)
|
|
7137
|
+
*/
|
|
7138
|
+
renderFastZoomLabels(ctx) {
|
|
7139
|
+
if (!this.config) return;
|
|
7140
|
+
const { nodes } = this.config;
|
|
7141
|
+
ctx.font = "9px Arial";
|
|
7142
|
+
ctx.textAlign = "center";
|
|
7143
|
+
ctx.textBaseline = "middle";
|
|
7144
|
+
for (const node of nodes) {
|
|
7145
|
+
if (!node.x || !node.y) continue;
|
|
7146
|
+
const cachedProps = this.fastZoomNodeCache.get(node.id);
|
|
7147
|
+
if (!cachedProps?.truncatedLabel) continue;
|
|
7148
|
+
ctx.fillStyle = cachedProps.textColor;
|
|
7149
|
+
ctx.fillText(cachedProps.truncatedLabel, node.x, node.y);
|
|
7150
|
+
}
|
|
7151
|
+
}
|
|
7152
|
+
/**
|
|
7153
|
+
* Build fast zoom cache with pre-computed properties for true O(1) performance
|
|
7154
|
+
*/
|
|
7155
|
+
buildFastZoomCache() {
|
|
7156
|
+
if (!this.config) return;
|
|
7157
|
+
try {
|
|
7158
|
+
this.fastZoomNodeCache.clear();
|
|
7159
|
+
const tempCanvas = document.createElement("canvas");
|
|
7160
|
+
const tempCtx = tempCanvas.getContext("2d");
|
|
7161
|
+
if (!tempCtx) return;
|
|
7162
|
+
tempCtx.font = "9px Arial";
|
|
7163
|
+
for (const node of this.config.nodes) {
|
|
7164
|
+
const nodeStyle = this.config.styleResolver.resolveNodeStyle({
|
|
7165
|
+
node,
|
|
7166
|
+
isHovered: false,
|
|
7167
|
+
isSelected: false
|
|
7168
|
+
});
|
|
7169
|
+
const label = node.label || node.id;
|
|
7170
|
+
const maxWidth = nodeStyle.radius * 2 - 6;
|
|
7171
|
+
const truncatedLabel = this.preComputeTruncatedLabel(tempCtx, label, maxWidth);
|
|
7172
|
+
const textColor = nodeStyle.label?.textColor || "#ffffff";
|
|
7173
|
+
this.fastZoomNodeCache.set(node.id, {
|
|
7174
|
+
radius: nodeStyle.radius,
|
|
7175
|
+
fill: nodeStyle.fill,
|
|
7176
|
+
truncatedLabel,
|
|
7177
|
+
textColor
|
|
7178
|
+
});
|
|
7179
|
+
}
|
|
7180
|
+
} catch (error) {
|
|
7181
|
+
ErrorHandler.logError(error);
|
|
7182
|
+
}
|
|
7183
|
+
}
|
|
7184
|
+
/**
|
|
7185
|
+
* Pre-compute truncated label (called only during cache build)
|
|
7186
|
+
*/
|
|
7187
|
+
preComputeTruncatedLabel(ctx, label, maxWidth) {
|
|
7188
|
+
if (ctx.measureText(label).width <= maxWidth) {
|
|
7189
|
+
return label;
|
|
7190
|
+
}
|
|
7191
|
+
let truncated = label;
|
|
7192
|
+
while (truncated.length > 1 && ctx.measureText(`${truncated}\u2026`).width > maxWidth) {
|
|
7193
|
+
truncated = truncated.slice(0, -1);
|
|
7194
|
+
}
|
|
7195
|
+
return truncated.length < label.length ? `${truncated}\u2026` : truncated;
|
|
7196
|
+
}
|
|
7197
|
+
/**
|
|
7198
|
+
* Set zoom state for performance optimization
|
|
7199
|
+
*/
|
|
7200
|
+
setZoomState(isZooming) {
|
|
7201
|
+
this.isZooming = isZooming;
|
|
7202
|
+
}
|
|
7203
|
+
/**
|
|
7204
|
+
* Get current zoom state
|
|
7205
|
+
*/
|
|
7206
|
+
getZoomState() {
|
|
7207
|
+
return this.isZooming;
|
|
7208
|
+
}
|
|
7209
|
+
/**
|
|
7210
|
+
* Update configuration and rebuild cache
|
|
7211
|
+
*/
|
|
7212
|
+
updateConfig(updates) {
|
|
7213
|
+
if (this.config) {
|
|
7214
|
+
Object.assign(this.config, updates);
|
|
7215
|
+
this.buildFastZoomCache();
|
|
7216
|
+
}
|
|
7217
|
+
}
|
|
7218
|
+
/**
|
|
7219
|
+
* Get performance stats
|
|
7220
|
+
*/
|
|
7221
|
+
getStats() {
|
|
7222
|
+
return {
|
|
7223
|
+
cachedNodeProperties: this.fastZoomNodeCache.size,
|
|
7224
|
+
isZooming: this.isZooming
|
|
7225
|
+
};
|
|
7226
|
+
}
|
|
7227
|
+
/**
|
|
7228
|
+
* Destroy and clean up
|
|
7229
|
+
*/
|
|
7230
|
+
destroy() {
|
|
7231
|
+
this.config = void 0;
|
|
7232
|
+
this.stateManager = void 0;
|
|
7233
|
+
this.fastZoomNodeCache.clear();
|
|
7234
|
+
this.isZooming = false;
|
|
7235
|
+
}
|
|
7236
|
+
};
|
|
7237
|
+
|
|
6346
7238
|
// src/v2/rendering/link-labels-renderer.ts
|
|
6347
7239
|
var LinkLabelsRenderer = class {
|
|
6348
7240
|
static textMetricsCache = /* @__PURE__ */ new Map();
|
|
@@ -6549,107 +7441,30 @@ var LinkLabelsRenderer = class {
|
|
|
6549
7441
|
}
|
|
6550
7442
|
};
|
|
6551
7443
|
|
|
6552
|
-
// src/v2/rendering/renderer.ts
|
|
6553
|
-
var
|
|
7444
|
+
// src/v2/rendering/hit-detection-renderer.ts
|
|
7445
|
+
var HitDetectionRenderer = class {
|
|
6554
7446
|
config;
|
|
6555
|
-
|
|
6556
|
-
|
|
6557
|
-
selectionManager;
|
|
6558
|
-
styleResolver;
|
|
6559
|
-
// Performance optimization: O(1) node lookups
|
|
6560
|
-
nodeMap = /* @__PURE__ */ new Map();
|
|
6561
|
-
// Shadow canvas optimization (Step 6 optimization)
|
|
7447
|
+
stateManager;
|
|
7448
|
+
// Shadow canvas optimization (throttling)
|
|
6562
7449
|
shadowCanvasDirty = true;
|
|
6563
7450
|
lastShadowRenderTime = 0;
|
|
6564
7451
|
SHADOW_RENDER_THROTTLE = 32;
|
|
6565
7452
|
// ~30 FPS max for shadow canvas
|
|
6566
|
-
// Large graph optimization flag
|
|
6567
|
-
hasLoggedLargeGraphOptimization = false;
|
|
6568
|
-
// Performance metrics
|
|
6569
|
-
performanceMetrics = {
|
|
6570
|
-
renderTotal: 0,
|
|
6571
|
-
renderNodes: 0,
|
|
6572
|
-
renderLinks: 0,
|
|
6573
|
-
renderLinkLabels: 0,
|
|
6574
|
-
renderNodeLabels: 0,
|
|
6575
|
-
styleResolution: 0,
|
|
6576
|
-
hoverChecks: 0,
|
|
6577
|
-
canvasCalls: 0,
|
|
6578
|
-
frameCount: 0
|
|
6579
|
-
};
|
|
6580
|
-
// Force-graph pattern: configurable link hover precision
|
|
6581
|
-
linkHoverPrecision = 4;
|
|
6582
|
-
// Default 4px like force-graph
|
|
6583
7453
|
/**
|
|
6584
|
-
* Initialize
|
|
7454
|
+
* Initialize hit detection renderer
|
|
6585
7455
|
*/
|
|
6586
|
-
initialize(config
|
|
6587
|
-
|
|
6588
|
-
|
|
6589
|
-
this.canvasState = canvasState;
|
|
6590
|
-
this.hoverManager = hoverManager;
|
|
6591
|
-
this.selectionManager = selectionManager;
|
|
6592
|
-
this.styleResolver = createStyleResolver(config.interaction);
|
|
6593
|
-
this.buildNodeIndex();
|
|
6594
|
-
} catch (error) {
|
|
6595
|
-
ErrorHandler.logError(error, {
|
|
6596
|
-
nodeCount: config.nodes.length,
|
|
6597
|
-
linkCount: config.links.length
|
|
6598
|
-
});
|
|
6599
|
-
throw error;
|
|
6600
|
-
}
|
|
6601
|
-
}
|
|
6602
|
-
/**
|
|
6603
|
-
* Main render method with performance metrics (Instrumented)
|
|
6604
|
-
*/
|
|
6605
|
-
render() {
|
|
6606
|
-
if (!this.config || !this.canvasState) {
|
|
6607
|
-
throw new RenderError("Renderer not initialized");
|
|
6608
|
-
}
|
|
6609
|
-
const startTime = performance.now();
|
|
6610
|
-
this.performanceMetrics.frameCount++;
|
|
6611
|
-
try {
|
|
6612
|
-
const { ctx } = this.canvasState;
|
|
6613
|
-
this.clearCanvas(ctx);
|
|
6614
|
-
this.renderWithLayersAndMetrics(ctx);
|
|
6615
|
-
this.markShadowCanvasDirty();
|
|
6616
|
-
this.performanceMetrics.renderTotal += performance.now() - startTime;
|
|
6617
|
-
if (this.performanceMetrics.frameCount % 100 === 0) {
|
|
6618
|
-
this.logPerformanceMetrics();
|
|
6619
|
-
}
|
|
6620
|
-
} catch (error) {
|
|
6621
|
-
ErrorHandler.logError(error);
|
|
6622
|
-
throw new RenderError("Failed to render graph", {
|
|
6623
|
-
nodeCount: this.config.nodes.length,
|
|
6624
|
-
linkCount: this.config.links.length,
|
|
6625
|
-
originalError: error.message
|
|
6626
|
-
});
|
|
6627
|
-
}
|
|
6628
|
-
}
|
|
6629
|
-
/**
|
|
6630
|
-
* Render with transform (called during zoom/pan) with shadow canvas dirty marking (Step 6 optimization)
|
|
6631
|
-
*/
|
|
6632
|
-
renderWithTransform() {
|
|
6633
|
-
if (!this.canvasState) return;
|
|
6634
|
-
try {
|
|
6635
|
-
const { canvas, ctx } = this.canvasState;
|
|
6636
|
-
const transform2 = transform(canvas);
|
|
6637
|
-
this.clearCanvas(ctx);
|
|
6638
|
-
this.applyTransform(transform2, ctx);
|
|
6639
|
-
this.renderWithLayers(ctx);
|
|
6640
|
-
this.markShadowCanvasDirty();
|
|
6641
|
-
} catch (error) {
|
|
6642
|
-
ErrorHandler.logError(error);
|
|
6643
|
-
}
|
|
7456
|
+
initialize(config) {
|
|
7457
|
+
this.config = config;
|
|
7458
|
+
this.stateManager = config.stateManager;
|
|
6644
7459
|
}
|
|
6645
7460
|
/**
|
|
6646
|
-
* Render shadow canvas for hit detection with throttling
|
|
7461
|
+
* Render shadow canvas for hit detection with throttling
|
|
6647
7462
|
*/
|
|
6648
7463
|
renderShadowCanvas() {
|
|
6649
|
-
if (!this.
|
|
7464
|
+
if (!this.config) return;
|
|
6650
7465
|
const now2 = Date.now();
|
|
6651
7466
|
try {
|
|
6652
|
-
const { shadowCtx, canvas } = this.
|
|
7467
|
+
const { shadowCtx, canvas } = this.config;
|
|
6653
7468
|
const transform2 = transform(canvas);
|
|
6654
7469
|
this.clearCanvas(shadowCtx);
|
|
6655
7470
|
this.applyTransform(transform2, shadowCtx);
|
|
@@ -6663,13 +7478,13 @@ var Renderer = class {
|
|
|
6663
7478
|
}
|
|
6664
7479
|
}
|
|
6665
7480
|
/**
|
|
6666
|
-
* Mark shadow canvas as dirty for next render
|
|
7481
|
+
* Mark shadow canvas as dirty for next render
|
|
6667
7482
|
*/
|
|
6668
7483
|
markShadowCanvasDirty() {
|
|
6669
7484
|
this.shadowCanvasDirty = true;
|
|
6670
7485
|
}
|
|
6671
7486
|
/**
|
|
6672
|
-
* Force shadow canvas render
|
|
7487
|
+
* Force shadow canvas render
|
|
6673
7488
|
*/
|
|
6674
7489
|
forceShadowCanvasRender() {
|
|
6675
7490
|
this.shadowCanvasDirty = true;
|
|
@@ -6677,96 +7492,53 @@ var Renderer = class {
|
|
|
6677
7492
|
this.renderShadowCanvas();
|
|
6678
7493
|
}
|
|
6679
7494
|
/**
|
|
6680
|
-
* Clear canvas context
|
|
7495
|
+
* Clear shadow canvas context
|
|
6681
7496
|
*/
|
|
6682
7497
|
clearCanvas(ctx) {
|
|
6683
|
-
if (!this.
|
|
7498
|
+
if (!this.config) return;
|
|
6684
7499
|
try {
|
|
6685
|
-
const {
|
|
7500
|
+
const { canvas } = this.config;
|
|
7501
|
+
const { width, height } = canvas;
|
|
6686
7502
|
CanvasUtils.resetTransform(ctx);
|
|
6687
7503
|
ctx.clearRect(0, 0, width, height);
|
|
6688
|
-
} catch {
|
|
6689
|
-
|
|
7504
|
+
} catch (error) {
|
|
7505
|
+
ErrorHandler.logError(error, { message: "Failed to clear shadow canvas" });
|
|
6690
7506
|
}
|
|
6691
7507
|
}
|
|
6692
7508
|
/**
|
|
6693
|
-
* Apply transform to canvas context
|
|
7509
|
+
* Apply transform to shadow canvas context
|
|
6694
7510
|
*/
|
|
6695
7511
|
applyTransform(transform2, ctx) {
|
|
6696
7512
|
try {
|
|
6697
7513
|
CanvasUtils.resetTransform(ctx);
|
|
6698
7514
|
ctx.translate(transform2.x, transform2.y);
|
|
6699
7515
|
ctx.scale(transform2.k, transform2.k);
|
|
6700
|
-
} catch {
|
|
6701
|
-
throw new RenderError("Failed to apply transform");
|
|
6702
|
-
}
|
|
6703
|
-
}
|
|
6704
|
-
/**
|
|
6705
|
-
* Get unique link ID for tracking (consistent with LinkLabelsRenderer)
|
|
6706
|
-
*/
|
|
6707
|
-
getLinkId(link) {
|
|
6708
|
-
const sourceId = typeof link.source === "string" ? link.source : link.source.id;
|
|
6709
|
-
const targetId = typeof link.target === "string" ? link.target : link.target.id;
|
|
6710
|
-
return `${sourceId}->${targetId}`;
|
|
6711
|
-
}
|
|
6712
|
-
/**
|
|
6713
|
-
* Render main canvas nodes
|
|
6714
|
-
*/
|
|
6715
|
-
renderNodes(ctx) {
|
|
6716
|
-
if (!this.config || !this.styleResolver) return;
|
|
6717
|
-
try {
|
|
6718
|
-
const { nodes } = this.config;
|
|
6719
|
-
NodesRenderer.renderWithStyleResolver(
|
|
6720
|
-
ctx,
|
|
6721
|
-
nodes,
|
|
6722
|
-
this.styleResolver,
|
|
6723
|
-
(nodeId) => this.isNodeHovered(nodeId),
|
|
6724
|
-
(nodeId) => this.isNodeSelected(nodeId),
|
|
6725
|
-
this.performanceMetrics
|
|
6726
|
-
);
|
|
6727
|
-
} catch (error) {
|
|
6728
|
-
ErrorHandler.logError(error);
|
|
6729
|
-
throw new RenderError("Failed to render nodes");
|
|
6730
|
-
}
|
|
6731
|
-
}
|
|
6732
|
-
/**
|
|
6733
|
-
* Render node labels
|
|
6734
|
-
*/
|
|
6735
|
-
renderNodeLabels(ctx) {
|
|
6736
|
-
if (!this.config || !this.styleResolver) return;
|
|
6737
|
-
try {
|
|
6738
|
-
const { nodes } = this.config;
|
|
6739
|
-
const defaultNodeStyle = this.styleResolver.resolveNodeStyle({
|
|
6740
|
-
node: { id: "temp" }
|
|
6741
|
-
});
|
|
6742
|
-
NodeLabelsRenderer.render(ctx, nodes, defaultNodeStyle.radius);
|
|
6743
7516
|
} catch (error) {
|
|
6744
|
-
ErrorHandler.logError(error);
|
|
6745
|
-
throw new RenderError("Failed to render node labels");
|
|
7517
|
+
ErrorHandler.logError(error, { message: "Failed to apply transform to shadow canvas" });
|
|
6746
7518
|
}
|
|
6747
7519
|
}
|
|
6748
7520
|
/**
|
|
6749
|
-
* Render shadow links with __indexColor
|
|
7521
|
+
* Render shadow links with __indexColor for hit detection
|
|
6750
7522
|
*/
|
|
6751
7523
|
renderShadowLinks(shadowCtx) {
|
|
6752
|
-
if (!this.config
|
|
7524
|
+
if (!this.config) return;
|
|
6753
7525
|
try {
|
|
6754
|
-
const { links } = this.config;
|
|
6755
|
-
const defaultLinkStyle =
|
|
7526
|
+
const { links, stateManager, styleResolver, linkHoverPrecision = 4 } = this.config;
|
|
7527
|
+
const defaultLinkStyle = styleResolver.resolveLinkStyle({
|
|
6756
7528
|
link: { source: "", target: "" }
|
|
6757
7529
|
});
|
|
6758
7530
|
for (const link of links) {
|
|
6759
7531
|
if (!link.__indexColor) continue;
|
|
6760
|
-
const sourceNode = typeof link.source === "string" ?
|
|
6761
|
-
const targetNode = typeof link.target === "string" ?
|
|
6762
|
-
if (sourceNode && targetNode && sourceNode.x && sourceNode.y && targetNode.x && targetNode.y && link.__indexColorRGB) {
|
|
7532
|
+
const sourceNode = typeof link.source === "string" ? stateManager.getNode(link.source) : link.source;
|
|
7533
|
+
const targetNode = typeof link.target === "string" ? stateManager.getNode(link.target) : link.target;
|
|
7534
|
+
if (sourceNode && targetNode && sourceNode.x != null && sourceNode.y != null && targetNode.x != null && targetNode.y != null && link.__indexColorRGB) {
|
|
6763
7535
|
const [r, g, b] = link.__indexColorRGB;
|
|
6764
7536
|
const rgbColor = `rgb(${r},${g},${b})`;
|
|
6765
7537
|
const dx = targetNode.x - sourceNode.x;
|
|
6766
7538
|
const dy = targetNode.y - sourceNode.y;
|
|
6767
7539
|
const length = Math.sqrt(dx * dx + dy * dy);
|
|
6768
7540
|
const angle = Math.atan2(dy, dx);
|
|
6769
|
-
const thickness = defaultLinkStyle.strokeWidth +
|
|
7541
|
+
const thickness = defaultLinkStyle.strokeWidth + linkHoverPrecision;
|
|
6770
7542
|
shadowCtx.save();
|
|
6771
7543
|
shadowCtx.translate(sourceNode.x, sourceNode.y);
|
|
6772
7544
|
shadowCtx.rotate(angle);
|
|
@@ -6783,27 +7555,30 @@ var Renderer = class {
|
|
|
6783
7555
|
* Render shadow link labels for hit detection
|
|
6784
7556
|
*/
|
|
6785
7557
|
renderShadowLinkLabels(shadowCtx) {
|
|
6786
|
-
if (!this.config
|
|
7558
|
+
if (!this.config) return;
|
|
6787
7559
|
try {
|
|
7560
|
+
const { links, styleResolver } = this.config;
|
|
6788
7561
|
const linkStateCache = /* @__PURE__ */ new Map();
|
|
6789
7562
|
const linkIdToLinkMap = /* @__PURE__ */ new Map();
|
|
6790
|
-
for (const link of
|
|
7563
|
+
for (const link of links) {
|
|
6791
7564
|
const linkId = this.getLinkId(link);
|
|
6792
7565
|
linkIdToLinkMap.set(linkId, link);
|
|
6793
7566
|
}
|
|
6794
7567
|
const labelPositions = LinkLabelsRenderer.calculateLabelPositions(
|
|
6795
|
-
|
|
7568
|
+
links,
|
|
6796
7569
|
(link) => {
|
|
6797
7570
|
const linkId = this.getLinkId(link);
|
|
6798
7571
|
let linkState = linkStateCache.get(linkId);
|
|
6799
7572
|
if (!linkState) {
|
|
6800
7573
|
linkState = {
|
|
6801
|
-
isHovered:
|
|
6802
|
-
|
|
7574
|
+
isHovered: false,
|
|
7575
|
+
// For hit detection, we don't need actual hover state
|
|
7576
|
+
isSelected: false
|
|
7577
|
+
// For hit detection, we don't need actual selection state
|
|
6803
7578
|
};
|
|
6804
7579
|
linkStateCache.set(linkId, linkState);
|
|
6805
7580
|
}
|
|
6806
|
-
const style =
|
|
7581
|
+
const style = styleResolver.resolveLinkStyle({
|
|
6807
7582
|
link,
|
|
6808
7583
|
isHovered: linkState.isHovered,
|
|
6809
7584
|
isSelected: linkState.isSelected
|
|
@@ -6811,34 +7586,10 @@ var Renderer = class {
|
|
|
6811
7586
|
return style.label || null;
|
|
6812
7587
|
},
|
|
6813
7588
|
(link) => this.getLinkMidpoint(link),
|
|
6814
|
-
(
|
|
6815
|
-
|
|
6816
|
-
|
|
6817
|
-
|
|
6818
|
-
if (link) {
|
|
6819
|
-
linkState = {
|
|
6820
|
-
isHovered: this.isLinkHovered(link),
|
|
6821
|
-
isSelected: this.isLinkSelected(link)
|
|
6822
|
-
};
|
|
6823
|
-
linkStateCache.set(linkId, linkState);
|
|
6824
|
-
}
|
|
6825
|
-
}
|
|
6826
|
-
return linkState?.isHovered || false;
|
|
6827
|
-
},
|
|
6828
|
-
(linkId) => {
|
|
6829
|
-
let linkState = linkStateCache.get(linkId);
|
|
6830
|
-
if (!linkState) {
|
|
6831
|
-
const link = linkIdToLinkMap.get(linkId);
|
|
6832
|
-
if (link) {
|
|
6833
|
-
linkState = {
|
|
6834
|
-
isHovered: this.isLinkHovered(link),
|
|
6835
|
-
isSelected: this.isLinkSelected(link)
|
|
6836
|
-
};
|
|
6837
|
-
linkStateCache.set(linkId, linkState);
|
|
6838
|
-
}
|
|
6839
|
-
}
|
|
6840
|
-
return linkState?.isSelected || false;
|
|
6841
|
-
}
|
|
7589
|
+
(_linkId) => false,
|
|
7590
|
+
// isHovered - not needed for hit detection
|
|
7591
|
+
(_linkId) => false
|
|
7592
|
+
// isSelected - not needed for hit detection
|
|
6842
7593
|
);
|
|
6843
7594
|
for (const [linkId, position] of labelPositions) {
|
|
6844
7595
|
const link = linkIdToLinkMap.get(linkId);
|
|
@@ -6854,13 +7605,13 @@ var Renderer = class {
|
|
|
6854
7605
|
}
|
|
6855
7606
|
}
|
|
6856
7607
|
/**
|
|
6857
|
-
* Render shadow nodes with __indexColor
|
|
7608
|
+
* Render shadow nodes with __indexColor for hit detection
|
|
6858
7609
|
*/
|
|
6859
7610
|
renderShadowNodes(shadowCtx) {
|
|
6860
|
-
if (!this.config
|
|
7611
|
+
if (!this.config) return;
|
|
6861
7612
|
try {
|
|
6862
|
-
const { nodes } = this.config;
|
|
6863
|
-
const defaultNodeStyle =
|
|
7613
|
+
const { nodes, styleResolver } = this.config;
|
|
7614
|
+
const defaultNodeStyle = styleResolver.resolveNodeStyle({
|
|
6864
7615
|
node: { id: "temp" }
|
|
6865
7616
|
});
|
|
6866
7617
|
NodesRenderer.renderShadow(
|
|
@@ -6873,296 +7624,1167 @@ var Renderer = class {
|
|
|
6873
7624
|
}
|
|
6874
7625
|
}
|
|
6875
7626
|
/**
|
|
6876
|
-
* Get
|
|
7627
|
+
* Get unique link ID for tracking (delegates to StateManager)
|
|
6877
7628
|
*/
|
|
6878
|
-
|
|
6879
|
-
|
|
6880
|
-
const hoverState = this.hoverManager.getHoverState();
|
|
6881
|
-
if (!hoverState.currentHovered || hoverState.currentHovered.d.entityType !== "Node") {
|
|
6882
|
-
return null;
|
|
6883
|
-
}
|
|
6884
|
-
const hoveredNode = hoverState.currentHovered.d;
|
|
6885
|
-
return hoveredNode ? hoveredNode.id : null;
|
|
7629
|
+
getLinkId(link) {
|
|
7630
|
+
return this.stateManager.getLinkId(link);
|
|
6886
7631
|
}
|
|
6887
7632
|
/**
|
|
6888
|
-
*
|
|
7633
|
+
* Calculate midpoint of a link for label positioning (delegates to StateManager)
|
|
6889
7634
|
*/
|
|
6890
|
-
|
|
6891
|
-
|
|
6892
|
-
const selectionState = this.selectionManager.getSelectionState();
|
|
6893
|
-
return selectionState.selectedNode?.id || null;
|
|
7635
|
+
getLinkMidpoint(link) {
|
|
7636
|
+
return this.stateManager.getLinkMidpoint(link);
|
|
6894
7637
|
}
|
|
6895
7638
|
/**
|
|
6896
|
-
*
|
|
7639
|
+
* Update configuration
|
|
6897
7640
|
*/
|
|
6898
|
-
|
|
6899
|
-
if (
|
|
6900
|
-
|
|
6901
|
-
if (!hoverState.currentHovered || hoverState.currentHovered.d.entityType !== "Link") {
|
|
6902
|
-
return null;
|
|
7641
|
+
updateConfig(updates) {
|
|
7642
|
+
if (this.config) {
|
|
7643
|
+
Object.assign(this.config, updates);
|
|
6903
7644
|
}
|
|
6904
|
-
return hoverState.currentHovered.d;
|
|
6905
7645
|
}
|
|
6906
7646
|
/**
|
|
6907
|
-
*
|
|
7647
|
+
* Debug shadow canvas export
|
|
6908
7648
|
*/
|
|
6909
|
-
|
|
6910
|
-
|
|
6911
|
-
|
|
6912
|
-
|
|
7649
|
+
debugShadowCanvas() {
|
|
7650
|
+
try {
|
|
7651
|
+
if (!this.config) return;
|
|
7652
|
+
const { shadowCtx } = this.config;
|
|
7653
|
+
const shadowCanvas = shadowCtx.canvas;
|
|
7654
|
+
const link = document.createElement("a");
|
|
7655
|
+
link.download = "shadow-canvas-debug.png";
|
|
7656
|
+
link.href = shadowCanvas.toDataURL("image/png");
|
|
7657
|
+
link.click();
|
|
7658
|
+
} catch (error) {
|
|
7659
|
+
ErrorHandler.logError(error);
|
|
7660
|
+
}
|
|
6913
7661
|
}
|
|
6914
7662
|
/**
|
|
6915
|
-
*
|
|
7663
|
+
* Get hit detection stats
|
|
6916
7664
|
*/
|
|
6917
|
-
|
|
6918
|
-
|
|
6919
|
-
|
|
6920
|
-
|
|
6921
|
-
|
|
6922
|
-
|
|
6923
|
-
console.log("\u23F1\uFE0F Total render:", (this.performanceMetrics.renderTotal / frames).toFixed(2), "ms");
|
|
6924
|
-
console.log("\u{1F517} Links render:", (this.performanceMetrics.renderLinks / frames).toFixed(2), "ms");
|
|
6925
|
-
console.log("\u{1F3F7}\uFE0F Link labels:", (this.performanceMetrics.renderLinkLabels / frames).toFixed(2), "ms");
|
|
6926
|
-
console.log("\u2B55 Nodes render:", (this.performanceMetrics.renderNodes / frames).toFixed(2), "ms");
|
|
6927
|
-
console.log("\u{1F4DD} Node labels:", (this.performanceMetrics.renderNodeLabels / frames).toFixed(2), "ms");
|
|
6928
|
-
console.log("\u{1F3A8} Style resolution:", (this.performanceMetrics.styleResolution / frames).toFixed(2), "ms");
|
|
6929
|
-
console.log("\u{1F446} Hover checks:", (this.performanceMetrics.hoverChecks / frames).toFixed(2), "ms");
|
|
6930
|
-
console.log("\u{1F5BC}\uFE0F Canvas calls:", (this.performanceMetrics.canvasCalls / frames).toFixed(2), "ms");
|
|
6931
|
-
console.log("---");
|
|
7665
|
+
getStats() {
|
|
7666
|
+
return {
|
|
7667
|
+
shadowCanvasDirty: this.shadowCanvasDirty,
|
|
7668
|
+
throttleRate: this.SHADOW_RENDER_THROTTLE,
|
|
7669
|
+
lastRenderTime: this.lastShadowRenderTime
|
|
7670
|
+
};
|
|
6932
7671
|
}
|
|
6933
7672
|
/**
|
|
6934
|
-
*
|
|
7673
|
+
* Destroy and clean up
|
|
6935
7674
|
*/
|
|
6936
|
-
|
|
6937
|
-
this.
|
|
6938
|
-
|
|
6939
|
-
|
|
6940
|
-
|
|
6941
|
-
renderLinkLabels: 0,
|
|
6942
|
-
renderNodeLabels: 0,
|
|
6943
|
-
styleResolution: 0,
|
|
6944
|
-
hoverChecks: 0,
|
|
6945
|
-
canvasCalls: 0,
|
|
6946
|
-
frameCount: 0
|
|
6947
|
-
};
|
|
7675
|
+
destroy() {
|
|
7676
|
+
this.config = void 0;
|
|
7677
|
+
this.stateManager = void 0;
|
|
7678
|
+
this.shadowCanvasDirty = false;
|
|
7679
|
+
this.lastShadowRenderTime = 0;
|
|
6948
7680
|
}
|
|
7681
|
+
};
|
|
7682
|
+
|
|
7683
|
+
// src/v2/rendering/performance-metrics-manager.ts
|
|
7684
|
+
var PerformanceMetricsManager = class {
|
|
7685
|
+
metrics = {
|
|
7686
|
+
renderTotal: 0,
|
|
7687
|
+
renderNodes: 0,
|
|
7688
|
+
renderLinks: 0,
|
|
7689
|
+
renderLinkLabels: 0,
|
|
7690
|
+
renderNodeLabels: 0,
|
|
7691
|
+
styleResolution: 0,
|
|
7692
|
+
hoverChecks: 0,
|
|
7693
|
+
canvasCalls: 0,
|
|
7694
|
+
frameCount: 0
|
|
7695
|
+
};
|
|
6949
7696
|
/**
|
|
6950
|
-
*
|
|
7697
|
+
* Increment frame count
|
|
6951
7698
|
*/
|
|
6952
|
-
|
|
6953
|
-
|
|
7699
|
+
incrementFrame() {
|
|
7700
|
+
this.metrics.frameCount++;
|
|
6954
7701
|
}
|
|
6955
7702
|
/**
|
|
6956
|
-
*
|
|
7703
|
+
* Add timing for a specific metric
|
|
6957
7704
|
*/
|
|
6958
|
-
|
|
6959
|
-
this.
|
|
7705
|
+
addTiming(metric, time) {
|
|
7706
|
+
this.metrics[metric] += time;
|
|
6960
7707
|
}
|
|
6961
7708
|
/**
|
|
6962
|
-
*
|
|
7709
|
+
* Get current metrics (copy to prevent mutation)
|
|
6963
7710
|
*/
|
|
6964
|
-
|
|
6965
|
-
|
|
6966
|
-
const hoverState = this.hoverManager.getHoverState();
|
|
6967
|
-
if (!hoverState.currentHovered || hoverState.currentHovered.d.entityType !== "Node") {
|
|
6968
|
-
return false;
|
|
6969
|
-
}
|
|
6970
|
-
const hoveredNode = hoverState.currentHovered.d;
|
|
6971
|
-
return hoveredNode && hoveredNode.id === nodeId;
|
|
7711
|
+
getMetrics() {
|
|
7712
|
+
return { ...this.metrics };
|
|
6972
7713
|
}
|
|
6973
7714
|
/**
|
|
6974
|
-
*
|
|
7715
|
+
* Reset all metrics
|
|
6975
7716
|
*/
|
|
6976
|
-
|
|
6977
|
-
|
|
6978
|
-
|
|
6979
|
-
|
|
6980
|
-
|
|
6981
|
-
|
|
6982
|
-
|
|
6983
|
-
|
|
6984
|
-
|
|
6985
|
-
|
|
6986
|
-
|
|
6987
|
-
|
|
6988
|
-
}
|
|
6989
|
-
if (hoverState.currentHovered.d.entityType === "Node") {
|
|
6990
|
-
const hoveredNode = hoverState.currentHovered.d;
|
|
6991
|
-
if (!hoveredNode) return false;
|
|
6992
|
-
const linkSourceId = typeof link.source === "string" ? link.source : link.source.id;
|
|
6993
|
-
const linkTargetId = typeof link.target === "string" ? link.target : link.target.id;
|
|
6994
|
-
return hoveredNode.id === linkSourceId || hoveredNode.id === linkTargetId;
|
|
6995
|
-
}
|
|
6996
|
-
return false;
|
|
7717
|
+
reset() {
|
|
7718
|
+
this.metrics = {
|
|
7719
|
+
renderTotal: 0,
|
|
7720
|
+
renderNodes: 0,
|
|
7721
|
+
renderLinks: 0,
|
|
7722
|
+
renderLinkLabels: 0,
|
|
7723
|
+
renderNodeLabels: 0,
|
|
7724
|
+
styleResolution: 0,
|
|
7725
|
+
hoverChecks: 0,
|
|
7726
|
+
canvasCalls: 0,
|
|
7727
|
+
frameCount: 0
|
|
7728
|
+
};
|
|
6997
7729
|
}
|
|
6998
7730
|
/**
|
|
6999
|
-
*
|
|
7731
|
+
* Log performance metrics for analysis
|
|
7000
7732
|
*/
|
|
7001
|
-
|
|
7002
|
-
|
|
7003
|
-
|
|
7004
|
-
|
|
7733
|
+
logMetrics(nodeCount, linkCount) {
|
|
7734
|
+
const frames = this.metrics.frameCount;
|
|
7735
|
+
console.log("\u{1F50D} PERFORMANCE METRICS (avg per frame over", frames, "frames):");
|
|
7736
|
+
console.log("\u{1F4CA} Graph size:", nodeCount, "nodes,", linkCount, "links");
|
|
7737
|
+
console.log("\u23F1\uFE0F Total render:", (this.metrics.renderTotal / frames).toFixed(2), "ms");
|
|
7738
|
+
console.log("\u{1F517} Links render:", (this.metrics.renderLinks / frames).toFixed(2), "ms");
|
|
7739
|
+
console.log("\u{1F3F7}\uFE0F Link labels:", (this.metrics.renderLinkLabels / frames).toFixed(2), "ms");
|
|
7740
|
+
console.log("\u2B55 Nodes render:", (this.metrics.renderNodes / frames).toFixed(2), "ms");
|
|
7741
|
+
console.log("\u{1F4DD} Node labels:", (this.metrics.renderNodeLabels / frames).toFixed(2), "ms");
|
|
7742
|
+
console.log("\u{1F3A8} Style resolution:", (this.metrics.styleResolution / frames).toFixed(2), "ms");
|
|
7743
|
+
console.log("\u{1F446} Hover checks:", (this.metrics.hoverChecks / frames).toFixed(2), "ms");
|
|
7744
|
+
console.log("\u{1F5BC}\uFE0F Canvas calls:", (this.metrics.canvasCalls / frames).toFixed(2), "ms");
|
|
7745
|
+
console.log("---");
|
|
7005
7746
|
}
|
|
7006
7747
|
/**
|
|
7007
|
-
* Check if
|
|
7748
|
+
* Check if it's time to log metrics (every N frames)
|
|
7008
7749
|
*/
|
|
7009
|
-
|
|
7010
|
-
|
|
7011
|
-
const selectionState = this.selectionManager.getSelectionState();
|
|
7012
|
-
if (!selectionState.selectedLink) return false;
|
|
7013
|
-
const sourceId1 = typeof link.source === "string" ? link.source : link.source.id;
|
|
7014
|
-
const targetId1 = typeof link.target === "string" ? link.target : link.target.id;
|
|
7015
|
-
const selectedLink = selectionState.selectedLink;
|
|
7016
|
-
const sourceId2 = typeof selectedLink.source === "string" ? selectedLink.source : selectedLink.source.id;
|
|
7017
|
-
const targetId2 = typeof selectedLink.target === "string" ? selectedLink.target : selectedLink.target.id;
|
|
7018
|
-
return sourceId1 === sourceId2 && targetId1 === targetId2;
|
|
7750
|
+
shouldLogMetrics(intervalFrames = 100) {
|
|
7751
|
+
return this.metrics.frameCount % intervalFrames === 0 && this.metrics.frameCount > 0;
|
|
7019
7752
|
}
|
|
7753
|
+
};
|
|
7754
|
+
|
|
7755
|
+
// src/v2/rendering/link-renderer.ts
|
|
7756
|
+
var LinkRenderer = class {
|
|
7020
7757
|
/**
|
|
7021
|
-
*
|
|
7022
|
-
* (either the link itself is selected, or its connected node is selected)
|
|
7758
|
+
* Render a directed link with optional arrow head
|
|
7023
7759
|
*/
|
|
7024
|
-
|
|
7025
|
-
|
|
7026
|
-
|
|
7027
|
-
|
|
7028
|
-
|
|
7029
|
-
|
|
7030
|
-
|
|
7031
|
-
|
|
7032
|
-
|
|
7033
|
-
|
|
7034
|
-
|
|
7760
|
+
static renderDirectedLink(ctx, source, target, style, styleResolver, isNodeHovered, isNodeSelected) {
|
|
7761
|
+
try {
|
|
7762
|
+
const sourcePoint = this.getShortenedSourcePoint(
|
|
7763
|
+
source,
|
|
7764
|
+
target,
|
|
7765
|
+
style,
|
|
7766
|
+
styleResolver,
|
|
7767
|
+
isNodeHovered,
|
|
7768
|
+
isNodeSelected
|
|
7769
|
+
);
|
|
7770
|
+
const targetPoint = this.getShortenedTargetPoint(
|
|
7771
|
+
source,
|
|
7772
|
+
target,
|
|
7773
|
+
style,
|
|
7774
|
+
styleResolver,
|
|
7775
|
+
isNodeHovered,
|
|
7776
|
+
isNodeSelected
|
|
7777
|
+
);
|
|
7778
|
+
ctx.strokeStyle = style.stroke;
|
|
7779
|
+
ctx.lineWidth = style.strokeWidth;
|
|
7780
|
+
ctx.globalAlpha = style.opacity;
|
|
7781
|
+
ctx.beginPath();
|
|
7782
|
+
ctx.moveTo(sourcePoint.x, sourcePoint.y);
|
|
7783
|
+
ctx.lineTo(targetPoint.x, targetPoint.y);
|
|
7784
|
+
ctx.stroke();
|
|
7785
|
+
if (style.arrow?.enabled) {
|
|
7786
|
+
this.renderArrow(ctx, sourcePoint, targetPoint, style.arrow);
|
|
7035
7787
|
}
|
|
7788
|
+
ctx.globalAlpha = 1;
|
|
7789
|
+
} catch (error) {
|
|
7790
|
+
ErrorHandler.logError(error);
|
|
7036
7791
|
}
|
|
7037
|
-
if (selectionState.selectedNode) {
|
|
7038
|
-
const selectedNode = selectionState.selectedNode;
|
|
7039
|
-
const linkSourceId = typeof link.source === "string" ? link.source : link.source.id;
|
|
7040
|
-
const linkTargetId = typeof link.target === "string" ? link.target : link.target.id;
|
|
7041
|
-
return selectedNode.id === linkSourceId || selectedNode.id === linkTargetId;
|
|
7042
|
-
}
|
|
7043
|
-
return false;
|
|
7044
7792
|
}
|
|
7045
7793
|
/**
|
|
7046
|
-
*
|
|
7794
|
+
* V1-compatible link shortening for source point
|
|
7047
7795
|
*/
|
|
7048
|
-
|
|
7049
|
-
|
|
7050
|
-
const
|
|
7051
|
-
const
|
|
7052
|
-
|
|
7053
|
-
|
|
7054
|
-
|
|
7796
|
+
static getShortenedSourcePoint(source, target, _style, styleResolver, isNodeHovered, isNodeSelected) {
|
|
7797
|
+
const sourceX = source.x ?? 0;
|
|
7798
|
+
const sourceY = source.y ?? 0;
|
|
7799
|
+
const targetX = target.x ?? 0;
|
|
7800
|
+
const targetY = target.y ?? 0;
|
|
7801
|
+
const dx = targetX - sourceX;
|
|
7802
|
+
const dy = targetY - sourceY;
|
|
7803
|
+
const distance = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
7804
|
+
const sourceNodeStyle = styleResolver.resolveNodeStyle({
|
|
7805
|
+
node: source,
|
|
7806
|
+
isHovered: isNodeHovered(source.id),
|
|
7807
|
+
isSelected: isNodeSelected(source.id)
|
|
7808
|
+
});
|
|
7809
|
+
const visualRadius = sourceNodeStyle.radius + sourceNodeStyle.strokeWidth / 2;
|
|
7810
|
+
const offset = visualRadius + 1;
|
|
7055
7811
|
return {
|
|
7056
|
-
x:
|
|
7057
|
-
y:
|
|
7812
|
+
x: sourceX + dx / distance * offset,
|
|
7813
|
+
y: sourceY + dy / distance * offset
|
|
7058
7814
|
};
|
|
7059
7815
|
}
|
|
7060
7816
|
/**
|
|
7061
|
-
*
|
|
7817
|
+
* V1-compatible link shortening for target point
|
|
7062
7818
|
*/
|
|
7063
|
-
|
|
7819
|
+
static getShortenedTargetPoint(source, target, style, styleResolver, isNodeHovered, isNodeSelected) {
|
|
7820
|
+
const sourceX = source.x ?? 0;
|
|
7821
|
+
const sourceY = source.y ?? 0;
|
|
7822
|
+
const targetX = target.x ?? 0;
|
|
7823
|
+
const targetY = target.y ?? 0;
|
|
7824
|
+
const dx = targetX - sourceX;
|
|
7825
|
+
const dy = targetY - sourceY;
|
|
7826
|
+
const distance = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
7827
|
+
const targetNodeStyle = styleResolver.resolveNodeStyle({
|
|
7828
|
+
node: target,
|
|
7829
|
+
isHovered: isNodeHovered(target.id),
|
|
7830
|
+
isSelected: isNodeSelected(target.id)
|
|
7831
|
+
});
|
|
7832
|
+
const visualRadius = targetNodeStyle.radius + targetNodeStyle.strokeWidth / 2;
|
|
7833
|
+
const arrowLength = style.arrow?.enabled ? style.arrow.size ?? 4 : 0;
|
|
7834
|
+
const offset = style.arrow?.enabled ? visualRadius + arrowLength : visualRadius + 1;
|
|
7835
|
+
const shortenedPoint = {
|
|
7836
|
+
x: targetX - dx / distance * offset,
|
|
7837
|
+
y: targetY - dy / distance * offset
|
|
7838
|
+
};
|
|
7839
|
+
return shortenedPoint;
|
|
7840
|
+
}
|
|
7841
|
+
/**
|
|
7842
|
+
* Render arrow head at specific points
|
|
7843
|
+
*/
|
|
7844
|
+
static renderArrow(ctx, sourcePoint, targetPoint, arrowStyle) {
|
|
7064
7845
|
try {
|
|
7065
|
-
const
|
|
7066
|
-
const
|
|
7067
|
-
|
|
7068
|
-
|
|
7069
|
-
|
|
7846
|
+
const dx = targetPoint.x - sourcePoint.x;
|
|
7847
|
+
const dy = targetPoint.y - sourcePoint.y;
|
|
7848
|
+
const angle = Math.atan2(dy, dx);
|
|
7849
|
+
const arrowLength = arrowStyle.size ?? 4;
|
|
7850
|
+
const arrowTipX = targetPoint.x + arrowLength * Math.cos(angle);
|
|
7851
|
+
const arrowTipY = targetPoint.y + arrowLength * Math.sin(angle);
|
|
7852
|
+
const x1 = arrowTipX - arrowLength * Math.cos(angle - Math.PI / 6);
|
|
7853
|
+
const y1 = arrowTipY - arrowLength * Math.sin(angle - Math.PI / 6);
|
|
7854
|
+
const x22 = arrowTipX - arrowLength * Math.cos(angle + Math.PI / 6);
|
|
7855
|
+
const y22 = arrowTipY - arrowLength * Math.sin(angle + Math.PI / 6);
|
|
7856
|
+
ctx.fillStyle = arrowStyle.fill ?? "#000000";
|
|
7070
7857
|
ctx.beginPath();
|
|
7071
|
-
ctx.moveTo(
|
|
7072
|
-
ctx.lineTo(
|
|
7073
|
-
ctx.
|
|
7074
|
-
|
|
7075
|
-
|
|
7858
|
+
ctx.moveTo(arrowTipX, arrowTipY);
|
|
7859
|
+
ctx.lineTo(x1, y1);
|
|
7860
|
+
ctx.lineTo(x22, y22);
|
|
7861
|
+
ctx.closePath();
|
|
7862
|
+
ctx.fill();
|
|
7863
|
+
} catch (error) {
|
|
7864
|
+
ErrorHandler.logError(error);
|
|
7865
|
+
}
|
|
7866
|
+
}
|
|
7867
|
+
};
|
|
7868
|
+
|
|
7869
|
+
// src/v2/rendering/interaction-state-resolver.ts
|
|
7870
|
+
var InteractionStateResolver = class {
|
|
7871
|
+
constructor(hoverManager, selectionManager) {
|
|
7872
|
+
this.hoverManager = hoverManager;
|
|
7873
|
+
this.selectionManager = selectionManager;
|
|
7874
|
+
}
|
|
7875
|
+
hoverManager;
|
|
7876
|
+
selectionManager;
|
|
7877
|
+
/**
|
|
7878
|
+
* Check if a node is currently hovered
|
|
7879
|
+
*/
|
|
7880
|
+
isNodeHovered(nodeId) {
|
|
7881
|
+
if (!this.hoverManager) return false;
|
|
7882
|
+
const hoverState = this.hoverManager.getHoverState();
|
|
7883
|
+
if (!hoverState.currentHovered || hoverState.currentHovered.d.entityType !== "Node") {
|
|
7884
|
+
return false;
|
|
7885
|
+
}
|
|
7886
|
+
const hoveredNode = hoverState.currentHovered.d;
|
|
7887
|
+
return hoveredNode && hoveredNode.id === nodeId;
|
|
7888
|
+
}
|
|
7889
|
+
/**
|
|
7890
|
+
* Check if a link is currently hovered (either directly or through associated node hover)
|
|
7891
|
+
*/
|
|
7892
|
+
isLinkHovered(link) {
|
|
7893
|
+
if (!this.hoverManager) return false;
|
|
7894
|
+
const hoverState = this.hoverManager.getHoverState();
|
|
7895
|
+
if (!hoverState.currentHovered) return false;
|
|
7896
|
+
if (hoverState.currentHovered.d.entityType === "Link") {
|
|
7897
|
+
const hoveredLink = hoverState.currentHovered.d;
|
|
7898
|
+
if (!hoveredLink) return false;
|
|
7899
|
+
const sourceId1 = typeof link.source === "string" ? link.source : link.source.id;
|
|
7900
|
+
const targetId1 = typeof link.target === "string" ? link.target : link.target.id;
|
|
7901
|
+
const sourceId2 = typeof hoveredLink.source === "string" ? hoveredLink.source : hoveredLink.source.id;
|
|
7902
|
+
const targetId2 = typeof hoveredLink.target === "string" ? hoveredLink.target : hoveredLink.target.id;
|
|
7903
|
+
return sourceId1 === sourceId2 && targetId1 === targetId2;
|
|
7904
|
+
}
|
|
7905
|
+
if (hoverState.currentHovered.d.entityType === "Node") {
|
|
7906
|
+
const hoveredNode = hoverState.currentHovered.d;
|
|
7907
|
+
if (!hoveredNode) return false;
|
|
7908
|
+
const linkSourceId = typeof link.source === "string" ? link.source : link.source.id;
|
|
7909
|
+
const linkTargetId = typeof link.target === "string" ? link.target : link.target.id;
|
|
7910
|
+
return hoveredNode.id === linkSourceId || hoveredNode.id === linkTargetId;
|
|
7911
|
+
}
|
|
7912
|
+
return false;
|
|
7913
|
+
}
|
|
7914
|
+
/**
|
|
7915
|
+
* Check if a node is currently selected
|
|
7916
|
+
*/
|
|
7917
|
+
isNodeSelected(nodeId) {
|
|
7918
|
+
if (!this.selectionManager) return false;
|
|
7919
|
+
const selectionState = this.selectionManager.getSelectionState();
|
|
7920
|
+
return selectionState.selectedNode?.id === nodeId;
|
|
7921
|
+
}
|
|
7922
|
+
/**
|
|
7923
|
+
* Check if a link is currently selected
|
|
7924
|
+
*/
|
|
7925
|
+
isLinkSelected(link) {
|
|
7926
|
+
if (!this.selectionManager) return false;
|
|
7927
|
+
const selectionState = this.selectionManager.getSelectionState();
|
|
7928
|
+
if (!selectionState.selectedLink) return false;
|
|
7929
|
+
const sourceId1 = typeof link.source === "string" ? link.source : link.source.id;
|
|
7930
|
+
const targetId1 = typeof link.target === "string" ? link.target : link.target.id;
|
|
7931
|
+
const selectedLink = selectionState.selectedLink;
|
|
7932
|
+
const sourceId2 = typeof selectedLink.source === "string" ? selectedLink.source : selectedLink.source.id;
|
|
7933
|
+
const targetId2 = typeof selectedLink.target === "string" ? selectedLink.target : selectedLink.target.id;
|
|
7934
|
+
return sourceId1 === sourceId2 && targetId1 === targetId2;
|
|
7935
|
+
}
|
|
7936
|
+
/**
|
|
7937
|
+
* Get interaction state for a node (optimized for caching)
|
|
7938
|
+
*/
|
|
7939
|
+
getNodeState(nodeId) {
|
|
7940
|
+
return {
|
|
7941
|
+
isHovered: this.isNodeHovered(nodeId),
|
|
7942
|
+
isSelected: this.isNodeSelected(nodeId)
|
|
7943
|
+
};
|
|
7944
|
+
}
|
|
7945
|
+
/**
|
|
7946
|
+
* Get interaction state for a link (optimized for caching)
|
|
7947
|
+
*/
|
|
7948
|
+
getLinkState(link) {
|
|
7949
|
+
return {
|
|
7950
|
+
isHovered: this.isLinkHovered(link),
|
|
7951
|
+
isSelected: this.isLinkSelected(link)
|
|
7952
|
+
};
|
|
7953
|
+
}
|
|
7954
|
+
/**
|
|
7955
|
+
* Create callback functions for external use (useful for renderers)
|
|
7956
|
+
*/
|
|
7957
|
+
createCallbacks() {
|
|
7958
|
+
return {
|
|
7959
|
+
isNodeHovered: (nodeId) => this.isNodeHovered(nodeId),
|
|
7960
|
+
isNodeSelected: (nodeId) => this.isNodeSelected(nodeId),
|
|
7961
|
+
isLinkHovered: (link) => this.isLinkHovered(link),
|
|
7962
|
+
isLinkSelected: (link) => this.isLinkSelected(link)
|
|
7963
|
+
};
|
|
7964
|
+
}
|
|
7965
|
+
/**
|
|
7966
|
+
* Update the managers (useful for re-initialization)
|
|
7967
|
+
*/
|
|
7968
|
+
updateManagers(hoverManager, selectionManager) {
|
|
7969
|
+
this.hoverManager = hoverManager;
|
|
7970
|
+
this.selectionManager = selectionManager;
|
|
7971
|
+
}
|
|
7972
|
+
};
|
|
7973
|
+
|
|
7974
|
+
// src/v2/utils/object-pool.ts
|
|
7975
|
+
var ObjectPool = class {
|
|
7976
|
+
pool = [];
|
|
7977
|
+
createFn;
|
|
7978
|
+
resetFn;
|
|
7979
|
+
maxSize;
|
|
7980
|
+
constructor(createFn, resetFn, maxSize = 1e3) {
|
|
7981
|
+
this.createFn = createFn;
|
|
7982
|
+
this.resetFn = resetFn;
|
|
7983
|
+
this.maxSize = maxSize;
|
|
7984
|
+
}
|
|
7985
|
+
/**
|
|
7986
|
+
* Get object from pool or create new one
|
|
7987
|
+
*/
|
|
7988
|
+
acquire() {
|
|
7989
|
+
const obj = this.pool.pop();
|
|
7990
|
+
if (obj) {
|
|
7991
|
+
if (this.resetFn) {
|
|
7992
|
+
this.resetFn(obj);
|
|
7993
|
+
} else if (obj.reset) {
|
|
7994
|
+
obj.reset();
|
|
7995
|
+
}
|
|
7996
|
+
return obj;
|
|
7997
|
+
}
|
|
7998
|
+
return this.createFn();
|
|
7999
|
+
}
|
|
8000
|
+
/**
|
|
8001
|
+
* Return object to pool for reuse
|
|
8002
|
+
*/
|
|
8003
|
+
release(obj) {
|
|
8004
|
+
if (this.pool.length < this.maxSize) {
|
|
8005
|
+
this.pool.push(obj);
|
|
8006
|
+
}
|
|
8007
|
+
}
|
|
8008
|
+
/**
|
|
8009
|
+
* Pre-warm pool with initial objects
|
|
8010
|
+
*/
|
|
8011
|
+
prewarm(count) {
|
|
8012
|
+
for (let i = 0; i < count; i++) {
|
|
8013
|
+
this.pool.push(this.createFn());
|
|
8014
|
+
}
|
|
8015
|
+
}
|
|
8016
|
+
/**
|
|
8017
|
+
* Get pool statistics
|
|
8018
|
+
*/
|
|
8019
|
+
getStats() {
|
|
8020
|
+
return {
|
|
8021
|
+
available: this.pool.length,
|
|
8022
|
+
maxSize: this.maxSize,
|
|
8023
|
+
utilization: (this.maxSize - this.pool.length) / this.maxSize
|
|
8024
|
+
};
|
|
8025
|
+
}
|
|
8026
|
+
/**
|
|
8027
|
+
* Clear the pool
|
|
8028
|
+
*/
|
|
8029
|
+
clear() {
|
|
8030
|
+
this.pool.length = 0;
|
|
8031
|
+
}
|
|
8032
|
+
};
|
|
8033
|
+
var DragObjectPoolManager = class {
|
|
8034
|
+
vector2DPool;
|
|
8035
|
+
nodeStatePool;
|
|
8036
|
+
linkStatePool;
|
|
8037
|
+
renderContextPool;
|
|
8038
|
+
// Pre-allocated arrays to avoid GC during drag
|
|
8039
|
+
reusableNodeArray = [];
|
|
8040
|
+
reusableLinkArray = [];
|
|
8041
|
+
reusableVector2DArray = [];
|
|
8042
|
+
constructor() {
|
|
8043
|
+
this.vector2DPool = new ObjectPool(
|
|
8044
|
+
() => ({ x: 0, y: 0, reset() {
|
|
8045
|
+
this.x = 0;
|
|
8046
|
+
this.y = 0;
|
|
8047
|
+
} }),
|
|
8048
|
+
(obj) => {
|
|
8049
|
+
obj.x = 0;
|
|
8050
|
+
obj.y = 0;
|
|
8051
|
+
},
|
|
8052
|
+
500
|
|
8053
|
+
);
|
|
8054
|
+
this.nodeStatePool = new ObjectPool(
|
|
8055
|
+
() => ({
|
|
8056
|
+
id: "",
|
|
8057
|
+
x: 0,
|
|
8058
|
+
y: 0,
|
|
8059
|
+
isHovered: false,
|
|
8060
|
+
isSelected: false,
|
|
8061
|
+
reset() {
|
|
8062
|
+
this.id = "";
|
|
8063
|
+
this.x = 0;
|
|
8064
|
+
this.y = 0;
|
|
8065
|
+
this.isHovered = false;
|
|
8066
|
+
this.isSelected = false;
|
|
8067
|
+
}
|
|
8068
|
+
}),
|
|
8069
|
+
(obj) => {
|
|
8070
|
+
obj.id = "";
|
|
8071
|
+
obj.x = 0;
|
|
8072
|
+
obj.y = 0;
|
|
8073
|
+
obj.isHovered = false;
|
|
8074
|
+
obj.isSelected = false;
|
|
8075
|
+
},
|
|
8076
|
+
1e3
|
|
8077
|
+
);
|
|
8078
|
+
this.linkStatePool = new ObjectPool(
|
|
8079
|
+
() => ({
|
|
8080
|
+
sourceId: "",
|
|
8081
|
+
targetId: "",
|
|
8082
|
+
sourceX: 0,
|
|
8083
|
+
sourceY: 0,
|
|
8084
|
+
targetX: 0,
|
|
8085
|
+
targetY: 0,
|
|
8086
|
+
isHovered: false,
|
|
8087
|
+
isSelected: false,
|
|
8088
|
+
reset() {
|
|
8089
|
+
this.sourceId = "";
|
|
8090
|
+
this.targetId = "";
|
|
8091
|
+
this.sourceX = 0;
|
|
8092
|
+
this.sourceY = 0;
|
|
8093
|
+
this.targetX = 0;
|
|
8094
|
+
this.targetY = 0;
|
|
8095
|
+
this.isHovered = false;
|
|
8096
|
+
this.isSelected = false;
|
|
8097
|
+
}
|
|
8098
|
+
}),
|
|
8099
|
+
(obj) => {
|
|
8100
|
+
obj.sourceId = "";
|
|
8101
|
+
obj.targetId = "";
|
|
8102
|
+
obj.sourceX = 0;
|
|
8103
|
+
obj.sourceY = 0;
|
|
8104
|
+
obj.targetX = 0;
|
|
8105
|
+
obj.targetY = 0;
|
|
8106
|
+
obj.isHovered = false;
|
|
8107
|
+
obj.isSelected = false;
|
|
8108
|
+
},
|
|
8109
|
+
2e3
|
|
8110
|
+
);
|
|
8111
|
+
this.renderContextPool = new ObjectPool(
|
|
8112
|
+
() => ({
|
|
8113
|
+
nodeStates: /* @__PURE__ */ new Map(),
|
|
8114
|
+
linkStates: /* @__PURE__ */ new Map(),
|
|
8115
|
+
tempVectors: [],
|
|
8116
|
+
reset() {
|
|
8117
|
+
this.nodeStates.clear();
|
|
8118
|
+
this.linkStates.clear();
|
|
8119
|
+
this.tempVectors.length = 0;
|
|
8120
|
+
}
|
|
8121
|
+
}),
|
|
8122
|
+
(obj) => {
|
|
8123
|
+
obj.nodeStates.clear();
|
|
8124
|
+
obj.linkStates.clear();
|
|
8125
|
+
obj.tempVectors.length = 0;
|
|
8126
|
+
},
|
|
8127
|
+
10
|
|
8128
|
+
);
|
|
8129
|
+
this.prewarmPools();
|
|
8130
|
+
}
|
|
8131
|
+
/**
|
|
8132
|
+
* Pre-warm pools with initial objects
|
|
8133
|
+
*/
|
|
8134
|
+
prewarmPools() {
|
|
8135
|
+
this.vector2DPool.prewarm(50);
|
|
8136
|
+
this.nodeStatePool.prewarm(100);
|
|
8137
|
+
this.linkStatePool.prewarm(200);
|
|
8138
|
+
this.renderContextPool.prewarm(2);
|
|
8139
|
+
}
|
|
8140
|
+
/**
|
|
8141
|
+
* Acquire temporary 2D vector
|
|
8142
|
+
*/
|
|
8143
|
+
acquireVector2D(x3 = 0, y3 = 0) {
|
|
8144
|
+
const vector = this.vector2DPool.acquire();
|
|
8145
|
+
vector.x = x3;
|
|
8146
|
+
vector.y = y3;
|
|
8147
|
+
return vector;
|
|
8148
|
+
}
|
|
8149
|
+
/**
|
|
8150
|
+
* Release 2D vector back to pool
|
|
8151
|
+
*/
|
|
8152
|
+
releaseVector2D(vector) {
|
|
8153
|
+
this.vector2DPool.release(vector);
|
|
8154
|
+
}
|
|
8155
|
+
/**
|
|
8156
|
+
* Acquire node state object
|
|
8157
|
+
*/
|
|
8158
|
+
acquireNodeState(id2, x3, y3, isHovered = false, isSelected = false) {
|
|
8159
|
+
const state = this.nodeStatePool.acquire();
|
|
8160
|
+
state.id = id2;
|
|
8161
|
+
state.x = x3;
|
|
8162
|
+
state.y = y3;
|
|
8163
|
+
state.isHovered = isHovered;
|
|
8164
|
+
state.isSelected = isSelected;
|
|
8165
|
+
return state;
|
|
8166
|
+
}
|
|
8167
|
+
/**
|
|
8168
|
+
* Release node state back to pool
|
|
8169
|
+
*/
|
|
8170
|
+
releaseNodeState(state) {
|
|
8171
|
+
this.nodeStatePool.release(state);
|
|
8172
|
+
}
|
|
8173
|
+
/**
|
|
8174
|
+
* Acquire link state object
|
|
8175
|
+
*/
|
|
8176
|
+
acquireLinkState(sourceId, targetId, sourceX, sourceY, targetX, targetY, isHovered = false, isSelected = false) {
|
|
8177
|
+
const state = this.linkStatePool.acquire();
|
|
8178
|
+
state.sourceId = sourceId;
|
|
8179
|
+
state.targetId = targetId;
|
|
8180
|
+
state.sourceX = sourceX;
|
|
8181
|
+
state.sourceY = sourceY;
|
|
8182
|
+
state.targetX = targetX;
|
|
8183
|
+
state.targetY = targetY;
|
|
8184
|
+
state.isHovered = isHovered;
|
|
8185
|
+
state.isSelected = isSelected;
|
|
8186
|
+
return state;
|
|
8187
|
+
}
|
|
8188
|
+
/**
|
|
8189
|
+
* Release link state back to pool
|
|
8190
|
+
*/
|
|
8191
|
+
releaseLinkState(state) {
|
|
8192
|
+
this.linkStatePool.release(state);
|
|
8193
|
+
}
|
|
8194
|
+
/**
|
|
8195
|
+
* Acquire render context
|
|
8196
|
+
*/
|
|
8197
|
+
acquireRenderContext() {
|
|
8198
|
+
return this.renderContextPool.acquire();
|
|
8199
|
+
}
|
|
8200
|
+
/**
|
|
8201
|
+
* Release render context back to pool
|
|
8202
|
+
*/
|
|
8203
|
+
releaseRenderContext(context) {
|
|
8204
|
+
this.renderContextPool.release(context);
|
|
8205
|
+
}
|
|
8206
|
+
/**
|
|
8207
|
+
* Get reusable node array (cleared and ready for use)
|
|
8208
|
+
*/
|
|
8209
|
+
getReusableNodeArray() {
|
|
8210
|
+
this.reusableNodeArray.length = 0;
|
|
8211
|
+
return this.reusableNodeArray;
|
|
8212
|
+
}
|
|
8213
|
+
/**
|
|
8214
|
+
* Get reusable link array (cleared and ready for use)
|
|
8215
|
+
*/
|
|
8216
|
+
getReusableLinkArray() {
|
|
8217
|
+
this.reusableLinkArray.length = 0;
|
|
8218
|
+
return this.reusableLinkArray;
|
|
8219
|
+
}
|
|
8220
|
+
/**
|
|
8221
|
+
* Get reusable vector array (cleared and ready for use)
|
|
8222
|
+
*/
|
|
8223
|
+
getReusableVector2DArray() {
|
|
8224
|
+
this.reusableVector2DArray.length = 0;
|
|
8225
|
+
return this.reusableVector2DArray;
|
|
8226
|
+
}
|
|
8227
|
+
/**
|
|
8228
|
+
* Batch acquire multiple vectors
|
|
8229
|
+
*/
|
|
8230
|
+
batchAcquireVectors(count) {
|
|
8231
|
+
const vectors = this.getReusableVector2DArray();
|
|
8232
|
+
for (let i = 0; i < count; i++) {
|
|
8233
|
+
vectors.push(this.acquireVector2D());
|
|
8234
|
+
}
|
|
8235
|
+
return vectors;
|
|
8236
|
+
}
|
|
8237
|
+
/**
|
|
8238
|
+
* Batch release multiple vectors
|
|
8239
|
+
*/
|
|
8240
|
+
batchReleaseVectors(vectors) {
|
|
8241
|
+
for (const vector of vectors) {
|
|
8242
|
+
this.releaseVector2D(vector);
|
|
8243
|
+
}
|
|
8244
|
+
}
|
|
8245
|
+
/**
|
|
8246
|
+
* Get comprehensive pool statistics
|
|
8247
|
+
*/
|
|
8248
|
+
getStats() {
|
|
8249
|
+
const vector2DStats = this.vector2DPool.getStats();
|
|
8250
|
+
const nodeStateStats = this.nodeStatePool.getStats();
|
|
8251
|
+
const linkStateStats = this.linkStatePool.getStats();
|
|
8252
|
+
const renderContextStats = this.renderContextPool.getStats();
|
|
8253
|
+
const vector2DBytes = vector2DStats.maxSize * (2 * 8);
|
|
8254
|
+
const nodeStateBytes = nodeStateStats.maxSize * (32 + 2 * 8 + 2 * 1);
|
|
8255
|
+
const linkStateBytes = linkStateStats.maxSize * (64 + 4 * 8 + 2 * 1);
|
|
8256
|
+
return {
|
|
8257
|
+
vector2D: vector2DStats,
|
|
8258
|
+
nodeState: nodeStateStats,
|
|
8259
|
+
linkState: linkStateStats,
|
|
8260
|
+
renderContext: renderContextStats,
|
|
8261
|
+
memoryEstimate: {
|
|
8262
|
+
vector2DBytes,
|
|
8263
|
+
nodeStateBytes,
|
|
8264
|
+
linkStateBytes,
|
|
8265
|
+
totalBytes: vector2DBytes + nodeStateBytes + linkStateBytes
|
|
8266
|
+
}
|
|
8267
|
+
};
|
|
8268
|
+
}
|
|
8269
|
+
/**
|
|
8270
|
+
* Force garbage collection optimization by clearing and re-prewarming pools
|
|
8271
|
+
*/
|
|
8272
|
+
optimizeMemory() {
|
|
8273
|
+
this.vector2DPool.clear();
|
|
8274
|
+
this.nodeStatePool.clear();
|
|
8275
|
+
this.linkStatePool.clear();
|
|
8276
|
+
this.renderContextPool.clear();
|
|
8277
|
+
this.reusableNodeArray.length = 0;
|
|
8278
|
+
this.reusableLinkArray.length = 0;
|
|
8279
|
+
this.reusableVector2DArray.length = 0;
|
|
8280
|
+
this.prewarmPools();
|
|
8281
|
+
}
|
|
8282
|
+
/**
|
|
8283
|
+
* Destroy and clean up all pools
|
|
8284
|
+
*/
|
|
8285
|
+
destroy() {
|
|
8286
|
+
this.vector2DPool.clear();
|
|
8287
|
+
this.nodeStatePool.clear();
|
|
8288
|
+
this.linkStatePool.clear();
|
|
8289
|
+
this.renderContextPool.clear();
|
|
8290
|
+
this.reusableNodeArray.length = 0;
|
|
8291
|
+
this.reusableLinkArray.length = 0;
|
|
8292
|
+
this.reusableVector2DArray.length = 0;
|
|
8293
|
+
}
|
|
8294
|
+
};
|
|
8295
|
+
var dragObjectPool = new DragObjectPoolManager();
|
|
8296
|
+
|
|
8297
|
+
// src/v2/rendering/drag-optimizer.ts
|
|
8298
|
+
var DragOptimizer = class {
|
|
8299
|
+
config;
|
|
8300
|
+
state = {
|
|
8301
|
+
isDragModeActive: false,
|
|
8302
|
+
lastDragRenderTime: 0
|
|
8303
|
+
};
|
|
8304
|
+
// Fast drag cache for O(1) lookups - everything pre-computed
|
|
8305
|
+
fastDragNodeCache = /* @__PURE__ */ new Map();
|
|
8306
|
+
// Object pool optimization
|
|
8307
|
+
reusableNodeStates = [];
|
|
8308
|
+
reusableVectors = [];
|
|
8309
|
+
/**
|
|
8310
|
+
* Initialize drag optimizer with object pooling optimizations
|
|
8311
|
+
*/
|
|
8312
|
+
initialize(config) {
|
|
8313
|
+
this.config = config;
|
|
8314
|
+
this.buildFastDragCache();
|
|
8315
|
+
this.reusableNodeStates = dragObjectPool.getReusableNodeArray();
|
|
8316
|
+
this.reusableVectors = dragObjectPool.getReusableVector2DArray();
|
|
8317
|
+
}
|
|
8318
|
+
/**
|
|
8319
|
+
* Set the dragged node (like zoom renderer - simple state tracking)
|
|
8320
|
+
*/
|
|
8321
|
+
setDraggedNode(draggedNode) {
|
|
8322
|
+
if (!this.config) return;
|
|
8323
|
+
try {
|
|
8324
|
+
this.state.draggedNodeId = draggedNode.id;
|
|
8325
|
+
console.log(`\u{1F680} Fast drag mode - rendering ALL ${this.config.nodes.length} nodes and ${this.config.links.length} links with simplified styling`);
|
|
8326
|
+
} catch (error) {
|
|
8327
|
+
ErrorHandler.logError(error);
|
|
8328
|
+
}
|
|
8329
|
+
}
|
|
8330
|
+
/**
|
|
8331
|
+
* Render with drag optimization using object pooling
|
|
8332
|
+
*/
|
|
8333
|
+
render(ctx, performanceMetrics) {
|
|
8334
|
+
if (!this.config) return;
|
|
8335
|
+
if (this.state.isDragModeActive) {
|
|
8336
|
+
this.renderFastDragOptimized(ctx);
|
|
8337
|
+
} else {
|
|
8338
|
+
this.config.zIndexRenderer.render(ctx, performanceMetrics);
|
|
8339
|
+
}
|
|
8340
|
+
}
|
|
8341
|
+
/**
|
|
8342
|
+
* Handle drag movement with RAF throttling
|
|
8343
|
+
*/
|
|
8344
|
+
handleDragMove() {
|
|
8345
|
+
if (!this.config || !this.state.isDragModeActive) return;
|
|
8346
|
+
this.state.lastDragRenderTime = performance.now();
|
|
8347
|
+
}
|
|
8348
|
+
/**
|
|
8349
|
+
* End drag mode and restore full rendering
|
|
8350
|
+
*/
|
|
8351
|
+
endDragMode() {
|
|
8352
|
+
if (!this.config) return;
|
|
8353
|
+
try {
|
|
8354
|
+
this.state.isDragModeActive = false;
|
|
8355
|
+
this.state.draggedNodeId = void 0;
|
|
8356
|
+
} catch (error) {
|
|
8357
|
+
ErrorHandler.logError(error);
|
|
8358
|
+
}
|
|
8359
|
+
}
|
|
8360
|
+
/**
|
|
8361
|
+
* Optimized fast rendering with object pooling and minimal GC
|
|
8362
|
+
*/
|
|
8363
|
+
renderFastDragOptimized(ctx) {
|
|
8364
|
+
if (!this.config) return;
|
|
8365
|
+
const { nodes, links } = this.config;
|
|
8366
|
+
const startTime = performance.now();
|
|
8367
|
+
this.reusableNodeStates = dragObjectPool.getReusableNodeArray();
|
|
8368
|
+
this.reusableVectors = dragObjectPool.getReusableVector2DArray();
|
|
8369
|
+
try {
|
|
8370
|
+
const linkVectors = dragObjectPool.batchAcquireVectors(links.length * 2);
|
|
8371
|
+
let vectorIndex = 0;
|
|
8372
|
+
ctx.strokeStyle = "#999";
|
|
8373
|
+
ctx.lineWidth = 1;
|
|
8374
|
+
ctx.globalAlpha = 0.6;
|
|
8375
|
+
for (const link of links) {
|
|
8376
|
+
const sourceNode = typeof link.source === "string" ? this.config.stateManager.getNode(link.source) : link.source;
|
|
8377
|
+
const targetNode = typeof link.target === "string" ? this.config.stateManager.getNode(link.target) : link.target;
|
|
8378
|
+
if (sourceNode?.x != null && sourceNode?.y != null && targetNode?.x != null && targetNode?.y != null) {
|
|
8379
|
+
const sourceVec = linkVectors[vectorIndex++];
|
|
8380
|
+
const targetVec = linkVectors[vectorIndex++];
|
|
8381
|
+
if (sourceVec && targetVec) {
|
|
8382
|
+
sourceVec.x = sourceNode.x;
|
|
8383
|
+
sourceVec.y = sourceNode.y;
|
|
8384
|
+
targetVec.x = targetNode.x;
|
|
8385
|
+
targetVec.y = targetNode.y;
|
|
8386
|
+
ctx.beginPath();
|
|
8387
|
+
ctx.moveTo(sourceVec.x, sourceVec.y);
|
|
8388
|
+
ctx.lineTo(targetVec.x, targetVec.y);
|
|
8389
|
+
ctx.stroke();
|
|
8390
|
+
}
|
|
8391
|
+
}
|
|
7076
8392
|
}
|
|
7077
8393
|
ctx.globalAlpha = 1;
|
|
8394
|
+
for (const node of nodes) {
|
|
8395
|
+
if (node.x == null || node.y == null) continue;
|
|
8396
|
+
const cachedProps = this.fastDragNodeCache.get(node.id);
|
|
8397
|
+
if (!cachedProps) continue;
|
|
8398
|
+
ctx.beginPath();
|
|
8399
|
+
ctx.arc(node.x, node.y, cachedProps.radius, 0, 2 * Math.PI);
|
|
8400
|
+
ctx.fillStyle = cachedProps.fill;
|
|
8401
|
+
ctx.fill();
|
|
8402
|
+
if (cachedProps.strokeWidth > 0) {
|
|
8403
|
+
ctx.strokeStyle = cachedProps.stroke;
|
|
8404
|
+
ctx.lineWidth = cachedProps.strokeWidth;
|
|
8405
|
+
ctx.stroke();
|
|
8406
|
+
}
|
|
8407
|
+
}
|
|
8408
|
+
this.renderFastDragLabelsOptimized(ctx);
|
|
8409
|
+
dragObjectPool.batchReleaseVectors(linkVectors);
|
|
7078
8410
|
} catch (error) {
|
|
7079
8411
|
ErrorHandler.logError(error);
|
|
7080
8412
|
}
|
|
8413
|
+
this.state.lastDragRenderTime = performance.now() - startTime;
|
|
7081
8414
|
}
|
|
7082
8415
|
/**
|
|
7083
|
-
*
|
|
8416
|
+
* Optimized label rendering with minimal object allocation
|
|
7084
8417
|
*/
|
|
7085
|
-
|
|
7086
|
-
|
|
7087
|
-
const
|
|
7088
|
-
|
|
7089
|
-
|
|
7090
|
-
|
|
7091
|
-
const
|
|
7092
|
-
|
|
7093
|
-
|
|
7094
|
-
|
|
7095
|
-
|
|
7096
|
-
|
|
7097
|
-
}
|
|
7098
|
-
|
|
7099
|
-
|
|
8418
|
+
renderFastDragLabelsOptimized(ctx) {
|
|
8419
|
+
if (!this.config) return;
|
|
8420
|
+
const { nodes } = this.config;
|
|
8421
|
+
ctx.font = "10px Arial";
|
|
8422
|
+
ctx.textAlign = "center";
|
|
8423
|
+
ctx.textBaseline = "middle";
|
|
8424
|
+
for (const node of nodes) {
|
|
8425
|
+
if (node.x == null || node.y == null) continue;
|
|
8426
|
+
const cachedProps = this.fastDragNodeCache.get(node.id);
|
|
8427
|
+
if (!cachedProps?.truncatedLabel) continue;
|
|
8428
|
+
ctx.fillStyle = cachedProps.textColor;
|
|
8429
|
+
ctx.fillText(cachedProps.truncatedLabel, node.x, node.y);
|
|
8430
|
+
}
|
|
8431
|
+
}
|
|
8432
|
+
/**
|
|
8433
|
+
* Build fast drag cache with pre-computed properties (zoom-renderer pattern)
|
|
8434
|
+
*/
|
|
8435
|
+
buildFastDragCache() {
|
|
8436
|
+
if (!this.config) return;
|
|
8437
|
+
try {
|
|
8438
|
+
this.fastDragNodeCache.clear();
|
|
8439
|
+
const tempCanvas = document.createElement("canvas");
|
|
8440
|
+
const tempCtx = tempCanvas.getContext("2d");
|
|
8441
|
+
if (!tempCtx) return;
|
|
8442
|
+
tempCtx.font = "10px Arial";
|
|
8443
|
+
for (const node of this.config.nodes) {
|
|
8444
|
+
const nodeStyle = this.config.styleResolver.resolveNodeStyle({
|
|
8445
|
+
node,
|
|
8446
|
+
isHovered: false,
|
|
8447
|
+
isSelected: false
|
|
8448
|
+
});
|
|
8449
|
+
const label = node.label || node.id;
|
|
8450
|
+
const maxWidth = nodeStyle.radius * 2 - 6;
|
|
8451
|
+
const truncatedLabel = this.preComputeTruncatedLabel(tempCtx, label, maxWidth);
|
|
8452
|
+
const textColor = nodeStyle.label?.textColor || "#ffffff";
|
|
8453
|
+
this.fastDragNodeCache.set(node.id, {
|
|
8454
|
+
radius: nodeStyle.radius,
|
|
8455
|
+
fill: nodeStyle.fill,
|
|
8456
|
+
stroke: nodeStyle.stroke,
|
|
8457
|
+
strokeWidth: nodeStyle.strokeWidth,
|
|
8458
|
+
textColor,
|
|
8459
|
+
truncatedLabel
|
|
8460
|
+
});
|
|
8461
|
+
}
|
|
8462
|
+
} catch (error) {
|
|
8463
|
+
ErrorHandler.logError(error);
|
|
8464
|
+
}
|
|
8465
|
+
}
|
|
8466
|
+
/**
|
|
8467
|
+
* Pre-compute truncated label (zoom-renderer pattern)
|
|
8468
|
+
*/
|
|
8469
|
+
preComputeTruncatedLabel(ctx, label, maxWidth) {
|
|
8470
|
+
if (ctx.measureText(label).width <= maxWidth) {
|
|
8471
|
+
return label;
|
|
8472
|
+
}
|
|
8473
|
+
let truncated = label;
|
|
8474
|
+
while (truncated.length > 1 && ctx.measureText(`${truncated}\u2026`).width > maxWidth) {
|
|
8475
|
+
truncated = truncated.slice(0, -1);
|
|
8476
|
+
}
|
|
8477
|
+
return truncated.length < label.length ? `${truncated}\u2026` : truncated;
|
|
8478
|
+
}
|
|
8479
|
+
/**
|
|
8480
|
+
* Set drag state for performance optimization (like ZoomRenderer.setZoomState)
|
|
8481
|
+
*/
|
|
8482
|
+
setDragState(isDragging) {
|
|
8483
|
+
if (isDragging) {
|
|
8484
|
+
this.state.isDragModeActive = true;
|
|
8485
|
+
} else {
|
|
8486
|
+
this.state.isDragModeActive = false;
|
|
8487
|
+
this.state.draggedNodeId = void 0;
|
|
8488
|
+
}
|
|
8489
|
+
}
|
|
8490
|
+
/**
|
|
8491
|
+
* Update configuration and rebuild cache
|
|
8492
|
+
*/
|
|
8493
|
+
updateConfig(updates) {
|
|
8494
|
+
if (this.config) {
|
|
8495
|
+
Object.assign(this.config, updates);
|
|
8496
|
+
this.buildFastDragCache();
|
|
8497
|
+
}
|
|
8498
|
+
}
|
|
8499
|
+
/**
|
|
8500
|
+
* Get drag optimization statistics including object pool metrics
|
|
8501
|
+
*/
|
|
8502
|
+
getStats() {
|
|
8503
|
+
const poolStats = dragObjectPool.getStats();
|
|
7100
8504
|
return {
|
|
7101
|
-
|
|
7102
|
-
|
|
8505
|
+
isDragModeActive: this.state.isDragModeActive,
|
|
8506
|
+
cachedNodeProperties: this.fastDragNodeCache.size,
|
|
8507
|
+
lastRenderTime: this.state.lastDragRenderTime,
|
|
8508
|
+
objectPool: {
|
|
8509
|
+
vector2D: poolStats.vector2D,
|
|
8510
|
+
nodeState: poolStats.nodeState,
|
|
8511
|
+
memoryEstimate: poolStats.memoryEstimate
|
|
8512
|
+
}
|
|
7103
8513
|
};
|
|
7104
8514
|
}
|
|
7105
8515
|
/**
|
|
7106
|
-
*
|
|
8516
|
+
* Destroy and clean up all resources
|
|
8517
|
+
*/
|
|
8518
|
+
destroy() {
|
|
8519
|
+
this.reusableNodeStates.length = 0;
|
|
8520
|
+
this.reusableVectors.length = 0;
|
|
8521
|
+
this.config = void 0;
|
|
8522
|
+
this.fastDragNodeCache.clear();
|
|
8523
|
+
this.state = {
|
|
8524
|
+
isDragModeActive: false,
|
|
8525
|
+
lastDragRenderTime: 0
|
|
8526
|
+
};
|
|
8527
|
+
}
|
|
8528
|
+
/**
|
|
8529
|
+
* Force memory optimization by clearing and rebuilding caches
|
|
8530
|
+
*/
|
|
8531
|
+
optimizeMemory() {
|
|
8532
|
+
this.buildFastDragCache();
|
|
8533
|
+
dragObjectPool.optimizeMemory();
|
|
8534
|
+
}
|
|
8535
|
+
};
|
|
8536
|
+
|
|
8537
|
+
// src/v2/rendering/renderer.ts
|
|
8538
|
+
var Renderer = class {
|
|
8539
|
+
config;
|
|
8540
|
+
canvasState;
|
|
8541
|
+
hoverManager;
|
|
8542
|
+
selectionManager;
|
|
8543
|
+
styleResolver;
|
|
8544
|
+
// Centralized state management for O(1) lookups
|
|
8545
|
+
stateManager = new StateManager();
|
|
8546
|
+
// Performance metrics tracking
|
|
8547
|
+
metricsManager = new PerformanceMetricsManager();
|
|
8548
|
+
// Interaction state resolution
|
|
8549
|
+
interactionResolver = new InteractionStateResolver();
|
|
8550
|
+
// Drag optimization
|
|
8551
|
+
dragOptimizer = new DragOptimizer();
|
|
8552
|
+
// Optimized Z-Index Renderer
|
|
8553
|
+
zIndexRenderer = new OptimizedZIndexRenderer();
|
|
8554
|
+
// Dedicated Zoom Renderer for separation of concerns
|
|
8555
|
+
zoomRenderer = new ZoomRenderer();
|
|
8556
|
+
// Dedicated Hit Detection Renderer for shadow canvas
|
|
8557
|
+
hitDetectionRenderer = new HitDetectionRenderer();
|
|
8558
|
+
/**
|
|
8559
|
+
* Initialize the renderer
|
|
8560
|
+
*/
|
|
8561
|
+
initialize(config, canvasState, hoverManager, selectionManager) {
|
|
8562
|
+
try {
|
|
8563
|
+
this.config = config;
|
|
8564
|
+
this.canvasState = canvasState;
|
|
8565
|
+
this.hoverManager = hoverManager;
|
|
8566
|
+
this.selectionManager = selectionManager;
|
|
8567
|
+
this.styleResolver = createStyleResolver(config.interaction);
|
|
8568
|
+
this.stateManager.initialize({ nodes: config.nodes, links: config.links });
|
|
8569
|
+
this.interactionResolver.updateManagers(hoverManager, selectionManager);
|
|
8570
|
+
this.initializeZIndexRenderer();
|
|
8571
|
+
this.initializeZoomRenderer();
|
|
8572
|
+
this.initializeHitDetectionRenderer();
|
|
8573
|
+
this.initializeDragOptimizer();
|
|
8574
|
+
} catch (error) {
|
|
8575
|
+
ErrorHandler.logError(error, {
|
|
8576
|
+
nodeCount: config.nodes.length,
|
|
8577
|
+
linkCount: config.links.length
|
|
8578
|
+
});
|
|
8579
|
+
throw error;
|
|
8580
|
+
}
|
|
8581
|
+
}
|
|
8582
|
+
/**
|
|
8583
|
+
* Initialize the optimized z-index renderer with all required dependencies
|
|
8584
|
+
*/
|
|
8585
|
+
initializeZIndexRenderer() {
|
|
8586
|
+
if (!this.config || !this.styleResolver) return;
|
|
8587
|
+
const callbacks = this.interactionResolver.createCallbacks();
|
|
8588
|
+
this.zIndexRenderer.initialize({
|
|
8589
|
+
nodes: this.config.nodes,
|
|
8590
|
+
links: this.config.links,
|
|
8591
|
+
styleResolver: this.styleResolver,
|
|
8592
|
+
isNodeHovered: callbacks.isNodeHovered,
|
|
8593
|
+
isNodeSelected: callbacks.isNodeSelected,
|
|
8594
|
+
isLinkHovered: callbacks.isLinkHovered,
|
|
8595
|
+
isLinkSelected: callbacks.isLinkSelected,
|
|
8596
|
+
getLinkId: (link) => this.getLinkId(link),
|
|
8597
|
+
getLinkMidpoint: (link) => this.getLinkMidpoint(link),
|
|
8598
|
+
renderNodes: (ctx, nodes) => this.renderNodesLayer(ctx, nodes),
|
|
8599
|
+
renderLinks: (ctx, links) => this.renderLinksLayer(ctx, links),
|
|
8600
|
+
renderNodeLabels: (ctx, nodes) => this.renderNodeLabelsLayer(ctx, nodes)
|
|
8601
|
+
});
|
|
8602
|
+
}
|
|
8603
|
+
/**
|
|
8604
|
+
* Initialize the zoom renderer for zoom-specific optimizations
|
|
8605
|
+
*/
|
|
8606
|
+
initializeZoomRenderer() {
|
|
8607
|
+
if (!this.config || !this.styleResolver) return;
|
|
8608
|
+
this.zoomRenderer.initialize({
|
|
8609
|
+
nodes: this.config.nodes,
|
|
8610
|
+
links: this.config.links,
|
|
8611
|
+
styleResolver: this.styleResolver,
|
|
8612
|
+
stateManager: this.stateManager
|
|
8613
|
+
});
|
|
8614
|
+
}
|
|
8615
|
+
/**
|
|
8616
|
+
* Initialize the hit detection renderer for shadow canvas
|
|
8617
|
+
*/
|
|
8618
|
+
initializeHitDetectionRenderer() {
|
|
8619
|
+
if (!this.config || !this.styleResolver || !this.canvasState) return;
|
|
8620
|
+
this.hitDetectionRenderer.initialize({
|
|
8621
|
+
nodes: this.config.nodes,
|
|
8622
|
+
links: this.config.links,
|
|
8623
|
+
styleResolver: this.styleResolver,
|
|
8624
|
+
stateManager: this.stateManager,
|
|
8625
|
+
canvas: this.canvasState.canvas,
|
|
8626
|
+
shadowCtx: this.canvasState.shadowCtx,
|
|
8627
|
+
linkHoverPrecision: 4
|
|
8628
|
+
// Default 4px like force-graph
|
|
8629
|
+
});
|
|
8630
|
+
}
|
|
8631
|
+
/**
|
|
8632
|
+
* Initialize the drag optimizer for fast drag rendering
|
|
8633
|
+
*/
|
|
8634
|
+
initializeDragOptimizer() {
|
|
8635
|
+
if (!this.config || !this.styleResolver) return;
|
|
8636
|
+
this.dragOptimizer.initialize({
|
|
8637
|
+
nodes: this.config.nodes,
|
|
8638
|
+
links: this.config.links,
|
|
8639
|
+
stateManager: this.stateManager,
|
|
8640
|
+
styleResolver: this.styleResolver,
|
|
8641
|
+
interactionResolver: this.interactionResolver,
|
|
8642
|
+
zIndexRenderer: this.zIndexRenderer
|
|
8643
|
+
});
|
|
8644
|
+
}
|
|
8645
|
+
/**
|
|
8646
|
+
* Main render method with performance metrics (Instrumented)
|
|
8647
|
+
*/
|
|
8648
|
+
render() {
|
|
8649
|
+
if (!this.config || !this.canvasState) {
|
|
8650
|
+
throw new RenderError("Renderer not initialized");
|
|
8651
|
+
}
|
|
8652
|
+
const startTime = performance.now();
|
|
8653
|
+
this.metricsManager.incrementFrame();
|
|
8654
|
+
try {
|
|
8655
|
+
const { ctx } = this.canvasState;
|
|
8656
|
+
this.clearCanvas(ctx);
|
|
8657
|
+
this.dragOptimizer.render(ctx, this.metricsManager.getMetrics());
|
|
8658
|
+
this.hitDetectionRenderer.markShadowCanvasDirty();
|
|
8659
|
+
this.metricsManager.addTiming("renderTotal", performance.now() - startTime);
|
|
8660
|
+
if (this.metricsManager.shouldLogMetrics()) {
|
|
8661
|
+
this.metricsManager.logMetrics(this.config.nodes.length, this.config.links.length);
|
|
8662
|
+
}
|
|
8663
|
+
} catch (error) {
|
|
8664
|
+
ErrorHandler.logError(error);
|
|
8665
|
+
throw new RenderError("Failed to render graph", {
|
|
8666
|
+
nodeCount: this.config.nodes.length,
|
|
8667
|
+
linkCount: this.config.links.length,
|
|
8668
|
+
originalError: error.message
|
|
8669
|
+
});
|
|
8670
|
+
}
|
|
8671
|
+
}
|
|
8672
|
+
/**
|
|
8673
|
+
* Render with transform (called during zoom/pan) - OPTIMIZED PATTERN
|
|
8674
|
+
* Fast rendering during zoom, full rendering when stopped
|
|
8675
|
+
*/
|
|
8676
|
+
renderWithTransform() {
|
|
8677
|
+
if (!this.canvasState) return;
|
|
8678
|
+
try {
|
|
8679
|
+
const { canvas, ctx } = this.canvasState;
|
|
8680
|
+
const transform2 = transform(canvas);
|
|
8681
|
+
this.clearCanvas(ctx);
|
|
8682
|
+
this.applyTransform(transform2, ctx);
|
|
8683
|
+
const isDragging = this.dragOptimizer.getStats().isDragModeActive;
|
|
8684
|
+
const isZooming = this.zoomRenderer.getZoomState();
|
|
8685
|
+
if (isDragging) {
|
|
8686
|
+
this.dragOptimizer.render(ctx, this.metricsManager.getMetrics());
|
|
8687
|
+
} else if (isZooming) {
|
|
8688
|
+
this.zoomRenderer.render(ctx, this.zIndexRenderer);
|
|
8689
|
+
} else {
|
|
8690
|
+
this.zIndexRenderer.render(ctx, this.metricsManager.getMetrics());
|
|
8691
|
+
}
|
|
8692
|
+
} catch (error) {
|
|
8693
|
+
ErrorHandler.logError(error);
|
|
8694
|
+
}
|
|
8695
|
+
}
|
|
8696
|
+
/**
|
|
8697
|
+
* Set zoom state for performance optimization (delegates to ZoomRenderer)
|
|
8698
|
+
*/
|
|
8699
|
+
setZoomState(isZooming) {
|
|
8700
|
+
this.zoomRenderer.setZoomState(isZooming);
|
|
8701
|
+
}
|
|
8702
|
+
/**
|
|
8703
|
+
* Set drag state for performance optimization (like setZoomState)
|
|
8704
|
+
*/
|
|
8705
|
+
setDragState(isDragging) {
|
|
8706
|
+
this.dragOptimizer.setDragState(isDragging);
|
|
8707
|
+
}
|
|
8708
|
+
/**
|
|
8709
|
+
* Set the currently dragged node
|
|
8710
|
+
*/
|
|
8711
|
+
setDraggedNode(draggedNode) {
|
|
8712
|
+
this.dragOptimizer.setDraggedNode(draggedNode);
|
|
8713
|
+
}
|
|
8714
|
+
/**
|
|
8715
|
+
* Render shadow canvas for hit detection (delegates to HitDetectionRenderer)
|
|
8716
|
+
*/
|
|
8717
|
+
renderShadowCanvas() {
|
|
8718
|
+
this.hitDetectionRenderer.renderShadowCanvas();
|
|
8719
|
+
}
|
|
8720
|
+
/**
|
|
8721
|
+
* Mark shadow canvas as dirty for next render (delegates to HitDetectionRenderer)
|
|
8722
|
+
*/
|
|
8723
|
+
markShadowCanvasDirty() {
|
|
8724
|
+
this.hitDetectionRenderer.markShadowCanvasDirty();
|
|
8725
|
+
}
|
|
8726
|
+
/**
|
|
8727
|
+
* Force shadow canvas render (delegates to HitDetectionRenderer)
|
|
8728
|
+
*/
|
|
8729
|
+
forceShadowCanvasRender() {
|
|
8730
|
+
this.hitDetectionRenderer.forceShadowCanvasRender();
|
|
8731
|
+
}
|
|
8732
|
+
/**
|
|
8733
|
+
* Clear canvas context
|
|
8734
|
+
*/
|
|
8735
|
+
clearCanvas(ctx) {
|
|
8736
|
+
if (!this.canvasState) return;
|
|
8737
|
+
try {
|
|
8738
|
+
const { width, height } = this.canvasState;
|
|
8739
|
+
CanvasUtils.resetTransform(ctx);
|
|
8740
|
+
ctx.clearRect(0, 0, width, height);
|
|
8741
|
+
} catch {
|
|
8742
|
+
throw new RenderError("Failed to clear canvas");
|
|
8743
|
+
}
|
|
8744
|
+
}
|
|
8745
|
+
/**
|
|
8746
|
+
* Apply transform to canvas context
|
|
8747
|
+
*/
|
|
8748
|
+
applyTransform(transform2, ctx) {
|
|
8749
|
+
try {
|
|
8750
|
+
CanvasUtils.resetTransform(ctx);
|
|
8751
|
+
ctx.translate(transform2.x, transform2.y);
|
|
8752
|
+
ctx.scale(transform2.k, transform2.k);
|
|
8753
|
+
} catch {
|
|
8754
|
+
throw new RenderError("Failed to apply transform");
|
|
8755
|
+
}
|
|
8756
|
+
}
|
|
8757
|
+
/**
|
|
8758
|
+
* Get unique link ID for tracking (delegates to StateManager)
|
|
8759
|
+
*/
|
|
8760
|
+
getLinkId(link) {
|
|
8761
|
+
return this.stateManager.getLinkId(link);
|
|
8762
|
+
}
|
|
8763
|
+
/**
|
|
8764
|
+
* Reset performance metrics
|
|
8765
|
+
*/
|
|
8766
|
+
resetPerformanceMetrics() {
|
|
8767
|
+
this.metricsManager.reset();
|
|
8768
|
+
}
|
|
8769
|
+
/**
|
|
8770
|
+
* Get current performance metrics
|
|
7107
8771
|
*/
|
|
7108
|
-
|
|
7109
|
-
|
|
7110
|
-
const sourceY = source.y ?? 0;
|
|
7111
|
-
const targetX = target.x ?? 0;
|
|
7112
|
-
const targetY = target.y ?? 0;
|
|
7113
|
-
const dx = targetX - sourceX;
|
|
7114
|
-
const dy = targetY - sourceY;
|
|
7115
|
-
const distance = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
7116
|
-
const targetNodeStyle = this.styleResolver.resolveNodeStyle({
|
|
7117
|
-
node: target,
|
|
7118
|
-
isHovered: this.isNodeHovered(target.id),
|
|
7119
|
-
isSelected: this.isNodeSelected(target.id)
|
|
7120
|
-
});
|
|
7121
|
-
const visualRadius = targetNodeStyle.radius + targetNodeStyle.strokeWidth / 2;
|
|
7122
|
-
const arrowLength = style.arrow?.enabled ? style.arrow.size ?? 4 : 0;
|
|
7123
|
-
const offset = style.arrow?.enabled ? visualRadius + arrowLength : visualRadius + 1;
|
|
7124
|
-
const shortenedPoint = {
|
|
7125
|
-
x: targetX - dx / distance * offset,
|
|
7126
|
-
y: targetY - dy / distance * offset
|
|
7127
|
-
};
|
|
7128
|
-
return shortenedPoint;
|
|
8772
|
+
getPerformanceMetrics() {
|
|
8773
|
+
return this.metricsManager.getMetrics();
|
|
7129
8774
|
}
|
|
7130
8775
|
/**
|
|
7131
|
-
*
|
|
8776
|
+
* Force log performance metrics immediately (for debugging)
|
|
7132
8777
|
*/
|
|
7133
|
-
|
|
7134
|
-
|
|
7135
|
-
|
|
7136
|
-
|
|
7137
|
-
const angle = Math.atan2(dy, dx);
|
|
7138
|
-
const arrowLength = arrowStyle.size ?? 4;
|
|
7139
|
-
const arrowTipX = targetPoint.x + arrowLength * Math.cos(angle);
|
|
7140
|
-
const arrowTipY = targetPoint.y + arrowLength * Math.sin(angle);
|
|
7141
|
-
const x1 = arrowTipX - arrowLength * Math.cos(angle - Math.PI / 6);
|
|
7142
|
-
const y1 = arrowTipY - arrowLength * Math.sin(angle - Math.PI / 6);
|
|
7143
|
-
const x22 = arrowTipX - arrowLength * Math.cos(angle + Math.PI / 6);
|
|
7144
|
-
const y22 = arrowTipY - arrowLength * Math.sin(angle + Math.PI / 6);
|
|
7145
|
-
ctx.fillStyle = arrowStyle.fill ?? "#000000";
|
|
7146
|
-
ctx.beginPath();
|
|
7147
|
-
ctx.moveTo(arrowTipX, arrowTipY);
|
|
7148
|
-
ctx.lineTo(x1, y1);
|
|
7149
|
-
ctx.lineTo(x22, y22);
|
|
7150
|
-
ctx.closePath();
|
|
7151
|
-
ctx.fill();
|
|
7152
|
-
} catch (error) {
|
|
7153
|
-
ErrorHandler.logError(error);
|
|
7154
|
-
}
|
|
8778
|
+
forceLogMetrics() {
|
|
8779
|
+
const nodeCount = this.config?.nodes.length || 0;
|
|
8780
|
+
const linkCount = this.config?.links.length || 0;
|
|
8781
|
+
this.metricsManager.logMetrics(nodeCount, linkCount);
|
|
7155
8782
|
}
|
|
7156
8783
|
/**
|
|
7157
|
-
*
|
|
8784
|
+
* Calculate midpoint of a link for label positioning (delegates to StateManager)
|
|
7158
8785
|
*/
|
|
7159
|
-
|
|
7160
|
-
this.
|
|
7161
|
-
ctx,
|
|
7162
|
-
{ x: source.x, y: source.y },
|
|
7163
|
-
{ x: target.x, y: target.y },
|
|
7164
|
-
arrowStyle
|
|
7165
|
-
);
|
|
8786
|
+
getLinkMidpoint(link) {
|
|
8787
|
+
return this.stateManager.getLinkMidpoint(link);
|
|
7166
8788
|
}
|
|
7167
8789
|
/**
|
|
7168
8790
|
* Initialize node positions if needed
|
|
@@ -7187,6 +8809,29 @@ var Renderer = class {
|
|
|
7187
8809
|
if (!this.config) return;
|
|
7188
8810
|
try {
|
|
7189
8811
|
Object.assign(this.config, updates);
|
|
8812
|
+
this.zIndexRenderer.updateConfig({
|
|
8813
|
+
nodes: this.config.nodes,
|
|
8814
|
+
links: this.config.links
|
|
8815
|
+
});
|
|
8816
|
+
this.stateManager.updateState({ nodes: this.config.nodes, links: this.config.links });
|
|
8817
|
+
this.zoomRenderer.updateConfig({
|
|
8818
|
+
nodes: this.config.nodes,
|
|
8819
|
+
links: this.config.links,
|
|
8820
|
+
stateManager: this.stateManager
|
|
8821
|
+
});
|
|
8822
|
+
this.hitDetectionRenderer.updateConfig({
|
|
8823
|
+
nodes: this.config.nodes,
|
|
8824
|
+
links: this.config.links,
|
|
8825
|
+
stateManager: this.stateManager
|
|
8826
|
+
});
|
|
8827
|
+
this.dragOptimizer.updateConfig({
|
|
8828
|
+
nodes: this.config.nodes,
|
|
8829
|
+
links: this.config.links,
|
|
8830
|
+
stateManager: this.stateManager,
|
|
8831
|
+
styleResolver: this.styleResolver,
|
|
8832
|
+
interactionResolver: this.interactionResolver,
|
|
8833
|
+
zIndexRenderer: this.zIndexRenderer
|
|
8834
|
+
});
|
|
7190
8835
|
} catch (error) {
|
|
7191
8836
|
ErrorHandler.logError(error);
|
|
7192
8837
|
}
|
|
@@ -7209,247 +8854,22 @@ var Renderer = class {
|
|
|
7209
8854
|
};
|
|
7210
8855
|
}
|
|
7211
8856
|
/**
|
|
7212
|
-
* Debug shadow canvas export (
|
|
8857
|
+
* Debug shadow canvas export (delegates to HitDetectionRenderer)
|
|
7213
8858
|
*/
|
|
7214
8859
|
debugShadowCanvas() {
|
|
7215
|
-
|
|
7216
|
-
if (!this.canvasState) return;
|
|
7217
|
-
const { shadowCanvas } = this.canvasState;
|
|
7218
|
-
const link = document.createElement("a");
|
|
7219
|
-
link.download = "shadow-canvas-debug.png";
|
|
7220
|
-
link.href = shadowCanvas.toDataURL("image/png");
|
|
7221
|
-
link.click();
|
|
7222
|
-
} catch (error) {
|
|
7223
|
-
ErrorHandler.logError(error);
|
|
7224
|
-
}
|
|
7225
|
-
}
|
|
7226
|
-
/**
|
|
7227
|
-
* Build node index for O(1) lookups (Step 3 optimization)
|
|
7228
|
-
*/
|
|
7229
|
-
buildNodeIndex() {
|
|
7230
|
-
if (!this.config) return;
|
|
7231
|
-
try {
|
|
7232
|
-
this.nodeMap.clear();
|
|
7233
|
-
for (const node of this.config.nodes) {
|
|
7234
|
-
this.nodeMap.set(node.id, node);
|
|
7235
|
-
}
|
|
7236
|
-
for (const link of this.config.links) {
|
|
7237
|
-
if (typeof link.source === "string") {
|
|
7238
|
-
const sourceNode = this.nodeMap.get(link.source);
|
|
7239
|
-
if (sourceNode) {
|
|
7240
|
-
link.source = sourceNode;
|
|
7241
|
-
}
|
|
7242
|
-
}
|
|
7243
|
-
if (typeof link.target === "string") {
|
|
7244
|
-
const targetNode = this.nodeMap.get(link.target);
|
|
7245
|
-
if (targetNode) {
|
|
7246
|
-
link.target = targetNode;
|
|
7247
|
-
}
|
|
7248
|
-
}
|
|
7249
|
-
}
|
|
7250
|
-
} catch (error) {
|
|
7251
|
-
ErrorHandler.logError(error);
|
|
7252
|
-
}
|
|
7253
|
-
}
|
|
7254
|
-
/**
|
|
7255
|
-
* Get node by ID using O(1) lookup (Step 3 optimization)
|
|
7256
|
-
*/
|
|
7257
|
-
getNodeById(nodeId) {
|
|
7258
|
-
return this.nodeMap.get(nodeId);
|
|
7259
|
-
}
|
|
7260
|
-
/**
|
|
7261
|
-
* Render with z-index layers (for renderWithTransform)
|
|
7262
|
-
*/
|
|
7263
|
-
renderWithLayers(ctx) {
|
|
7264
|
-
if (!this.config) return;
|
|
7265
|
-
try {
|
|
7266
|
-
const hoveredNode = this.getHoveredNode();
|
|
7267
|
-
const selectedNode = this.getSelectedNode();
|
|
7268
|
-
const hoveredLink = this.getHoveredLink();
|
|
7269
|
-
const selectedLink = this.getSelectedLink();
|
|
7270
|
-
const nodeHighlightChecker = ZIndexManager.createNodeHighlightChecker(
|
|
7271
|
-
hoveredNode?.id || null,
|
|
7272
|
-
selectedNode?.id || null
|
|
7273
|
-
);
|
|
7274
|
-
const linkHighlightChecker = ZIndexManager.createLinkHighlightChecker(
|
|
7275
|
-
hoveredNode?.id || null,
|
|
7276
|
-
selectedNode?.id || null,
|
|
7277
|
-
hoveredLink ? this.getLinkId(hoveredLink) : null,
|
|
7278
|
-
selectedLink ? this.getLinkId(selectedLink) : null
|
|
7279
|
-
);
|
|
7280
|
-
const linkLayers = ZIndexManager.separateIntoLayers(this.config.links, linkHighlightChecker);
|
|
7281
|
-
const nodeLayers = ZIndexManager.separateIntoLayers(this.config.nodes, nodeHighlightChecker);
|
|
7282
|
-
this.renderLinksLayer(ctx, linkLayers.background);
|
|
7283
|
-
this.renderLinkLabelsLayer(ctx, linkLayers.background);
|
|
7284
|
-
this.renderNodesWithLabelsLayer(ctx, nodeLayers.background);
|
|
7285
|
-
this.renderLinksLayer(ctx, linkLayers.foreground);
|
|
7286
|
-
this.renderLinkLabelsLayer(ctx, linkLayers.foreground);
|
|
7287
|
-
this.renderNodesWithLabelsLayer(ctx, nodeLayers.foreground);
|
|
7288
|
-
} catch (error) {
|
|
7289
|
-
ErrorHandler.logError(error);
|
|
7290
|
-
}
|
|
7291
|
-
}
|
|
7292
|
-
/**
|
|
7293
|
-
* Render with z-index layers and performance metrics (for main render)
|
|
7294
|
-
*/
|
|
7295
|
-
renderWithLayersAndMetrics(ctx) {
|
|
7296
|
-
if (!this.config) return;
|
|
7297
|
-
try {
|
|
7298
|
-
const hoveredNode = this.getHoveredNode();
|
|
7299
|
-
const selectedNode = this.getSelectedNode();
|
|
7300
|
-
const hoveredLink = this.getHoveredLink();
|
|
7301
|
-
const selectedLink = this.getSelectedLink();
|
|
7302
|
-
const nodeHighlightChecker = ZIndexManager.createNodeHighlightChecker(
|
|
7303
|
-
hoveredNode?.id || null,
|
|
7304
|
-
selectedNode?.id || null
|
|
7305
|
-
);
|
|
7306
|
-
const linkHighlightChecker = ZIndexManager.createLinkHighlightChecker(
|
|
7307
|
-
hoveredNode?.id || null,
|
|
7308
|
-
selectedNode?.id || null,
|
|
7309
|
-
hoveredLink ? this.getLinkId(hoveredLink) : null,
|
|
7310
|
-
selectedLink ? this.getLinkId(selectedLink) : null
|
|
7311
|
-
);
|
|
7312
|
-
const linkLayers = ZIndexManager.separateIntoLayers(this.config.links, linkHighlightChecker);
|
|
7313
|
-
const nodeLayers = ZIndexManager.separateIntoLayers(this.config.nodes, nodeHighlightChecker);
|
|
7314
|
-
let stepStart = performance.now();
|
|
7315
|
-
this.renderLinksLayer(ctx, linkLayers.background);
|
|
7316
|
-
this.renderLinksLayer(ctx, linkLayers.foreground);
|
|
7317
|
-
this.performanceMetrics.renderLinks += performance.now() - stepStart;
|
|
7318
|
-
stepStart = performance.now();
|
|
7319
|
-
this.renderNodesLayer(ctx, nodeLayers.background);
|
|
7320
|
-
this.renderNodesLayer(ctx, nodeLayers.foreground);
|
|
7321
|
-
this.performanceMetrics.renderNodes += performance.now() - stepStart;
|
|
7322
|
-
stepStart = performance.now();
|
|
7323
|
-
this.renderLinkLabelsLayer(ctx, linkLayers.background);
|
|
7324
|
-
this.renderLinkLabelsLayer(ctx, linkLayers.foreground);
|
|
7325
|
-
this.performanceMetrics.renderLinkLabels += performance.now() - stepStart;
|
|
7326
|
-
stepStart = performance.now();
|
|
7327
|
-
this.renderNodeLabelsLayer(ctx, nodeLayers.background);
|
|
7328
|
-
this.renderNodeLabelsLayer(ctx, nodeLayers.foreground);
|
|
7329
|
-
this.performanceMetrics.renderNodeLabels += performance.now() - stepStart;
|
|
7330
|
-
} catch (error) {
|
|
7331
|
-
ErrorHandler.logError(error);
|
|
7332
|
-
}
|
|
7333
|
-
}
|
|
7334
|
-
/**
|
|
7335
|
-
* Helper methods for getting current interaction states
|
|
7336
|
-
*/
|
|
7337
|
-
getHoveredNode() {
|
|
7338
|
-
if (!this.hoverManager) return null;
|
|
7339
|
-
const hoverState = this.hoverManager.getHoverState();
|
|
7340
|
-
return hoverState.currentHovered?.d.entityType === "Node" ? hoverState.currentHovered.d : null;
|
|
7341
|
-
}
|
|
7342
|
-
getSelectedNode() {
|
|
7343
|
-
if (!this.selectionManager) return null;
|
|
7344
|
-
const selectionState = this.selectionManager.getSelectionState();
|
|
7345
|
-
return selectionState.selectedNode;
|
|
7346
|
-
}
|
|
7347
|
-
getHoveredLink() {
|
|
7348
|
-
if (!this.hoverManager) return null;
|
|
7349
|
-
const hoverState = this.hoverManager.getHoverState();
|
|
7350
|
-
return hoverState.currentHovered?.d.entityType === "Link" ? hoverState.currentHovered.d : null;
|
|
7351
|
-
}
|
|
7352
|
-
getSelectedLink() {
|
|
7353
|
-
if (!this.selectionManager) return null;
|
|
7354
|
-
const selectionState = this.selectionManager.getSelectionState();
|
|
7355
|
-
return selectionState.selectedLink;
|
|
7356
|
-
}
|
|
7357
|
-
/**
|
|
7358
|
-
* Layer-specific rendering methods (render subsets of entities)
|
|
7359
|
-
*/
|
|
7360
|
-
renderNodesWithLabelsLayer(ctx, nodes) {
|
|
7361
|
-
if (!this.config || !this.styleResolver) return;
|
|
7362
|
-
const nodeCount = this.config.nodes.length;
|
|
7363
|
-
const isLargeGraph = nodeCount > 1e4;
|
|
7364
|
-
let currentZoom = 1;
|
|
7365
|
-
let isZoomedOutForNodes = false;
|
|
7366
|
-
if (isLargeGraph) {
|
|
7367
|
-
currentZoom = this.canvasState ? transform(this.canvasState.canvas).k : 1;
|
|
7368
|
-
isZoomedOutForNodes = currentZoom <= 0.8;
|
|
7369
|
-
}
|
|
7370
|
-
const nodeStateCache = /* @__PURE__ */ new Map();
|
|
7371
|
-
for (const node of nodes) {
|
|
7372
|
-
if (!node.x || !node.y) continue;
|
|
7373
|
-
const nodeId = node.id;
|
|
7374
|
-
let nodeState = nodeStateCache.get(nodeId);
|
|
7375
|
-
if (!nodeState) {
|
|
7376
|
-
nodeState = {
|
|
7377
|
-
isHovered: this.isNodeHovered(nodeId),
|
|
7378
|
-
isSelected: this.isNodeSelected(nodeId)
|
|
7379
|
-
};
|
|
7380
|
-
nodeStateCache.set(nodeId, nodeState);
|
|
7381
|
-
}
|
|
7382
|
-
const nodeStyle = this.styleResolver.resolveNodeStyle({
|
|
7383
|
-
node,
|
|
7384
|
-
isHovered: nodeState.isHovered,
|
|
7385
|
-
isSelected: nodeState.isSelected
|
|
7386
|
-
});
|
|
7387
|
-
ctx.beginPath();
|
|
7388
|
-
ctx.arc(node.x, node.y, nodeStyle.radius, 0, 2 * Math.PI);
|
|
7389
|
-
ctx.fillStyle = nodeStyle.fill;
|
|
7390
|
-
ctx.fill();
|
|
7391
|
-
if (nodeStyle.stroke && nodeStyle.strokeWidth > 0) {
|
|
7392
|
-
ctx.strokeStyle = nodeStyle.stroke;
|
|
7393
|
-
ctx.lineWidth = nodeStyle.strokeWidth;
|
|
7394
|
-
ctx.stroke();
|
|
7395
|
-
}
|
|
7396
|
-
if (isLargeGraph && isZoomedOutForNodes) {
|
|
7397
|
-
continue;
|
|
7398
|
-
}
|
|
7399
|
-
const nodeStyleWithLabel = nodeStyle;
|
|
7400
|
-
if (nodeStyleWithLabel.label && !nodeStyleWithLabel.label.enabled) continue;
|
|
7401
|
-
const fullLabel = node.label || node.id;
|
|
7402
|
-
const defaultStyle = {
|
|
7403
|
-
font: "9px sans-serif",
|
|
7404
|
-
textAlign: "center",
|
|
7405
|
-
textBaseline: "middle",
|
|
7406
|
-
fillStyle: "#ffffff",
|
|
7407
|
-
offsetY: 0
|
|
7408
|
-
};
|
|
7409
|
-
if (nodeStyleWithLabel.label) {
|
|
7410
|
-
ctx.font = nodeStyleWithLabel.label.font || defaultStyle.font;
|
|
7411
|
-
ctx.textAlign = nodeStyleWithLabel.label.textAlign || defaultStyle.textAlign;
|
|
7412
|
-
ctx.textBaseline = nodeStyleWithLabel.label.textBaseline || defaultStyle.textBaseline;
|
|
7413
|
-
ctx.fillStyle = nodeStyleWithLabel.label.textColor || defaultStyle.fillStyle;
|
|
7414
|
-
} else {
|
|
7415
|
-
ctx.font = defaultStyle.font;
|
|
7416
|
-
ctx.textAlign = defaultStyle.textAlign;
|
|
7417
|
-
ctx.textBaseline = defaultStyle.textBaseline;
|
|
7418
|
-
ctx.fillStyle = defaultStyle.fillStyle;
|
|
7419
|
-
}
|
|
7420
|
-
const maxWidth = nodeStyle.radius * 2 - 6;
|
|
7421
|
-
const truncatedLabel = this.truncateLabel(ctx, fullLabel, maxWidth);
|
|
7422
|
-
const labelY = node.y + (nodeStyleWithLabel.label?.offsetY || defaultStyle.offsetY);
|
|
7423
|
-
ctx.fillText(truncatedLabel, node.x, labelY);
|
|
7424
|
-
}
|
|
7425
|
-
}
|
|
7426
|
-
/**
|
|
7427
|
-
* Helper method to truncate labels (copied from NodeLabelsRenderer)
|
|
7428
|
-
*/
|
|
7429
|
-
truncateLabel(ctx, label, maxWidth) {
|
|
7430
|
-
let truncatedLabel = label;
|
|
7431
|
-
if (ctx.measureText(truncatedLabel).width <= maxWidth) {
|
|
7432
|
-
return truncatedLabel;
|
|
7433
|
-
}
|
|
7434
|
-
while (truncatedLabel.length > 1 && ctx.measureText(`${truncatedLabel}\u2026`).width > maxWidth) {
|
|
7435
|
-
truncatedLabel = truncatedLabel.slice(0, -1);
|
|
7436
|
-
}
|
|
7437
|
-
return truncatedLabel.length < label.length ? `${truncatedLabel}\u2026` : truncatedLabel;
|
|
8860
|
+
this.hitDetectionRenderer.debugShadowCanvas();
|
|
7438
8861
|
}
|
|
7439
8862
|
renderLinksLayer(ctx, links) {
|
|
7440
8863
|
if (!this.config || !this.styleResolver) return;
|
|
7441
8864
|
const linkStateCache = /* @__PURE__ */ new Map();
|
|
7442
8865
|
for (const link of links) {
|
|
7443
|
-
const sourceNode = typeof link.source === "string" ? this.
|
|
7444
|
-
const targetNode = typeof link.target === "string" ? this.
|
|
8866
|
+
const sourceNode = typeof link.source === "string" ? this.stateManager.getNode(link.source) : link.source;
|
|
8867
|
+
const targetNode = typeof link.target === "string" ? this.stateManager.getNode(link.target) : link.target;
|
|
7445
8868
|
if (sourceNode && targetNode && sourceNode.x && sourceNode.y && targetNode.x && targetNode.y) {
|
|
7446
8869
|
const linkId = this.getLinkId(link);
|
|
7447
8870
|
let linkState = linkStateCache.get(linkId);
|
|
7448
8871
|
if (!linkState) {
|
|
7449
|
-
linkState =
|
|
7450
|
-
isHovered: this.isLinkHovered(link),
|
|
7451
|
-
isSelected: this.isLinkSelected(link)
|
|
7452
|
-
};
|
|
8872
|
+
linkState = this.interactionResolver.getLinkState(link);
|
|
7453
8873
|
linkStateCache.set(linkId, linkState);
|
|
7454
8874
|
}
|
|
7455
8875
|
const style = this.styleResolver.resolveLinkStyle({
|
|
@@ -7457,85 +8877,18 @@ var Renderer = class {
|
|
|
7457
8877
|
isHovered: linkState.isHovered,
|
|
7458
8878
|
isSelected: linkState.isSelected
|
|
7459
8879
|
});
|
|
7460
|
-
this.
|
|
7461
|
-
|
|
7462
|
-
|
|
7463
|
-
|
|
7464
|
-
|
|
7465
|
-
|
|
7466
|
-
|
|
7467
|
-
|
|
7468
|
-
|
|
7469
|
-
|
|
7470
|
-
this.hasLoggedLargeGraphOptimization = true;
|
|
7471
|
-
}
|
|
7472
|
-
if (isLargeGraph) {
|
|
7473
|
-
const currentZoom = this.canvasState ? transform(this.canvasState.canvas).k : 1;
|
|
7474
|
-
const isZoomedOut = currentZoom <= 1;
|
|
7475
|
-
if (isZoomedOut) {
|
|
7476
|
-
return;
|
|
8880
|
+
const callbacks = this.interactionResolver.createCallbacks();
|
|
8881
|
+
LinkRenderer.renderDirectedLink(
|
|
8882
|
+
ctx,
|
|
8883
|
+
sourceNode,
|
|
8884
|
+
targetNode,
|
|
8885
|
+
style,
|
|
8886
|
+
this.styleResolver,
|
|
8887
|
+
callbacks.isNodeHovered,
|
|
8888
|
+
callbacks.isNodeSelected
|
|
8889
|
+
);
|
|
7477
8890
|
}
|
|
7478
8891
|
}
|
|
7479
|
-
const linkStateCache = /* @__PURE__ */ new Map();
|
|
7480
|
-
LinkLabelsRenderer.renderWithVisibility(
|
|
7481
|
-
ctx,
|
|
7482
|
-
links,
|
|
7483
|
-
(link) => {
|
|
7484
|
-
if (!link.label) {
|
|
7485
|
-
return null;
|
|
7486
|
-
}
|
|
7487
|
-
const linkId = this.getLinkId(link);
|
|
7488
|
-
let linkState = linkStateCache.get(linkId);
|
|
7489
|
-
if (!linkState) {
|
|
7490
|
-
linkState = {
|
|
7491
|
-
isHovered: this.isLinkHovered(link),
|
|
7492
|
-
isSelected: this.isLinkSelected(link)
|
|
7493
|
-
};
|
|
7494
|
-
linkStateCache.set(linkId, linkState);
|
|
7495
|
-
}
|
|
7496
|
-
if (isLargeGraph) {
|
|
7497
|
-
const isInteractive = linkState.isHovered || linkState.isSelected;
|
|
7498
|
-
if (!isInteractive) {
|
|
7499
|
-
return null;
|
|
7500
|
-
}
|
|
7501
|
-
}
|
|
7502
|
-
const style = this.styleResolver.resolveLinkStyle({
|
|
7503
|
-
link,
|
|
7504
|
-
isHovered: linkState.isHovered,
|
|
7505
|
-
isSelected: linkState.isSelected
|
|
7506
|
-
});
|
|
7507
|
-
return style.label || null;
|
|
7508
|
-
},
|
|
7509
|
-
(link) => this.getLinkMidpoint(link),
|
|
7510
|
-
(linkId) => {
|
|
7511
|
-
let linkState = linkStateCache.get(linkId);
|
|
7512
|
-
if (!linkState) {
|
|
7513
|
-
const link = links.find((l) => this.getLinkId(l) === linkId);
|
|
7514
|
-
if (link) {
|
|
7515
|
-
linkState = {
|
|
7516
|
-
isHovered: this.isLinkHovered(link),
|
|
7517
|
-
isSelected: this.isLinkSelected(link)
|
|
7518
|
-
};
|
|
7519
|
-
linkStateCache.set(linkId, linkState);
|
|
7520
|
-
}
|
|
7521
|
-
}
|
|
7522
|
-
return linkState?.isHovered || false;
|
|
7523
|
-
},
|
|
7524
|
-
(linkId) => {
|
|
7525
|
-
let linkState = linkStateCache.get(linkId);
|
|
7526
|
-
if (!linkState) {
|
|
7527
|
-
const link = links.find((l) => this.getLinkId(l) === linkId);
|
|
7528
|
-
if (link) {
|
|
7529
|
-
linkState = {
|
|
7530
|
-
isHovered: this.isLinkHovered(link),
|
|
7531
|
-
isSelected: this.isLinkSelected(link)
|
|
7532
|
-
};
|
|
7533
|
-
linkStateCache.set(linkId, linkState);
|
|
7534
|
-
}
|
|
7535
|
-
}
|
|
7536
|
-
return linkState?.isSelected || false;
|
|
7537
|
-
}
|
|
7538
|
-
);
|
|
7539
8892
|
}
|
|
7540
8893
|
renderNodesLayer(ctx, nodes) {
|
|
7541
8894
|
if (!this.config || !this.styleResolver) return;
|
|
@@ -7547,9 +8900,11 @@ var Renderer = class {
|
|
|
7547
8900
|
(nodeId) => {
|
|
7548
8901
|
let nodeState = nodeStateCache.get(nodeId);
|
|
7549
8902
|
if (!nodeState) {
|
|
8903
|
+
const interactionState = this.interactionResolver.getNodeState(nodeId);
|
|
7550
8904
|
nodeState = {
|
|
7551
|
-
isHovered:
|
|
7552
|
-
isSelected:
|
|
8905
|
+
isHovered: interactionState.isHovered,
|
|
8906
|
+
isSelected: interactionState.isSelected,
|
|
8907
|
+
isHighlighted: this.stateManager.isNodeHighlighted(nodeId)
|
|
7553
8908
|
};
|
|
7554
8909
|
nodeStateCache.set(nodeId, nodeState);
|
|
7555
8910
|
}
|
|
@@ -7558,13 +8913,28 @@ var Renderer = class {
|
|
|
7558
8913
|
(nodeId) => {
|
|
7559
8914
|
let nodeState = nodeStateCache.get(nodeId);
|
|
7560
8915
|
if (!nodeState) {
|
|
8916
|
+
const interactionState = this.interactionResolver.getNodeState(nodeId);
|
|
7561
8917
|
nodeState = {
|
|
7562
|
-
isHovered:
|
|
7563
|
-
isSelected:
|
|
8918
|
+
isHovered: interactionState.isHovered,
|
|
8919
|
+
isSelected: interactionState.isSelected,
|
|
8920
|
+
isHighlighted: this.stateManager.isNodeHighlighted(nodeId)
|
|
7564
8921
|
};
|
|
7565
8922
|
nodeStateCache.set(nodeId, nodeState);
|
|
7566
8923
|
}
|
|
7567
8924
|
return nodeState.isSelected;
|
|
8925
|
+
},
|
|
8926
|
+
(nodeId) => {
|
|
8927
|
+
let nodeState = nodeStateCache.get(nodeId);
|
|
8928
|
+
if (!nodeState) {
|
|
8929
|
+
const interactionState = this.interactionResolver.getNodeState(nodeId);
|
|
8930
|
+
nodeState = {
|
|
8931
|
+
isHovered: interactionState.isHovered,
|
|
8932
|
+
isSelected: interactionState.isSelected,
|
|
8933
|
+
isHighlighted: this.stateManager.isNodeHighlighted(nodeId)
|
|
8934
|
+
};
|
|
8935
|
+
nodeStateCache.set(nodeId, nodeState);
|
|
8936
|
+
}
|
|
8937
|
+
return nodeState.isHighlighted;
|
|
7568
8938
|
}
|
|
7569
8939
|
);
|
|
7570
8940
|
}
|
|
@@ -7587,10 +8957,7 @@ var Renderer = class {
|
|
|
7587
8957
|
(nodeId) => {
|
|
7588
8958
|
let nodeState = nodeStateCache.get(nodeId);
|
|
7589
8959
|
if (!nodeState) {
|
|
7590
|
-
nodeState =
|
|
7591
|
-
isHovered: this.isNodeHovered(nodeId),
|
|
7592
|
-
isSelected: this.isNodeSelected(nodeId)
|
|
7593
|
-
};
|
|
8960
|
+
nodeState = this.interactionResolver.getNodeState(nodeId);
|
|
7594
8961
|
nodeStateCache.set(nodeId, nodeState);
|
|
7595
8962
|
}
|
|
7596
8963
|
return nodeState.isHovered;
|
|
@@ -7598,28 +8965,33 @@ var Renderer = class {
|
|
|
7598
8965
|
(nodeId) => {
|
|
7599
8966
|
let nodeState = nodeStateCache.get(nodeId);
|
|
7600
8967
|
if (!nodeState) {
|
|
7601
|
-
nodeState =
|
|
7602
|
-
isHovered: this.isNodeHovered(nodeId),
|
|
7603
|
-
isSelected: this.isNodeSelected(nodeId)
|
|
7604
|
-
};
|
|
8968
|
+
nodeState = this.interactionResolver.getNodeState(nodeId);
|
|
7605
8969
|
nodeStateCache.set(nodeId, nodeState);
|
|
7606
8970
|
}
|
|
7607
8971
|
return nodeState.isSelected;
|
|
7608
8972
|
}
|
|
7609
8973
|
);
|
|
7610
8974
|
}
|
|
8975
|
+
/**
|
|
8976
|
+
* Get the state manager for highlight operations
|
|
8977
|
+
*/
|
|
8978
|
+
getStateManager() {
|
|
8979
|
+
return this.stateManager;
|
|
8980
|
+
}
|
|
7611
8981
|
/**
|
|
7612
8982
|
* Destroy renderer and clean up resources
|
|
7613
8983
|
*/
|
|
7614
8984
|
destroy() {
|
|
7615
8985
|
try {
|
|
8986
|
+
this.zIndexRenderer.destroy();
|
|
8987
|
+
this.zoomRenderer.destroy();
|
|
8988
|
+
this.hitDetectionRenderer.destroy();
|
|
8989
|
+
this.dragOptimizer.destroy();
|
|
7616
8990
|
this.config = void 0;
|
|
7617
8991
|
this.canvasState = void 0;
|
|
7618
8992
|
this.hoverManager = void 0;
|
|
7619
8993
|
this.styleResolver = void 0;
|
|
7620
|
-
this.
|
|
7621
|
-
this.shadowCanvasDirty = false;
|
|
7622
|
-
this.lastShadowRenderTime = 0;
|
|
8994
|
+
this.stateManager.destroy();
|
|
7623
8995
|
} catch (error) {
|
|
7624
8996
|
ErrorHandler.logError(error);
|
|
7625
8997
|
}
|
|
@@ -8070,13 +9442,22 @@ var V2Graph = class {
|
|
|
8070
9442
|
// pointerManager: this.pointerManager,
|
|
8071
9443
|
physicsManager: this.physicsManager,
|
|
8072
9444
|
hoverManager: this.hoverManager,
|
|
9445
|
+
renderer: this.renderer,
|
|
9446
|
+
// Pass renderer for drag state management
|
|
8073
9447
|
onRender: () => this.renderer.renderWithTransform()
|
|
8074
9448
|
});
|
|
8075
9449
|
this.coordinateDragHover();
|
|
8076
9450
|
this.zoomManager.initialize({
|
|
8077
9451
|
canvas: canvasState.canvas,
|
|
8078
9452
|
canvasManager: this.canvasManager,
|
|
8079
|
-
|
|
9453
|
+
renderer: this.renderer,
|
|
9454
|
+
// Pass renderer for zoom state management
|
|
9455
|
+
onRender: () => {
|
|
9456
|
+
this.renderer.renderWithTransform();
|
|
9457
|
+
},
|
|
9458
|
+
onZoomEnd: () => {
|
|
9459
|
+
this.renderer.renderWithTransform();
|
|
9460
|
+
},
|
|
8080
9461
|
isOverEntity: () => {
|
|
8081
9462
|
const hoverState = this.hoverManager.getHoverState();
|
|
8082
9463
|
return hoverState.currentHovered !== null;
|
|
@@ -8588,6 +9969,76 @@ var V2Graph = class {
|
|
|
8588
9969
|
}
|
|
8589
9970
|
});
|
|
8590
9971
|
}
|
|
9972
|
+
/**
|
|
9973
|
+
* Highlight a node by ID
|
|
9974
|
+
*/
|
|
9975
|
+
highlightNode(nodeId) {
|
|
9976
|
+
try {
|
|
9977
|
+
if (!this.config) {
|
|
9978
|
+
throw new ValidationError("Graph not initialized");
|
|
9979
|
+
}
|
|
9980
|
+
this.renderer.getStateManager().highlightNode(nodeId);
|
|
9981
|
+
this.renderer.renderWithTransform();
|
|
9982
|
+
} catch (error) {
|
|
9983
|
+
ErrorHandler.logError(error, { nodeId });
|
|
9984
|
+
}
|
|
9985
|
+
}
|
|
9986
|
+
/**
|
|
9987
|
+
* Highlight multiple nodes by IDs
|
|
9988
|
+
*/
|
|
9989
|
+
highlightNodes(nodeIds) {
|
|
9990
|
+
try {
|
|
9991
|
+
if (!this.config) {
|
|
9992
|
+
throw new ValidationError("Graph not initialized");
|
|
9993
|
+
}
|
|
9994
|
+
this.renderer.getStateManager().highlightNodes(nodeIds);
|
|
9995
|
+
this.renderer.renderWithTransform();
|
|
9996
|
+
} catch (error) {
|
|
9997
|
+
ErrorHandler.logError(error, { nodeIds });
|
|
9998
|
+
}
|
|
9999
|
+
}
|
|
10000
|
+
/**
|
|
10001
|
+
* Remove highlight from a node
|
|
10002
|
+
*/
|
|
10003
|
+
unhighlightNode(nodeId) {
|
|
10004
|
+
try {
|
|
10005
|
+
if (!this.config) {
|
|
10006
|
+
throw new ValidationError("Graph not initialized");
|
|
10007
|
+
}
|
|
10008
|
+
this.renderer.getStateManager().unhighlightNode(nodeId);
|
|
10009
|
+
this.renderer.renderWithTransform();
|
|
10010
|
+
} catch (error) {
|
|
10011
|
+
ErrorHandler.logError(error, { nodeId });
|
|
10012
|
+
}
|
|
10013
|
+
}
|
|
10014
|
+
/**
|
|
10015
|
+
* Clear all node highlights
|
|
10016
|
+
*/
|
|
10017
|
+
clearHighlights() {
|
|
10018
|
+
try {
|
|
10019
|
+
if (!this.config) {
|
|
10020
|
+
throw new ValidationError("Graph not initialized");
|
|
10021
|
+
}
|
|
10022
|
+
this.renderer.getStateManager().clearHighlights();
|
|
10023
|
+
this.renderer.renderWithTransform();
|
|
10024
|
+
} catch (error) {
|
|
10025
|
+
ErrorHandler.logError(error);
|
|
10026
|
+
}
|
|
10027
|
+
}
|
|
10028
|
+
/**
|
|
10029
|
+
* Get all highlighted node IDs
|
|
10030
|
+
*/
|
|
10031
|
+
getHighlightedNodes() {
|
|
10032
|
+
try {
|
|
10033
|
+
if (!this.config) {
|
|
10034
|
+
throw new ValidationError("Graph not initialized");
|
|
10035
|
+
}
|
|
10036
|
+
return this.renderer.getStateManager().getHighlightedNodes();
|
|
10037
|
+
} catch (error) {
|
|
10038
|
+
ErrorHandler.logError(error);
|
|
10039
|
+
return /* @__PURE__ */ new Set();
|
|
10040
|
+
}
|
|
10041
|
+
}
|
|
8591
10042
|
/**
|
|
8592
10043
|
* Destroy the graph and clean up resources
|
|
8593
10044
|
*/
|