polly-graph 0.2.4 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +2291 -894
- package/dist/index.d.cts +267 -126
- package/dist/index.d.ts +267 -126
- package/dist/index.js +2291 -894
- package/package.json +1 -1
package/dist/index.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,6 +1923,266 @@ 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;
|
|
@@ -2000,7 +2192,9 @@ var PhysicsManager = class {
|
|
|
2000
2192
|
hasInitialAutoFitCompleted = false;
|
|
2001
2193
|
timerManager;
|
|
2002
2194
|
isVisibilityListenerAttached = false;
|
|
2003
|
-
|
|
2195
|
+
stateManager = new StateManager();
|
|
2196
|
+
isWarmingUp = false;
|
|
2197
|
+
warmupSteps = 0;
|
|
2004
2198
|
constructor(timerManager) {
|
|
2005
2199
|
this.timerManager = timerManager;
|
|
2006
2200
|
}
|
|
@@ -2031,19 +2225,19 @@ var PhysicsManager = class {
|
|
|
2031
2225
|
if (this.simulation) {
|
|
2032
2226
|
this.simulation.stop();
|
|
2033
2227
|
}
|
|
2034
|
-
this.
|
|
2035
|
-
const linkDistance = nodeCount > 1e4 ?
|
|
2036
|
-
const
|
|
2037
|
-
const
|
|
2038
|
-
const
|
|
2039
|
-
const
|
|
2040
|
-
const
|
|
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;
|
|
2041
2235
|
this.simulation = simulation_default(config.nodes).force(
|
|
2042
2236
|
"link",
|
|
2043
2237
|
link_default(config.links).id((d) => d.id).distance(linkDistance).strength(linkStrength).iterations(1)
|
|
2044
2238
|
).force(
|
|
2045
2239
|
"charge",
|
|
2046
|
-
manyBody_default().strength(chargeStrength).theta(nodeCount >
|
|
2240
|
+
manyBody_default().strength(chargeStrength).theta(nodeCount > 6e3 ? 0.8 : 0.9).distanceMax(nodeCount > 1e4 ? 800 : nodeCount > 6e3 ? 700 : 1e3)
|
|
2047
2241
|
).force(
|
|
2048
2242
|
"collision",
|
|
2049
2243
|
collide_default().radius((node) => (node.style?.radius ?? 20) + collisionRadius).strength(1).iterations(collisionIterations)
|
|
@@ -2051,12 +2245,15 @@ var PhysicsManager = class {
|
|
|
2051
2245
|
"center",
|
|
2052
2246
|
center_default(0, 0).strength(centerStrength)
|
|
2053
2247
|
).velocityDecay(
|
|
2054
|
-
nodeCount >
|
|
2248
|
+
nodeCount > 1e4 ? 0.4 : nodeCount > 6e3 ? 0.5 : adaptiveVelocityDecay
|
|
2055
2249
|
).alphaDecay(
|
|
2056
|
-
nodeCount >
|
|
2250
|
+
nodeCount > 1e4 ? 0.01 : nodeCount > 6e3 ? 0.02 : adaptiveAlphaDecay
|
|
2057
2251
|
).alphaMin(
|
|
2058
|
-
nodeCount >
|
|
2059
|
-
).on("tick", config.onTick).on("end", () => this.handleSimulationEnd());
|
|
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
|
+
}
|
|
2060
2257
|
if (config.cooldownTime) {
|
|
2061
2258
|
this.setupCooldownTimer(config.cooldownTime);
|
|
2062
2259
|
}
|
|
@@ -2069,12 +2266,64 @@ var PhysicsManager = class {
|
|
|
2069
2266
|
throw error;
|
|
2070
2267
|
}
|
|
2071
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
|
+
}
|
|
2072
2320
|
/**
|
|
2073
2321
|
* Handle simulation end
|
|
2074
2322
|
*/
|
|
2075
2323
|
handleSimulationEnd() {
|
|
2076
2324
|
try {
|
|
2077
2325
|
this.simulationEndTime = performance.now();
|
|
2326
|
+
this.isWarmingUp = false;
|
|
2078
2327
|
if (this.config?.onEnd) {
|
|
2079
2328
|
this.config.onEnd();
|
|
2080
2329
|
}
|
|
@@ -2109,34 +2358,6 @@ var PhysicsManager = class {
|
|
|
2109
2358
|
ErrorHandler.logError(error, { cooldownTime });
|
|
2110
2359
|
}
|
|
2111
2360
|
}
|
|
2112
|
-
/**
|
|
2113
|
-
* Build node index for O(1) lookups (Step 3 optimization)
|
|
2114
|
-
*/
|
|
2115
|
-
buildNodeIndex() {
|
|
2116
|
-
if (!this.config) return;
|
|
2117
|
-
try {
|
|
2118
|
-
this.nodeMap.clear();
|
|
2119
|
-
for (const node of this.config.nodes) {
|
|
2120
|
-
this.nodeMap.set(node.id, node);
|
|
2121
|
-
}
|
|
2122
|
-
for (const link of this.config.links) {
|
|
2123
|
-
if (typeof link.source === "string") {
|
|
2124
|
-
const sourceNode = this.nodeMap.get(link.source);
|
|
2125
|
-
if (sourceNode) {
|
|
2126
|
-
link.source = sourceNode;
|
|
2127
|
-
}
|
|
2128
|
-
}
|
|
2129
|
-
if (typeof link.target === "string") {
|
|
2130
|
-
const targetNode = this.nodeMap.get(link.target);
|
|
2131
|
-
if (targetNode) {
|
|
2132
|
-
link.target = targetNode;
|
|
2133
|
-
}
|
|
2134
|
-
}
|
|
2135
|
-
}
|
|
2136
|
-
} catch (error) {
|
|
2137
|
-
ErrorHandler.logError(error);
|
|
2138
|
-
}
|
|
2139
|
-
}
|
|
2140
2361
|
/**
|
|
2141
2362
|
* Get simulation instance
|
|
2142
2363
|
*/
|
|
@@ -2154,7 +2375,7 @@ var PhysicsManager = class {
|
|
|
2154
2375
|
return;
|
|
2155
2376
|
}
|
|
2156
2377
|
const nodeCount = this.config.nodes.length;
|
|
2157
|
-
const effectiveAlpha = alphaTarget ?? (nodeCount > 1e4 ?
|
|
2378
|
+
const effectiveAlpha = alphaTarget ?? (nodeCount > 1e4 ? 0.03 : nodeCount > 5e3 ? 0.05 : nodeCount > 2e3 ? 0.08 : 0.15);
|
|
2158
2379
|
try {
|
|
2159
2380
|
this.simulation.alphaTarget(effectiveAlpha).restart();
|
|
2160
2381
|
if (this.config.cooldownTime) {
|
|
@@ -2231,8 +2452,8 @@ var PhysicsManager = class {
|
|
|
2231
2452
|
const linkForce = this.simulation.force("link");
|
|
2232
2453
|
if (linkForce) {
|
|
2233
2454
|
linkForce.distance((link) => {
|
|
2234
|
-
const sourceNode = typeof link.source === "string" ? this.
|
|
2235
|
-
const targetNode = typeof link.target === "string" ? this.
|
|
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;
|
|
2236
2457
|
const sourceRadius = sourceNode?.style?.radius ?? 20;
|
|
2237
2458
|
const targetRadius = targetNode?.style?.radius ?? 20;
|
|
2238
2459
|
const arrowLength = this.getLinkArrowLength(link);
|
|
@@ -2250,11 +2471,13 @@ var PhysicsManager = class {
|
|
|
2250
2471
|
* Calculate base distance based on graph size and node count
|
|
2251
2472
|
*/
|
|
2252
2473
|
calculateBaseDistance() {
|
|
2253
|
-
if (!this.config) return
|
|
2474
|
+
if (!this.config) return 150;
|
|
2254
2475
|
const nodeCount = this.config.nodes.length;
|
|
2255
2476
|
const graphArea = Math.max(this.config.width * this.config.height, 1);
|
|
2256
2477
|
const nodeAreaRatio = nodeCount / (graphArea / 1e4);
|
|
2257
|
-
|
|
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);
|
|
2258
2481
|
}
|
|
2259
2482
|
/**
|
|
2260
2483
|
* Get arrow length from link style or default
|
|
@@ -2266,15 +2489,38 @@ var PhysicsManager = class {
|
|
|
2266
2489
|
return linkStyle?.arrow?.size ?? 8;
|
|
2267
2490
|
}
|
|
2268
2491
|
/**
|
|
2269
|
-
* Initialize node positions
|
|
2492
|
+
* Initialize node positions with improved distribution for smoother startup
|
|
2270
2493
|
*/
|
|
2271
2494
|
initializePositions() {
|
|
2272
2495
|
if (!this.config) return;
|
|
2273
2496
|
try {
|
|
2274
|
-
|
|
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;
|
|
2275
2504
|
if (node.x == null || node.y == null) {
|
|
2276
|
-
|
|
2277
|
-
|
|
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
|
+
}
|
|
2278
2524
|
}
|
|
2279
2525
|
}
|
|
2280
2526
|
} catch (error) {
|
|
@@ -2334,7 +2580,7 @@ var PhysicsManager = class {
|
|
|
2334
2580
|
return;
|
|
2335
2581
|
}
|
|
2336
2582
|
const nodeCount = this.config?.nodes.length ?? 0;
|
|
2337
|
-
const alpha = nodeCount > 1e4 ? 0.
|
|
2583
|
+
const alpha = nodeCount > 1e4 ? 0.05 : nodeCount > 5e3 ? 0.08 : nodeCount > 2e3 ? 0.12 : 0.3;
|
|
2338
2584
|
this.simulation.alpha(alpha).alphaTarget(0).restart();
|
|
2339
2585
|
if (this.config?.cooldownTime) {
|
|
2340
2586
|
this.setupCooldownTimer(this.config.cooldownTime);
|
|
@@ -2345,6 +2591,9 @@ var PhysicsManager = class {
|
|
|
2345
2591
|
this.pause();
|
|
2346
2592
|
} else {
|
|
2347
2593
|
this.resume();
|
|
2594
|
+
if (this.hasInitialAutoFitCompleted && this.simulation && this.simulation.alpha() > 0) {
|
|
2595
|
+
this.hasInitialAutoFitCompleted = false;
|
|
2596
|
+
}
|
|
2348
2597
|
}
|
|
2349
2598
|
};
|
|
2350
2599
|
/**
|
|
@@ -2362,10 +2611,12 @@ var PhysicsManager = class {
|
|
|
2362
2611
|
this.simulation = void 0;
|
|
2363
2612
|
}
|
|
2364
2613
|
this.config = void 0;
|
|
2365
|
-
this.
|
|
2614
|
+
this.stateManager.destroy();
|
|
2366
2615
|
this.simulationStartTime = void 0;
|
|
2367
2616
|
this.simulationEndTime = void 0;
|
|
2368
2617
|
this.hasInitialAutoFitCompleted = false;
|
|
2618
|
+
this.isWarmingUp = false;
|
|
2619
|
+
this.warmupSteps = 0;
|
|
2369
2620
|
} catch (error) {
|
|
2370
2621
|
ErrorHandler.logError(error);
|
|
2371
2622
|
}
|
|
@@ -5151,6 +5402,10 @@ var DragManager = class {
|
|
|
5151
5402
|
};
|
|
5152
5403
|
DRAG_CLICK_TOLERANCE_PX = 3;
|
|
5153
5404
|
// Force-graph constant
|
|
5405
|
+
// RAF throttling for smooth drag performance
|
|
5406
|
+
dragRenderPending = false;
|
|
5407
|
+
lastDragRenderTime = 0;
|
|
5408
|
+
pendingAnimationFrame;
|
|
5154
5409
|
/**
|
|
5155
5410
|
* Initialize drag behavior
|
|
5156
5411
|
*/
|
|
@@ -5200,6 +5455,10 @@ var DragManager = class {
|
|
|
5200
5455
|
obj.fy = obj.y;
|
|
5201
5456
|
}
|
|
5202
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
|
+
}
|
|
5203
5462
|
} catch (error) {
|
|
5204
5463
|
ErrorHandler.logError(error, {
|
|
5205
5464
|
nodeId: obj?.id,
|
|
@@ -5232,7 +5491,7 @@ var DragManager = class {
|
|
|
5232
5491
|
this.state.isDragging = true;
|
|
5233
5492
|
this.state.isPointerDragging = true;
|
|
5234
5493
|
obj.__dragged = true;
|
|
5235
|
-
this.
|
|
5494
|
+
this.throttledDragRender();
|
|
5236
5495
|
} catch (error) {
|
|
5237
5496
|
ErrorHandler.logError(error, {
|
|
5238
5497
|
nodeId: obj?.id,
|
|
@@ -5240,6 +5499,24 @@ var DragManager = class {
|
|
|
5240
5499
|
});
|
|
5241
5500
|
}
|
|
5242
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
|
+
}
|
|
5243
5520
|
/**
|
|
5244
5521
|
* Handle drag end
|
|
5245
5522
|
*/
|
|
@@ -5264,6 +5541,9 @@ var DragManager = class {
|
|
|
5264
5541
|
this.config.canvas.classList.remove("grabbable");
|
|
5265
5542
|
this.state.isDragging = false;
|
|
5266
5543
|
this.state.isPointerDragging = false;
|
|
5544
|
+
if (this.config.renderer) {
|
|
5545
|
+
this.config.renderer.setDragState(false);
|
|
5546
|
+
}
|
|
5267
5547
|
if (obj.__dragged) {
|
|
5268
5548
|
delete obj.__dragged;
|
|
5269
5549
|
this.config.onRender();
|
|
@@ -5298,6 +5578,10 @@ var DragManager = class {
|
|
|
5298
5578
|
*/
|
|
5299
5579
|
destroy() {
|
|
5300
5580
|
try {
|
|
5581
|
+
if (this.pendingAnimationFrame !== void 0) {
|
|
5582
|
+
cancelAnimationFrame(this.pendingAnimationFrame);
|
|
5583
|
+
this.pendingAnimationFrame = void 0;
|
|
5584
|
+
}
|
|
5301
5585
|
if (this.config?.canvas) {
|
|
5302
5586
|
select_default2(this.config.canvas).on(".drag", null);
|
|
5303
5587
|
this.config.canvas.classList.remove("grabbable");
|
|
@@ -5317,6 +5601,10 @@ var DragManager = class {
|
|
|
5317
5601
|
var ZoomManager = class {
|
|
5318
5602
|
config;
|
|
5319
5603
|
zoomBehavior;
|
|
5604
|
+
isZooming = false;
|
|
5605
|
+
zoomRenderPending = false;
|
|
5606
|
+
isProgrammaticZoom = false;
|
|
5607
|
+
// Flag for programmatic zoom operations
|
|
5320
5608
|
/**
|
|
5321
5609
|
* Initialize zoom behavior
|
|
5322
5610
|
*/
|
|
@@ -5364,49 +5652,69 @@ var ZoomManager = class {
|
|
|
5364
5652
|
* Handle zoom start (when panning begins)
|
|
5365
5653
|
*/
|
|
5366
5654
|
handleZoomStart(event) {
|
|
5655
|
+
this.isZooming = true;
|
|
5367
5656
|
if (!this.config) return;
|
|
5368
5657
|
try {
|
|
5369
5658
|
event.sourceEvent?.stopPropagation();
|
|
5370
5659
|
event.sourceEvent?.preventDefault();
|
|
5371
|
-
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
|
+
}
|
|
5372
5666
|
} catch (error) {
|
|
5373
5667
|
ErrorHandler.logError(error);
|
|
5374
5668
|
}
|
|
5375
5669
|
}
|
|
5376
5670
|
/**
|
|
5377
|
-
* Handle zoom events
|
|
5671
|
+
* Handle zoom events with RAF throttling for smooth performance
|
|
5378
5672
|
*/
|
|
5379
5673
|
handleZoom(event) {
|
|
5380
5674
|
if (!this.config) return;
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
|
|
5384
|
-
|
|
5385
|
-
this.config.canvasManager.clear();
|
|
5386
|
-
this.config.canvasManager.applyTransform(transform2);
|
|
5387
|
-
this.config.onRender();
|
|
5388
|
-
} catch (error) {
|
|
5389
|
-
ErrorHandler.logError(error, {
|
|
5390
|
-
transform: event.transform
|
|
5391
|
-
});
|
|
5675
|
+
event.sourceEvent?.stopPropagation();
|
|
5676
|
+
event.sourceEvent?.preventDefault();
|
|
5677
|
+
if (this.zoomRenderPending) {
|
|
5678
|
+
return;
|
|
5392
5679
|
}
|
|
5680
|
+
this.zoomRenderPending = true;
|
|
5681
|
+
requestAnimationFrame(() => {
|
|
5682
|
+
this.zoomRenderPending = false;
|
|
5683
|
+
if (!this.config) {
|
|
5684
|
+
return;
|
|
5685
|
+
}
|
|
5686
|
+
this.config.onRender();
|
|
5687
|
+
});
|
|
5393
5688
|
}
|
|
5394
5689
|
/**
|
|
5395
5690
|
* Handle zoom end (when panning ends)
|
|
5396
5691
|
*/
|
|
5397
5692
|
handleZoomEnd(event) {
|
|
5693
|
+
this.isZooming = false;
|
|
5398
5694
|
if (!this.config) return;
|
|
5399
5695
|
try {
|
|
5400
5696
|
event.sourceEvent?.stopPropagation();
|
|
5401
5697
|
event.sourceEvent?.preventDefault();
|
|
5402
|
-
|
|
5403
|
-
|
|
5404
|
-
|
|
5698
|
+
if (this.config.renderer && !this.isProgrammaticZoom) {
|
|
5699
|
+
this.config.renderer.setZoomState(false);
|
|
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();
|
|
5405
5709
|
}
|
|
5710
|
+
this.isProgrammaticZoom = false;
|
|
5406
5711
|
} catch (error) {
|
|
5407
5712
|
ErrorHandler.logError(error);
|
|
5408
5713
|
}
|
|
5409
5714
|
}
|
|
5715
|
+
isCurrentlyZooming() {
|
|
5716
|
+
return this.isZooming;
|
|
5717
|
+
}
|
|
5410
5718
|
/**
|
|
5411
5719
|
* Get zoom behavior instance
|
|
5412
5720
|
*/
|
|
@@ -5438,6 +5746,7 @@ var ZoomManager = class {
|
|
|
5438
5746
|
zoomIn(factor = 1.5, center) {
|
|
5439
5747
|
if (!this.config?.canvas || !this.zoomBehavior) return;
|
|
5440
5748
|
try {
|
|
5749
|
+
this.isProgrammaticZoom = true;
|
|
5441
5750
|
const canvas = this.config.canvas;
|
|
5442
5751
|
if (center) {
|
|
5443
5752
|
select_default2(canvas).transition().duration(300).call(this.zoomBehavior.scaleBy, factor, center);
|
|
@@ -5460,6 +5769,7 @@ var ZoomManager = class {
|
|
|
5460
5769
|
resetZoom(duration = 500) {
|
|
5461
5770
|
if (!this.config?.canvas || !this.zoomBehavior) return;
|
|
5462
5771
|
try {
|
|
5772
|
+
this.isProgrammaticZoom = true;
|
|
5463
5773
|
const canvasDims = this.config.canvasManager.getDimensions();
|
|
5464
5774
|
const transform2 = identity2.translate(canvasDims.width / 2, canvasDims.height / 2);
|
|
5465
5775
|
select_default2(this.config.canvas).transition().duration(duration).call(this.zoomBehavior.transform, transform2);
|
|
@@ -5473,6 +5783,7 @@ var ZoomManager = class {
|
|
|
5473
5783
|
setTransform(transform2, duration = 0) {
|
|
5474
5784
|
if (!this.config?.canvas || !this.zoomBehavior) return;
|
|
5475
5785
|
try {
|
|
5786
|
+
this.isProgrammaticZoom = true;
|
|
5476
5787
|
const selection2 = select_default2(this.config.canvas);
|
|
5477
5788
|
const zoomTransform = identity2.translate(transform2.x, transform2.y).scale(transform2.k);
|
|
5478
5789
|
if (duration > 0) {
|
|
@@ -5494,6 +5805,7 @@ var ZoomManager = class {
|
|
|
5494
5805
|
return;
|
|
5495
5806
|
}
|
|
5496
5807
|
try {
|
|
5808
|
+
this.isProgrammaticZoom = true;
|
|
5497
5809
|
const canvas = this.config.canvas;
|
|
5498
5810
|
const transitionDuration = 750;
|
|
5499
5811
|
const canvasDims = this.config.canvasManager.getDimensions();
|
|
@@ -5582,6 +5894,8 @@ var HoverManager = class {
|
|
|
5582
5894
|
flushShadowCanvas;
|
|
5583
5895
|
hasValidPointerPosition = false;
|
|
5584
5896
|
containerWarningLogged = false;
|
|
5897
|
+
// Store bound handlers for proper cleanup
|
|
5898
|
+
boundHandlers = /* @__PURE__ */ new Map();
|
|
5585
5899
|
/**
|
|
5586
5900
|
* Initialize hover manager with force-graph pattern
|
|
5587
5901
|
*/
|
|
@@ -5613,7 +5927,7 @@ var HoverManager = class {
|
|
|
5613
5927
|
console.error("Cannot add pointer event listeners - container is null");
|
|
5614
5928
|
return;
|
|
5615
5929
|
}
|
|
5616
|
-
|
|
5930
|
+
const eventHandler = (ev) => {
|
|
5617
5931
|
const pointerEvent = ev;
|
|
5618
5932
|
const container = this.container;
|
|
5619
5933
|
if (!container) {
|
|
@@ -5638,7 +5952,9 @@ var HoverManager = class {
|
|
|
5638
5952
|
this.containerWarningLogged = true;
|
|
5639
5953
|
}
|
|
5640
5954
|
}
|
|
5641
|
-
}
|
|
5955
|
+
};
|
|
5956
|
+
this.boundHandlers.set(evType, eventHandler);
|
|
5957
|
+
this.container.addEventListener(evType, eventHandler, { passive: true });
|
|
5642
5958
|
});
|
|
5643
5959
|
}
|
|
5644
5960
|
/**
|
|
@@ -5859,6 +6175,12 @@ var HoverManager = class {
|
|
|
5859
6175
|
*/
|
|
5860
6176
|
destroy() {
|
|
5861
6177
|
try {
|
|
6178
|
+
if (this.container) {
|
|
6179
|
+
this.boundHandlers.forEach((handler, eventType) => {
|
|
6180
|
+
this.container.removeEventListener(eventType, handler);
|
|
6181
|
+
});
|
|
6182
|
+
}
|
|
6183
|
+
this.boundHandlers.clear();
|
|
5862
6184
|
this.eventHandlers.clear();
|
|
5863
6185
|
this.hoverState = {
|
|
5864
6186
|
currentHovered: null,
|
|
@@ -5887,6 +6209,8 @@ var SelectionManager = class {
|
|
|
5887
6209
|
};
|
|
5888
6210
|
eventHandlers = /* @__PURE__ */ new Map();
|
|
5889
6211
|
container;
|
|
6212
|
+
// Store bound handlers for proper cleanup
|
|
6213
|
+
boundHandlers = /* @__PURE__ */ new Map();
|
|
5890
6214
|
/**
|
|
5891
6215
|
* Initialize selection manager
|
|
5892
6216
|
*/
|
|
@@ -5908,14 +6232,19 @@ var SelectionManager = class {
|
|
|
5908
6232
|
*/
|
|
5909
6233
|
setupSelectionListeners() {
|
|
5910
6234
|
if (!this.container || !this.canvasState) return;
|
|
5911
|
-
|
|
6235
|
+
const clickHandler = (event) => {
|
|
5912
6236
|
this.handleSelectionClick(event);
|
|
5913
|
-
}
|
|
5914
|
-
|
|
5915
|
-
|
|
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") {
|
|
5916
6243
|
this.clearSelection();
|
|
5917
6244
|
}
|
|
5918
|
-
}
|
|
6245
|
+
};
|
|
6246
|
+
this.boundHandlers.set("keydown", keydownHandler);
|
|
6247
|
+
document.addEventListener("keydown", keydownHandler);
|
|
5919
6248
|
}
|
|
5920
6249
|
/**
|
|
5921
6250
|
* Handle selection click
|
|
@@ -6147,14 +6476,13 @@ var SelectionManager = class {
|
|
|
6147
6476
|
*/
|
|
6148
6477
|
destroy() {
|
|
6149
6478
|
try {
|
|
6150
|
-
if (this.container) {
|
|
6151
|
-
this.container.removeEventListener("click", this.
|
|
6479
|
+
if (this.container && this.boundHandlers.has("click")) {
|
|
6480
|
+
this.container.removeEventListener("click", this.boundHandlers.get("click"));
|
|
6152
6481
|
}
|
|
6153
|
-
|
|
6154
|
-
|
|
6155
|
-
|
|
6156
|
-
|
|
6157
|
-
});
|
|
6482
|
+
if (this.boundHandlers.has("keydown")) {
|
|
6483
|
+
document.removeEventListener("keydown", this.boundHandlers.get("keydown"));
|
|
6484
|
+
}
|
|
6485
|
+
this.boundHandlers.clear();
|
|
6158
6486
|
this.eventHandlers.clear();
|
|
6159
6487
|
this.selectionState = {
|
|
6160
6488
|
selectedNode: null,
|
|
@@ -6232,7 +6560,7 @@ var NodesRenderer = class {
|
|
|
6232
6560
|
/**
|
|
6233
6561
|
* Render nodes to canvas using StyleResolver with performance metrics
|
|
6234
6562
|
*/
|
|
6235
|
-
static renderWithStyleResolver(ctx, nodes, styleResolver, isNodeHovered, isNodeSelected, performanceMetrics) {
|
|
6563
|
+
static renderWithStyleResolver(ctx, nodes, styleResolver, isNodeHovered, isNodeSelected, isNodeHighlighted, performanceMetrics) {
|
|
6236
6564
|
try {
|
|
6237
6565
|
for (const node of nodes) {
|
|
6238
6566
|
const x3 = node.x;
|
|
@@ -6240,6 +6568,7 @@ var NodesRenderer = class {
|
|
|
6240
6568
|
const hoverStart = performance.now();
|
|
6241
6569
|
const isHovered = isNodeHovered(node.id);
|
|
6242
6570
|
const isSelected = isNodeSelected ? isNodeSelected(node.id) : false;
|
|
6571
|
+
const isHighlighted = isNodeHighlighted ? isNodeHighlighted(node.id) : false;
|
|
6243
6572
|
if (performanceMetrics) {
|
|
6244
6573
|
performanceMetrics.hoverChecks += performance.now() - hoverStart;
|
|
6245
6574
|
}
|
|
@@ -6247,7 +6576,8 @@ var NodesRenderer = class {
|
|
|
6247
6576
|
const style = styleResolver.resolveNodeStyle({
|
|
6248
6577
|
node,
|
|
6249
6578
|
isHovered,
|
|
6250
|
-
isSelected
|
|
6579
|
+
isSelected,
|
|
6580
|
+
isHighlighted
|
|
6251
6581
|
});
|
|
6252
6582
|
if (performanceMetrics) {
|
|
6253
6583
|
performanceMetrics.styleResolution += performance.now() - styleStart;
|
|
@@ -6397,6 +6727,514 @@ var NodeLabelsRenderer = class {
|
|
|
6397
6727
|
}
|
|
6398
6728
|
};
|
|
6399
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
|
+
|
|
6400
7238
|
// src/v2/rendering/link-labels-renderer.ts
|
|
6401
7239
|
var LinkLabelsRenderer = class {
|
|
6402
7240
|
static textMetricsCache = /* @__PURE__ */ new Map();
|
|
@@ -6603,107 +7441,30 @@ var LinkLabelsRenderer = class {
|
|
|
6603
7441
|
}
|
|
6604
7442
|
};
|
|
6605
7443
|
|
|
6606
|
-
// src/v2/rendering/renderer.ts
|
|
6607
|
-
var
|
|
7444
|
+
// src/v2/rendering/hit-detection-renderer.ts
|
|
7445
|
+
var HitDetectionRenderer = class {
|
|
6608
7446
|
config;
|
|
6609
|
-
|
|
6610
|
-
|
|
6611
|
-
selectionManager;
|
|
6612
|
-
styleResolver;
|
|
6613
|
-
// Performance optimization: O(1) node lookups
|
|
6614
|
-
nodeMap = /* @__PURE__ */ new Map();
|
|
6615
|
-
// Shadow canvas optimization (Step 6 optimization)
|
|
7447
|
+
stateManager;
|
|
7448
|
+
// Shadow canvas optimization (throttling)
|
|
6616
7449
|
shadowCanvasDirty = true;
|
|
6617
7450
|
lastShadowRenderTime = 0;
|
|
6618
7451
|
SHADOW_RENDER_THROTTLE = 32;
|
|
6619
7452
|
// ~30 FPS max for shadow canvas
|
|
6620
|
-
// Large graph optimization flag
|
|
6621
|
-
hasLoggedLargeGraphOptimization = false;
|
|
6622
|
-
// Performance metrics
|
|
6623
|
-
performanceMetrics = {
|
|
6624
|
-
renderTotal: 0,
|
|
6625
|
-
renderNodes: 0,
|
|
6626
|
-
renderLinks: 0,
|
|
6627
|
-
renderLinkLabels: 0,
|
|
6628
|
-
renderNodeLabels: 0,
|
|
6629
|
-
styleResolution: 0,
|
|
6630
|
-
hoverChecks: 0,
|
|
6631
|
-
canvasCalls: 0,
|
|
6632
|
-
frameCount: 0
|
|
6633
|
-
};
|
|
6634
|
-
// Force-graph pattern: configurable link hover precision
|
|
6635
|
-
linkHoverPrecision = 4;
|
|
6636
|
-
// Default 4px like force-graph
|
|
6637
7453
|
/**
|
|
6638
|
-
* Initialize
|
|
7454
|
+
* Initialize hit detection renderer
|
|
6639
7455
|
*/
|
|
6640
|
-
initialize(config
|
|
7456
|
+
initialize(config) {
|
|
7457
|
+
this.config = config;
|
|
7458
|
+
this.stateManager = config.stateManager;
|
|
7459
|
+
}
|
|
7460
|
+
/**
|
|
7461
|
+
* Render shadow canvas for hit detection with throttling
|
|
7462
|
+
*/
|
|
7463
|
+
renderShadowCanvas() {
|
|
7464
|
+
if (!this.config) return;
|
|
7465
|
+
const now2 = Date.now();
|
|
6641
7466
|
try {
|
|
6642
|
-
|
|
6643
|
-
this.canvasState = canvasState;
|
|
6644
|
-
this.hoverManager = hoverManager;
|
|
6645
|
-
this.selectionManager = selectionManager;
|
|
6646
|
-
this.styleResolver = createStyleResolver(config.interaction);
|
|
6647
|
-
this.buildNodeIndex();
|
|
6648
|
-
} catch (error) {
|
|
6649
|
-
ErrorHandler.logError(error, {
|
|
6650
|
-
nodeCount: config.nodes.length,
|
|
6651
|
-
linkCount: config.links.length
|
|
6652
|
-
});
|
|
6653
|
-
throw error;
|
|
6654
|
-
}
|
|
6655
|
-
}
|
|
6656
|
-
/**
|
|
6657
|
-
* Main render method with performance metrics (Instrumented)
|
|
6658
|
-
*/
|
|
6659
|
-
render() {
|
|
6660
|
-
if (!this.config || !this.canvasState) {
|
|
6661
|
-
throw new RenderError("Renderer not initialized");
|
|
6662
|
-
}
|
|
6663
|
-
const startTime = performance.now();
|
|
6664
|
-
this.performanceMetrics.frameCount++;
|
|
6665
|
-
try {
|
|
6666
|
-
const { ctx } = this.canvasState;
|
|
6667
|
-
this.clearCanvas(ctx);
|
|
6668
|
-
this.renderWithLayersAndMetrics(ctx);
|
|
6669
|
-
this.markShadowCanvasDirty();
|
|
6670
|
-
this.performanceMetrics.renderTotal += performance.now() - startTime;
|
|
6671
|
-
if (this.performanceMetrics.frameCount % 100 === 0) {
|
|
6672
|
-
this.logPerformanceMetrics();
|
|
6673
|
-
}
|
|
6674
|
-
} catch (error) {
|
|
6675
|
-
ErrorHandler.logError(error);
|
|
6676
|
-
throw new RenderError("Failed to render graph", {
|
|
6677
|
-
nodeCount: this.config.nodes.length,
|
|
6678
|
-
linkCount: this.config.links.length,
|
|
6679
|
-
originalError: error.message
|
|
6680
|
-
});
|
|
6681
|
-
}
|
|
6682
|
-
}
|
|
6683
|
-
/**
|
|
6684
|
-
* Render with transform (called during zoom/pan) with shadow canvas dirty marking (Step 6 optimization)
|
|
6685
|
-
*/
|
|
6686
|
-
renderWithTransform() {
|
|
6687
|
-
if (!this.canvasState) return;
|
|
6688
|
-
try {
|
|
6689
|
-
const { canvas, ctx } = this.canvasState;
|
|
6690
|
-
const transform2 = transform(canvas);
|
|
6691
|
-
this.clearCanvas(ctx);
|
|
6692
|
-
this.applyTransform(transform2, ctx);
|
|
6693
|
-
this.renderWithLayers(ctx);
|
|
6694
|
-
this.markShadowCanvasDirty();
|
|
6695
|
-
} catch (error) {
|
|
6696
|
-
ErrorHandler.logError(error);
|
|
6697
|
-
}
|
|
6698
|
-
}
|
|
6699
|
-
/**
|
|
6700
|
-
* Render shadow canvas for hit detection with throttling (Step 6 optimization)
|
|
6701
|
-
*/
|
|
6702
|
-
renderShadowCanvas() {
|
|
6703
|
-
if (!this.canvasState || !this.config) return;
|
|
6704
|
-
const now2 = Date.now();
|
|
6705
|
-
try {
|
|
6706
|
-
const { shadowCtx, canvas } = this.canvasState;
|
|
7467
|
+
const { shadowCtx, canvas } = this.config;
|
|
6707
7468
|
const transform2 = transform(canvas);
|
|
6708
7469
|
this.clearCanvas(shadowCtx);
|
|
6709
7470
|
this.applyTransform(transform2, shadowCtx);
|
|
@@ -6717,13 +7478,13 @@ var Renderer = class {
|
|
|
6717
7478
|
}
|
|
6718
7479
|
}
|
|
6719
7480
|
/**
|
|
6720
|
-
* Mark shadow canvas as dirty for next render
|
|
7481
|
+
* Mark shadow canvas as dirty for next render
|
|
6721
7482
|
*/
|
|
6722
7483
|
markShadowCanvasDirty() {
|
|
6723
7484
|
this.shadowCanvasDirty = true;
|
|
6724
7485
|
}
|
|
6725
7486
|
/**
|
|
6726
|
-
* Force shadow canvas render
|
|
7487
|
+
* Force shadow canvas render
|
|
6727
7488
|
*/
|
|
6728
7489
|
forceShadowCanvasRender() {
|
|
6729
7490
|
this.shadowCanvasDirty = true;
|
|
@@ -6731,96 +7492,53 @@ var Renderer = class {
|
|
|
6731
7492
|
this.renderShadowCanvas();
|
|
6732
7493
|
}
|
|
6733
7494
|
/**
|
|
6734
|
-
* Clear canvas context
|
|
7495
|
+
* Clear shadow canvas context
|
|
6735
7496
|
*/
|
|
6736
7497
|
clearCanvas(ctx) {
|
|
6737
|
-
if (!this.
|
|
7498
|
+
if (!this.config) return;
|
|
6738
7499
|
try {
|
|
6739
|
-
const {
|
|
7500
|
+
const { canvas } = this.config;
|
|
7501
|
+
const { width, height } = canvas;
|
|
6740
7502
|
CanvasUtils.resetTransform(ctx);
|
|
6741
7503
|
ctx.clearRect(0, 0, width, height);
|
|
6742
|
-
} catch {
|
|
6743
|
-
|
|
7504
|
+
} catch (error) {
|
|
7505
|
+
ErrorHandler.logError(error, { message: "Failed to clear shadow canvas" });
|
|
6744
7506
|
}
|
|
6745
7507
|
}
|
|
6746
7508
|
/**
|
|
6747
|
-
* Apply transform to canvas context
|
|
7509
|
+
* Apply transform to shadow canvas context
|
|
6748
7510
|
*/
|
|
6749
7511
|
applyTransform(transform2, ctx) {
|
|
6750
7512
|
try {
|
|
6751
7513
|
CanvasUtils.resetTransform(ctx);
|
|
6752
7514
|
ctx.translate(transform2.x, transform2.y);
|
|
6753
7515
|
ctx.scale(transform2.k, transform2.k);
|
|
6754
|
-
} catch {
|
|
6755
|
-
throw new RenderError("Failed to apply transform");
|
|
6756
|
-
}
|
|
6757
|
-
}
|
|
6758
|
-
/**
|
|
6759
|
-
* Get unique link ID for tracking (consistent with LinkLabelsRenderer)
|
|
6760
|
-
*/
|
|
6761
|
-
getLinkId(link) {
|
|
6762
|
-
const sourceId = typeof link.source === "string" ? link.source : link.source.id;
|
|
6763
|
-
const targetId = typeof link.target === "string" ? link.target : link.target.id;
|
|
6764
|
-
return `${sourceId}->${targetId}`;
|
|
6765
|
-
}
|
|
6766
|
-
/**
|
|
6767
|
-
* Render main canvas nodes
|
|
6768
|
-
*/
|
|
6769
|
-
renderNodes(ctx) {
|
|
6770
|
-
if (!this.config || !this.styleResolver) return;
|
|
6771
|
-
try {
|
|
6772
|
-
const { nodes } = this.config;
|
|
6773
|
-
NodesRenderer.renderWithStyleResolver(
|
|
6774
|
-
ctx,
|
|
6775
|
-
nodes,
|
|
6776
|
-
this.styleResolver,
|
|
6777
|
-
(nodeId) => this.isNodeHovered(nodeId),
|
|
6778
|
-
(nodeId) => this.isNodeSelected(nodeId),
|
|
6779
|
-
this.performanceMetrics
|
|
6780
|
-
);
|
|
6781
|
-
} catch (error) {
|
|
6782
|
-
ErrorHandler.logError(error);
|
|
6783
|
-
throw new RenderError("Failed to render nodes");
|
|
6784
|
-
}
|
|
6785
|
-
}
|
|
6786
|
-
/**
|
|
6787
|
-
* Render node labels
|
|
6788
|
-
*/
|
|
6789
|
-
renderNodeLabels(ctx) {
|
|
6790
|
-
if (!this.config || !this.styleResolver) return;
|
|
6791
|
-
try {
|
|
6792
|
-
const { nodes } = this.config;
|
|
6793
|
-
const defaultNodeStyle = this.styleResolver.resolveNodeStyle({
|
|
6794
|
-
node: { id: "temp" }
|
|
6795
|
-
});
|
|
6796
|
-
NodeLabelsRenderer.render(ctx, nodes, defaultNodeStyle.radius);
|
|
6797
7516
|
} catch (error) {
|
|
6798
|
-
ErrorHandler.logError(error);
|
|
6799
|
-
throw new RenderError("Failed to render node labels");
|
|
7517
|
+
ErrorHandler.logError(error, { message: "Failed to apply transform to shadow canvas" });
|
|
6800
7518
|
}
|
|
6801
7519
|
}
|
|
6802
7520
|
/**
|
|
6803
|
-
* Render shadow links with __indexColor
|
|
7521
|
+
* Render shadow links with __indexColor for hit detection
|
|
6804
7522
|
*/
|
|
6805
7523
|
renderShadowLinks(shadowCtx) {
|
|
6806
|
-
if (!this.config
|
|
7524
|
+
if (!this.config) return;
|
|
6807
7525
|
try {
|
|
6808
|
-
const { links } = this.config;
|
|
6809
|
-
const defaultLinkStyle =
|
|
7526
|
+
const { links, stateManager, styleResolver, linkHoverPrecision = 4 } = this.config;
|
|
7527
|
+
const defaultLinkStyle = styleResolver.resolveLinkStyle({
|
|
6810
7528
|
link: { source: "", target: "" }
|
|
6811
7529
|
});
|
|
6812
7530
|
for (const link of links) {
|
|
6813
7531
|
if (!link.__indexColor) continue;
|
|
6814
|
-
const sourceNode = typeof link.source === "string" ?
|
|
6815
|
-
const targetNode = typeof link.target === "string" ?
|
|
6816
|
-
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) {
|
|
6817
7535
|
const [r, g, b] = link.__indexColorRGB;
|
|
6818
7536
|
const rgbColor = `rgb(${r},${g},${b})`;
|
|
6819
7537
|
const dx = targetNode.x - sourceNode.x;
|
|
6820
7538
|
const dy = targetNode.y - sourceNode.y;
|
|
6821
7539
|
const length = Math.sqrt(dx * dx + dy * dy);
|
|
6822
7540
|
const angle = Math.atan2(dy, dx);
|
|
6823
|
-
const thickness = defaultLinkStyle.strokeWidth +
|
|
7541
|
+
const thickness = defaultLinkStyle.strokeWidth + linkHoverPrecision;
|
|
6824
7542
|
shadowCtx.save();
|
|
6825
7543
|
shadowCtx.translate(sourceNode.x, sourceNode.y);
|
|
6826
7544
|
shadowCtx.rotate(angle);
|
|
@@ -6837,27 +7555,30 @@ var Renderer = class {
|
|
|
6837
7555
|
* Render shadow link labels for hit detection
|
|
6838
7556
|
*/
|
|
6839
7557
|
renderShadowLinkLabels(shadowCtx) {
|
|
6840
|
-
if (!this.config
|
|
7558
|
+
if (!this.config) return;
|
|
6841
7559
|
try {
|
|
7560
|
+
const { links, styleResolver } = this.config;
|
|
6842
7561
|
const linkStateCache = /* @__PURE__ */ new Map();
|
|
6843
7562
|
const linkIdToLinkMap = /* @__PURE__ */ new Map();
|
|
6844
|
-
for (const link of
|
|
7563
|
+
for (const link of links) {
|
|
6845
7564
|
const linkId = this.getLinkId(link);
|
|
6846
7565
|
linkIdToLinkMap.set(linkId, link);
|
|
6847
7566
|
}
|
|
6848
7567
|
const labelPositions = LinkLabelsRenderer.calculateLabelPositions(
|
|
6849
|
-
|
|
7568
|
+
links,
|
|
6850
7569
|
(link) => {
|
|
6851
7570
|
const linkId = this.getLinkId(link);
|
|
6852
7571
|
let linkState = linkStateCache.get(linkId);
|
|
6853
7572
|
if (!linkState) {
|
|
6854
7573
|
linkState = {
|
|
6855
|
-
isHovered:
|
|
6856
|
-
|
|
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
|
|
6857
7578
|
};
|
|
6858
7579
|
linkStateCache.set(linkId, linkState);
|
|
6859
7580
|
}
|
|
6860
|
-
const style =
|
|
7581
|
+
const style = styleResolver.resolveLinkStyle({
|
|
6861
7582
|
link,
|
|
6862
7583
|
isHovered: linkState.isHovered,
|
|
6863
7584
|
isSelected: linkState.isSelected
|
|
@@ -6865,34 +7586,10 @@ var Renderer = class {
|
|
|
6865
7586
|
return style.label || null;
|
|
6866
7587
|
},
|
|
6867
7588
|
(link) => this.getLinkMidpoint(link),
|
|
6868
|
-
(
|
|
6869
|
-
|
|
6870
|
-
|
|
6871
|
-
|
|
6872
|
-
if (link) {
|
|
6873
|
-
linkState = {
|
|
6874
|
-
isHovered: this.isLinkHovered(link),
|
|
6875
|
-
isSelected: this.isLinkSelected(link)
|
|
6876
|
-
};
|
|
6877
|
-
linkStateCache.set(linkId, linkState);
|
|
6878
|
-
}
|
|
6879
|
-
}
|
|
6880
|
-
return linkState?.isHovered || false;
|
|
6881
|
-
},
|
|
6882
|
-
(linkId) => {
|
|
6883
|
-
let linkState = linkStateCache.get(linkId);
|
|
6884
|
-
if (!linkState) {
|
|
6885
|
-
const link = linkIdToLinkMap.get(linkId);
|
|
6886
|
-
if (link) {
|
|
6887
|
-
linkState = {
|
|
6888
|
-
isHovered: this.isLinkHovered(link),
|
|
6889
|
-
isSelected: this.isLinkSelected(link)
|
|
6890
|
-
};
|
|
6891
|
-
linkStateCache.set(linkId, linkState);
|
|
6892
|
-
}
|
|
6893
|
-
}
|
|
6894
|
-
return linkState?.isSelected || false;
|
|
6895
|
-
}
|
|
7589
|
+
(_linkId) => false,
|
|
7590
|
+
// isHovered - not needed for hit detection
|
|
7591
|
+
(_linkId) => false
|
|
7592
|
+
// isSelected - not needed for hit detection
|
|
6896
7593
|
);
|
|
6897
7594
|
for (const [linkId, position] of labelPositions) {
|
|
6898
7595
|
const link = linkIdToLinkMap.get(linkId);
|
|
@@ -6908,13 +7605,13 @@ var Renderer = class {
|
|
|
6908
7605
|
}
|
|
6909
7606
|
}
|
|
6910
7607
|
/**
|
|
6911
|
-
* Render shadow nodes with __indexColor
|
|
7608
|
+
* Render shadow nodes with __indexColor for hit detection
|
|
6912
7609
|
*/
|
|
6913
7610
|
renderShadowNodes(shadowCtx) {
|
|
6914
|
-
if (!this.config
|
|
7611
|
+
if (!this.config) return;
|
|
6915
7612
|
try {
|
|
6916
|
-
const { nodes } = this.config;
|
|
6917
|
-
const defaultNodeStyle =
|
|
7613
|
+
const { nodes, styleResolver } = this.config;
|
|
7614
|
+
const defaultNodeStyle = styleResolver.resolveNodeStyle({
|
|
6918
7615
|
node: { id: "temp" }
|
|
6919
7616
|
});
|
|
6920
7617
|
NodesRenderer.renderShadow(
|
|
@@ -6927,68 +7624,98 @@ var Renderer = class {
|
|
|
6927
7624
|
}
|
|
6928
7625
|
}
|
|
6929
7626
|
/**
|
|
6930
|
-
* Get
|
|
7627
|
+
* Get unique link ID for tracking (delegates to StateManager)
|
|
6931
7628
|
*/
|
|
6932
|
-
|
|
6933
|
-
|
|
6934
|
-
const hoverState = this.hoverManager.getHoverState();
|
|
6935
|
-
if (!hoverState.currentHovered || hoverState.currentHovered.d.entityType !== "Node") {
|
|
6936
|
-
return null;
|
|
6937
|
-
}
|
|
6938
|
-
const hoveredNode = hoverState.currentHovered.d;
|
|
6939
|
-
return hoveredNode ? hoveredNode.id : null;
|
|
7629
|
+
getLinkId(link) {
|
|
7630
|
+
return this.stateManager.getLinkId(link);
|
|
6940
7631
|
}
|
|
6941
7632
|
/**
|
|
6942
|
-
*
|
|
7633
|
+
* Calculate midpoint of a link for label positioning (delegates to StateManager)
|
|
6943
7634
|
*/
|
|
6944
|
-
|
|
6945
|
-
|
|
6946
|
-
const selectionState = this.selectionManager.getSelectionState();
|
|
6947
|
-
return selectionState.selectedNode?.id || null;
|
|
7635
|
+
getLinkMidpoint(link) {
|
|
7636
|
+
return this.stateManager.getLinkMidpoint(link);
|
|
6948
7637
|
}
|
|
6949
7638
|
/**
|
|
6950
|
-
*
|
|
7639
|
+
* Update configuration
|
|
6951
7640
|
*/
|
|
6952
|
-
|
|
6953
|
-
if (
|
|
6954
|
-
|
|
6955
|
-
if (!hoverState.currentHovered || hoverState.currentHovered.d.entityType !== "Link") {
|
|
6956
|
-
return null;
|
|
7641
|
+
updateConfig(updates) {
|
|
7642
|
+
if (this.config) {
|
|
7643
|
+
Object.assign(this.config, updates);
|
|
6957
7644
|
}
|
|
6958
|
-
return hoverState.currentHovered.d;
|
|
6959
7645
|
}
|
|
6960
7646
|
/**
|
|
6961
|
-
*
|
|
7647
|
+
* Debug shadow canvas export
|
|
6962
7648
|
*/
|
|
6963
|
-
|
|
6964
|
-
|
|
6965
|
-
|
|
6966
|
-
|
|
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
|
+
}
|
|
6967
7661
|
}
|
|
6968
7662
|
/**
|
|
6969
|
-
*
|
|
7663
|
+
* Get hit detection stats
|
|
6970
7664
|
*/
|
|
6971
|
-
|
|
6972
|
-
|
|
6973
|
-
|
|
6974
|
-
|
|
6975
|
-
|
|
6976
|
-
|
|
6977
|
-
console.log("\u23F1\uFE0F Total render:", (this.performanceMetrics.renderTotal / frames).toFixed(2), "ms");
|
|
6978
|
-
console.log("\u{1F517} Links render:", (this.performanceMetrics.renderLinks / frames).toFixed(2), "ms");
|
|
6979
|
-
console.log("\u{1F3F7}\uFE0F Link labels:", (this.performanceMetrics.renderLinkLabels / frames).toFixed(2), "ms");
|
|
6980
|
-
console.log("\u2B55 Nodes render:", (this.performanceMetrics.renderNodes / frames).toFixed(2), "ms");
|
|
6981
|
-
console.log("\u{1F4DD} Node labels:", (this.performanceMetrics.renderNodeLabels / frames).toFixed(2), "ms");
|
|
6982
|
-
console.log("\u{1F3A8} Style resolution:", (this.performanceMetrics.styleResolution / frames).toFixed(2), "ms");
|
|
6983
|
-
console.log("\u{1F446} Hover checks:", (this.performanceMetrics.hoverChecks / frames).toFixed(2), "ms");
|
|
6984
|
-
console.log("\u{1F5BC}\uFE0F Canvas calls:", (this.performanceMetrics.canvasCalls / frames).toFixed(2), "ms");
|
|
6985
|
-
console.log("---");
|
|
7665
|
+
getStats() {
|
|
7666
|
+
return {
|
|
7667
|
+
shadowCanvasDirty: this.shadowCanvasDirty,
|
|
7668
|
+
throttleRate: this.SHADOW_RENDER_THROTTLE,
|
|
7669
|
+
lastRenderTime: this.lastShadowRenderTime
|
|
7670
|
+
};
|
|
6986
7671
|
}
|
|
6987
7672
|
/**
|
|
6988
|
-
*
|
|
7673
|
+
* Destroy and clean up
|
|
6989
7674
|
*/
|
|
6990
|
-
|
|
6991
|
-
this.
|
|
7675
|
+
destroy() {
|
|
7676
|
+
this.config = void 0;
|
|
7677
|
+
this.stateManager = void 0;
|
|
7678
|
+
this.shadowCanvasDirty = false;
|
|
7679
|
+
this.lastShadowRenderTime = 0;
|
|
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
|
+
};
|
|
7696
|
+
/**
|
|
7697
|
+
* Increment frame count
|
|
7698
|
+
*/
|
|
7699
|
+
incrementFrame() {
|
|
7700
|
+
this.metrics.frameCount++;
|
|
7701
|
+
}
|
|
7702
|
+
/**
|
|
7703
|
+
* Add timing for a specific metric
|
|
7704
|
+
*/
|
|
7705
|
+
addTiming(metric, time) {
|
|
7706
|
+
this.metrics[metric] += time;
|
|
7707
|
+
}
|
|
7708
|
+
/**
|
|
7709
|
+
* Get current metrics (copy to prevent mutation)
|
|
7710
|
+
*/
|
|
7711
|
+
getMetrics() {
|
|
7712
|
+
return { ...this.metrics };
|
|
7713
|
+
}
|
|
7714
|
+
/**
|
|
7715
|
+
* Reset all metrics
|
|
7716
|
+
*/
|
|
7717
|
+
reset() {
|
|
7718
|
+
this.metrics = {
|
|
6992
7719
|
renderTotal: 0,
|
|
6993
7720
|
renderNodes: 0,
|
|
6994
7721
|
renderLinks: 0,
|
|
@@ -7001,69 +7728,204 @@ var Renderer = class {
|
|
|
7001
7728
|
};
|
|
7002
7729
|
}
|
|
7003
7730
|
/**
|
|
7004
|
-
*
|
|
7005
|
-
*/
|
|
7006
|
-
getPerformanceMetrics() {
|
|
7007
|
-
return { ...this.performanceMetrics };
|
|
7008
|
-
}
|
|
7009
|
-
/**
|
|
7010
|
-
* Force log performance metrics immediately (for debugging)
|
|
7731
|
+
* Log performance metrics for analysis
|
|
7011
7732
|
*/
|
|
7012
|
-
|
|
7013
|
-
this.
|
|
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("---");
|
|
7014
7746
|
}
|
|
7015
7747
|
/**
|
|
7016
|
-
* Check if
|
|
7748
|
+
* Check if it's time to log metrics (every N frames)
|
|
7017
7749
|
*/
|
|
7018
|
-
|
|
7019
|
-
|
|
7020
|
-
const hoverState = this.hoverManager.getHoverState();
|
|
7021
|
-
if (!hoverState.currentHovered || hoverState.currentHovered.d.entityType !== "Node") {
|
|
7022
|
-
return false;
|
|
7023
|
-
}
|
|
7024
|
-
const hoveredNode = hoverState.currentHovered.d;
|
|
7025
|
-
return hoveredNode && hoveredNode.id === nodeId;
|
|
7750
|
+
shouldLogMetrics(intervalFrames = 100) {
|
|
7751
|
+
return this.metrics.frameCount % intervalFrames === 0 && this.metrics.frameCount > 0;
|
|
7026
7752
|
}
|
|
7753
|
+
};
|
|
7754
|
+
|
|
7755
|
+
// src/v2/rendering/link-renderer.ts
|
|
7756
|
+
var LinkRenderer = class {
|
|
7027
7757
|
/**
|
|
7028
|
-
*
|
|
7758
|
+
* Render a directed link with optional arrow head
|
|
7029
7759
|
*/
|
|
7030
|
-
|
|
7031
|
-
|
|
7032
|
-
|
|
7033
|
-
|
|
7034
|
-
|
|
7035
|
-
|
|
7036
|
-
|
|
7037
|
-
|
|
7038
|
-
|
|
7039
|
-
|
|
7040
|
-
const
|
|
7041
|
-
|
|
7042
|
-
|
|
7043
|
-
|
|
7044
|
-
|
|
7045
|
-
|
|
7046
|
-
|
|
7047
|
-
|
|
7048
|
-
|
|
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);
|
|
7787
|
+
}
|
|
7788
|
+
ctx.globalAlpha = 1;
|
|
7789
|
+
} catch (error) {
|
|
7790
|
+
ErrorHandler.logError(error);
|
|
7049
7791
|
}
|
|
7050
|
-
return false;
|
|
7051
7792
|
}
|
|
7052
7793
|
/**
|
|
7053
|
-
*
|
|
7794
|
+
* V1-compatible link shortening for source point
|
|
7054
7795
|
*/
|
|
7055
|
-
isNodeSelected
|
|
7056
|
-
|
|
7057
|
-
const
|
|
7058
|
-
|
|
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;
|
|
7811
|
+
return {
|
|
7812
|
+
x: sourceX + dx / distance * offset,
|
|
7813
|
+
y: sourceY + dy / distance * offset
|
|
7814
|
+
};
|
|
7059
7815
|
}
|
|
7060
7816
|
/**
|
|
7061
|
-
*
|
|
7817
|
+
* V1-compatible link shortening for target point
|
|
7062
7818
|
*/
|
|
7063
|
-
|
|
7064
|
-
|
|
7065
|
-
const
|
|
7066
|
-
|
|
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) {
|
|
7845
|
+
try {
|
|
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";
|
|
7857
|
+
ctx.beginPath();
|
|
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;
|
|
7067
7929
|
const sourceId1 = typeof link.source === "string" ? link.source : link.source.id;
|
|
7068
7930
|
const targetId1 = typeof link.target === "string" ? link.target : link.target.id;
|
|
7069
7931
|
const selectedLink = selectionState.selectedLink;
|
|
@@ -7072,151 +7934,857 @@ var Renderer = class {
|
|
|
7072
7934
|
return sourceId1 === sourceId2 && targetId1 === targetId2;
|
|
7073
7935
|
}
|
|
7074
7936
|
/**
|
|
7075
|
-
*
|
|
7076
|
-
* (either the link itself is selected, or its connected node is selected)
|
|
7937
|
+
* Get interaction state for a node (optimized for caching)
|
|
7077
7938
|
*/
|
|
7078
|
-
|
|
7079
|
-
|
|
7080
|
-
|
|
7081
|
-
|
|
7082
|
-
|
|
7083
|
-
|
|
7084
|
-
|
|
7085
|
-
|
|
7086
|
-
|
|
7087
|
-
|
|
7088
|
-
|
|
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();
|
|
7089
7995
|
}
|
|
7996
|
+
return obj;
|
|
7090
7997
|
}
|
|
7091
|
-
|
|
7092
|
-
|
|
7093
|
-
|
|
7094
|
-
|
|
7095
|
-
|
|
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);
|
|
7096
8006
|
}
|
|
7097
|
-
return false;
|
|
7098
8007
|
}
|
|
7099
8008
|
/**
|
|
7100
|
-
*
|
|
8009
|
+
* Pre-warm pool with initial objects
|
|
7101
8010
|
*/
|
|
7102
|
-
|
|
7103
|
-
|
|
7104
|
-
|
|
7105
|
-
const targetNode = typeof link.target === "string" ? this.nodeMap.get(link.target) : link.target;
|
|
7106
|
-
if (!sourceNode || !targetNode || sourceNode.x === void 0 || sourceNode.y === void 0 || targetNode.x === void 0 || targetNode.y === void 0) {
|
|
7107
|
-
return null;
|
|
8011
|
+
prewarm(count) {
|
|
8012
|
+
for (let i = 0; i < count; i++) {
|
|
8013
|
+
this.pool.push(this.createFn());
|
|
7108
8014
|
}
|
|
8015
|
+
}
|
|
8016
|
+
/**
|
|
8017
|
+
* Get pool statistics
|
|
8018
|
+
*/
|
|
8019
|
+
getStats() {
|
|
7109
8020
|
return {
|
|
7110
|
-
|
|
7111
|
-
|
|
8021
|
+
available: this.pool.length,
|
|
8022
|
+
maxSize: this.maxSize,
|
|
8023
|
+
utilization: (this.maxSize - this.pool.length) / this.maxSize
|
|
7112
8024
|
};
|
|
7113
8025
|
}
|
|
7114
8026
|
/**
|
|
7115
|
-
*
|
|
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
|
|
7116
8151
|
*/
|
|
7117
|
-
|
|
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;
|
|
7118
8323
|
try {
|
|
7119
|
-
|
|
7120
|
-
|
|
7121
|
-
|
|
7122
|
-
|
|
7123
|
-
|
|
7124
|
-
|
|
7125
|
-
|
|
7126
|
-
|
|
7127
|
-
|
|
7128
|
-
|
|
7129
|
-
|
|
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
|
+
}
|
|
7130
8392
|
}
|
|
7131
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);
|
|
7132
8410
|
} catch (error) {
|
|
7133
8411
|
ErrorHandler.logError(error);
|
|
7134
8412
|
}
|
|
8413
|
+
this.state.lastDragRenderTime = performance.now() - startTime;
|
|
7135
8414
|
}
|
|
7136
8415
|
/**
|
|
7137
|
-
*
|
|
8416
|
+
* Optimized label rendering with minimal object allocation
|
|
7138
8417
|
*/
|
|
7139
|
-
|
|
7140
|
-
|
|
7141
|
-
const
|
|
7142
|
-
|
|
7143
|
-
|
|
7144
|
-
|
|
7145
|
-
const
|
|
7146
|
-
|
|
7147
|
-
|
|
7148
|
-
|
|
7149
|
-
|
|
7150
|
-
|
|
7151
|
-
}
|
|
7152
|
-
|
|
7153
|
-
|
|
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();
|
|
7154
8504
|
return {
|
|
7155
|
-
|
|
7156
|
-
|
|
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
|
+
}
|
|
7157
8513
|
};
|
|
7158
8514
|
}
|
|
7159
8515
|
/**
|
|
7160
|
-
*
|
|
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
|
|
7161
8771
|
*/
|
|
7162
|
-
|
|
7163
|
-
|
|
7164
|
-
const sourceY = source.y ?? 0;
|
|
7165
|
-
const targetX = target.x ?? 0;
|
|
7166
|
-
const targetY = target.y ?? 0;
|
|
7167
|
-
const dx = targetX - sourceX;
|
|
7168
|
-
const dy = targetY - sourceY;
|
|
7169
|
-
const distance = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
7170
|
-
const targetNodeStyle = this.styleResolver.resolveNodeStyle({
|
|
7171
|
-
node: target,
|
|
7172
|
-
isHovered: this.isNodeHovered(target.id),
|
|
7173
|
-
isSelected: this.isNodeSelected(target.id)
|
|
7174
|
-
});
|
|
7175
|
-
const visualRadius = targetNodeStyle.radius + targetNodeStyle.strokeWidth / 2;
|
|
7176
|
-
const arrowLength = style.arrow?.enabled ? style.arrow.size ?? 4 : 0;
|
|
7177
|
-
const offset = style.arrow?.enabled ? visualRadius + arrowLength : visualRadius + 1;
|
|
7178
|
-
const shortenedPoint = {
|
|
7179
|
-
x: targetX - dx / distance * offset,
|
|
7180
|
-
y: targetY - dy / distance * offset
|
|
7181
|
-
};
|
|
7182
|
-
return shortenedPoint;
|
|
8772
|
+
getPerformanceMetrics() {
|
|
8773
|
+
return this.metricsManager.getMetrics();
|
|
7183
8774
|
}
|
|
7184
8775
|
/**
|
|
7185
|
-
*
|
|
8776
|
+
* Force log performance metrics immediately (for debugging)
|
|
7186
8777
|
*/
|
|
7187
|
-
|
|
7188
|
-
|
|
7189
|
-
|
|
7190
|
-
|
|
7191
|
-
const angle = Math.atan2(dy, dx);
|
|
7192
|
-
const arrowLength = arrowStyle.size ?? 4;
|
|
7193
|
-
const arrowTipX = targetPoint.x + arrowLength * Math.cos(angle);
|
|
7194
|
-
const arrowTipY = targetPoint.y + arrowLength * Math.sin(angle);
|
|
7195
|
-
const x1 = arrowTipX - arrowLength * Math.cos(angle - Math.PI / 6);
|
|
7196
|
-
const y1 = arrowTipY - arrowLength * Math.sin(angle - Math.PI / 6);
|
|
7197
|
-
const x22 = arrowTipX - arrowLength * Math.cos(angle + Math.PI / 6);
|
|
7198
|
-
const y22 = arrowTipY - arrowLength * Math.sin(angle + Math.PI / 6);
|
|
7199
|
-
ctx.fillStyle = arrowStyle.fill ?? "#000000";
|
|
7200
|
-
ctx.beginPath();
|
|
7201
|
-
ctx.moveTo(arrowTipX, arrowTipY);
|
|
7202
|
-
ctx.lineTo(x1, y1);
|
|
7203
|
-
ctx.lineTo(x22, y22);
|
|
7204
|
-
ctx.closePath();
|
|
7205
|
-
ctx.fill();
|
|
7206
|
-
} catch (error) {
|
|
7207
|
-
ErrorHandler.logError(error);
|
|
7208
|
-
}
|
|
8778
|
+
forceLogMetrics() {
|
|
8779
|
+
const nodeCount = this.config?.nodes.length || 0;
|
|
8780
|
+
const linkCount = this.config?.links.length || 0;
|
|
8781
|
+
this.metricsManager.logMetrics(nodeCount, linkCount);
|
|
7209
8782
|
}
|
|
7210
8783
|
/**
|
|
7211
|
-
*
|
|
8784
|
+
* Calculate midpoint of a link for label positioning (delegates to StateManager)
|
|
7212
8785
|
*/
|
|
7213
|
-
|
|
7214
|
-
this.
|
|
7215
|
-
ctx,
|
|
7216
|
-
{ x: source.x, y: source.y },
|
|
7217
|
-
{ x: target.x, y: target.y },
|
|
7218
|
-
arrowStyle
|
|
7219
|
-
);
|
|
8786
|
+
getLinkMidpoint(link) {
|
|
8787
|
+
return this.stateManager.getLinkMidpoint(link);
|
|
7220
8788
|
}
|
|
7221
8789
|
/**
|
|
7222
8790
|
* Initialize node positions if needed
|
|
@@ -7241,6 +8809,29 @@ var Renderer = class {
|
|
|
7241
8809
|
if (!this.config) return;
|
|
7242
8810
|
try {
|
|
7243
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
|
+
});
|
|
7244
8835
|
} catch (error) {
|
|
7245
8836
|
ErrorHandler.logError(error);
|
|
7246
8837
|
}
|
|
@@ -7263,247 +8854,22 @@ var Renderer = class {
|
|
|
7263
8854
|
};
|
|
7264
8855
|
}
|
|
7265
8856
|
/**
|
|
7266
|
-
* Debug shadow canvas export (
|
|
8857
|
+
* Debug shadow canvas export (delegates to HitDetectionRenderer)
|
|
7267
8858
|
*/
|
|
7268
8859
|
debugShadowCanvas() {
|
|
7269
|
-
|
|
7270
|
-
if (!this.canvasState) return;
|
|
7271
|
-
const { shadowCanvas } = this.canvasState;
|
|
7272
|
-
const link = document.createElement("a");
|
|
7273
|
-
link.download = "shadow-canvas-debug.png";
|
|
7274
|
-
link.href = shadowCanvas.toDataURL("image/png");
|
|
7275
|
-
link.click();
|
|
7276
|
-
} catch (error) {
|
|
7277
|
-
ErrorHandler.logError(error);
|
|
7278
|
-
}
|
|
7279
|
-
}
|
|
7280
|
-
/**
|
|
7281
|
-
* Build node index for O(1) lookups (Step 3 optimization)
|
|
7282
|
-
*/
|
|
7283
|
-
buildNodeIndex() {
|
|
7284
|
-
if (!this.config) return;
|
|
7285
|
-
try {
|
|
7286
|
-
this.nodeMap.clear();
|
|
7287
|
-
for (const node of this.config.nodes) {
|
|
7288
|
-
this.nodeMap.set(node.id, node);
|
|
7289
|
-
}
|
|
7290
|
-
for (const link of this.config.links) {
|
|
7291
|
-
if (typeof link.source === "string") {
|
|
7292
|
-
const sourceNode = this.nodeMap.get(link.source);
|
|
7293
|
-
if (sourceNode) {
|
|
7294
|
-
link.source = sourceNode;
|
|
7295
|
-
}
|
|
7296
|
-
}
|
|
7297
|
-
if (typeof link.target === "string") {
|
|
7298
|
-
const targetNode = this.nodeMap.get(link.target);
|
|
7299
|
-
if (targetNode) {
|
|
7300
|
-
link.target = targetNode;
|
|
7301
|
-
}
|
|
7302
|
-
}
|
|
7303
|
-
}
|
|
7304
|
-
} catch (error) {
|
|
7305
|
-
ErrorHandler.logError(error);
|
|
7306
|
-
}
|
|
7307
|
-
}
|
|
7308
|
-
/**
|
|
7309
|
-
* Get node by ID using O(1) lookup (Step 3 optimization)
|
|
7310
|
-
*/
|
|
7311
|
-
getNodeById(nodeId) {
|
|
7312
|
-
return this.nodeMap.get(nodeId);
|
|
7313
|
-
}
|
|
7314
|
-
/**
|
|
7315
|
-
* Render with z-index layers (for renderWithTransform)
|
|
7316
|
-
*/
|
|
7317
|
-
renderWithLayers(ctx) {
|
|
7318
|
-
if (!this.config) return;
|
|
7319
|
-
try {
|
|
7320
|
-
const hoveredNode = this.getHoveredNode();
|
|
7321
|
-
const selectedNode = this.getSelectedNode();
|
|
7322
|
-
const hoveredLink = this.getHoveredLink();
|
|
7323
|
-
const selectedLink = this.getSelectedLink();
|
|
7324
|
-
const nodeHighlightChecker = ZIndexManager.createNodeHighlightChecker(
|
|
7325
|
-
hoveredNode?.id || null,
|
|
7326
|
-
selectedNode?.id || null
|
|
7327
|
-
);
|
|
7328
|
-
const linkHighlightChecker = ZIndexManager.createLinkHighlightChecker(
|
|
7329
|
-
hoveredNode?.id || null,
|
|
7330
|
-
selectedNode?.id || null,
|
|
7331
|
-
hoveredLink ? this.getLinkId(hoveredLink) : null,
|
|
7332
|
-
selectedLink ? this.getLinkId(selectedLink) : null
|
|
7333
|
-
);
|
|
7334
|
-
const linkLayers = ZIndexManager.separateIntoLayers(this.config.links, linkHighlightChecker);
|
|
7335
|
-
const nodeLayers = ZIndexManager.separateIntoLayers(this.config.nodes, nodeHighlightChecker);
|
|
7336
|
-
this.renderLinksLayer(ctx, linkLayers.background);
|
|
7337
|
-
this.renderLinkLabelsLayer(ctx, linkLayers.background);
|
|
7338
|
-
this.renderNodesWithLabelsLayer(ctx, nodeLayers.background);
|
|
7339
|
-
this.renderLinksLayer(ctx, linkLayers.foreground);
|
|
7340
|
-
this.renderLinkLabelsLayer(ctx, linkLayers.foreground);
|
|
7341
|
-
this.renderNodesWithLabelsLayer(ctx, nodeLayers.foreground);
|
|
7342
|
-
} catch (error) {
|
|
7343
|
-
ErrorHandler.logError(error);
|
|
7344
|
-
}
|
|
7345
|
-
}
|
|
7346
|
-
/**
|
|
7347
|
-
* Render with z-index layers and performance metrics (for main render)
|
|
7348
|
-
*/
|
|
7349
|
-
renderWithLayersAndMetrics(ctx) {
|
|
7350
|
-
if (!this.config) return;
|
|
7351
|
-
try {
|
|
7352
|
-
const hoveredNode = this.getHoveredNode();
|
|
7353
|
-
const selectedNode = this.getSelectedNode();
|
|
7354
|
-
const hoveredLink = this.getHoveredLink();
|
|
7355
|
-
const selectedLink = this.getSelectedLink();
|
|
7356
|
-
const nodeHighlightChecker = ZIndexManager.createNodeHighlightChecker(
|
|
7357
|
-
hoveredNode?.id || null,
|
|
7358
|
-
selectedNode?.id || null
|
|
7359
|
-
);
|
|
7360
|
-
const linkHighlightChecker = ZIndexManager.createLinkHighlightChecker(
|
|
7361
|
-
hoveredNode?.id || null,
|
|
7362
|
-
selectedNode?.id || null,
|
|
7363
|
-
hoveredLink ? this.getLinkId(hoveredLink) : null,
|
|
7364
|
-
selectedLink ? this.getLinkId(selectedLink) : null
|
|
7365
|
-
);
|
|
7366
|
-
const linkLayers = ZIndexManager.separateIntoLayers(this.config.links, linkHighlightChecker);
|
|
7367
|
-
const nodeLayers = ZIndexManager.separateIntoLayers(this.config.nodes, nodeHighlightChecker);
|
|
7368
|
-
let stepStart = performance.now();
|
|
7369
|
-
this.renderLinksLayer(ctx, linkLayers.background);
|
|
7370
|
-
this.renderLinksLayer(ctx, linkLayers.foreground);
|
|
7371
|
-
this.performanceMetrics.renderLinks += performance.now() - stepStart;
|
|
7372
|
-
stepStart = performance.now();
|
|
7373
|
-
this.renderNodesLayer(ctx, nodeLayers.background);
|
|
7374
|
-
this.renderNodesLayer(ctx, nodeLayers.foreground);
|
|
7375
|
-
this.performanceMetrics.renderNodes += performance.now() - stepStart;
|
|
7376
|
-
stepStart = performance.now();
|
|
7377
|
-
this.renderLinkLabelsLayer(ctx, linkLayers.background);
|
|
7378
|
-
this.renderLinkLabelsLayer(ctx, linkLayers.foreground);
|
|
7379
|
-
this.performanceMetrics.renderLinkLabels += performance.now() - stepStart;
|
|
7380
|
-
stepStart = performance.now();
|
|
7381
|
-
this.renderNodeLabelsLayer(ctx, nodeLayers.background);
|
|
7382
|
-
this.renderNodeLabelsLayer(ctx, nodeLayers.foreground);
|
|
7383
|
-
this.performanceMetrics.renderNodeLabels += performance.now() - stepStart;
|
|
7384
|
-
} catch (error) {
|
|
7385
|
-
ErrorHandler.logError(error);
|
|
7386
|
-
}
|
|
7387
|
-
}
|
|
7388
|
-
/**
|
|
7389
|
-
* Helper methods for getting current interaction states
|
|
7390
|
-
*/
|
|
7391
|
-
getHoveredNode() {
|
|
7392
|
-
if (!this.hoverManager) return null;
|
|
7393
|
-
const hoverState = this.hoverManager.getHoverState();
|
|
7394
|
-
return hoverState.currentHovered?.d.entityType === "Node" ? hoverState.currentHovered.d : null;
|
|
7395
|
-
}
|
|
7396
|
-
getSelectedNode() {
|
|
7397
|
-
if (!this.selectionManager) return null;
|
|
7398
|
-
const selectionState = this.selectionManager.getSelectionState();
|
|
7399
|
-
return selectionState.selectedNode;
|
|
7400
|
-
}
|
|
7401
|
-
getHoveredLink() {
|
|
7402
|
-
if (!this.hoverManager) return null;
|
|
7403
|
-
const hoverState = this.hoverManager.getHoverState();
|
|
7404
|
-
return hoverState.currentHovered?.d.entityType === "Link" ? hoverState.currentHovered.d : null;
|
|
7405
|
-
}
|
|
7406
|
-
getSelectedLink() {
|
|
7407
|
-
if (!this.selectionManager) return null;
|
|
7408
|
-
const selectionState = this.selectionManager.getSelectionState();
|
|
7409
|
-
return selectionState.selectedLink;
|
|
7410
|
-
}
|
|
7411
|
-
/**
|
|
7412
|
-
* Layer-specific rendering methods (render subsets of entities)
|
|
7413
|
-
*/
|
|
7414
|
-
renderNodesWithLabelsLayer(ctx, nodes) {
|
|
7415
|
-
if (!this.config || !this.styleResolver) return;
|
|
7416
|
-
const nodeCount = this.config.nodes.length;
|
|
7417
|
-
const isLargeGraph = nodeCount > 1e4;
|
|
7418
|
-
let currentZoom = 1;
|
|
7419
|
-
let isZoomedOutForNodes = false;
|
|
7420
|
-
if (isLargeGraph) {
|
|
7421
|
-
currentZoom = this.canvasState ? transform(this.canvasState.canvas).k : 1;
|
|
7422
|
-
isZoomedOutForNodes = currentZoom <= 0.8;
|
|
7423
|
-
}
|
|
7424
|
-
const nodeStateCache = /* @__PURE__ */ new Map();
|
|
7425
|
-
for (const node of nodes) {
|
|
7426
|
-
if (!node.x || !node.y) continue;
|
|
7427
|
-
const nodeId = node.id;
|
|
7428
|
-
let nodeState = nodeStateCache.get(nodeId);
|
|
7429
|
-
if (!nodeState) {
|
|
7430
|
-
nodeState = {
|
|
7431
|
-
isHovered: this.isNodeHovered(nodeId),
|
|
7432
|
-
isSelected: this.isNodeSelected(nodeId)
|
|
7433
|
-
};
|
|
7434
|
-
nodeStateCache.set(nodeId, nodeState);
|
|
7435
|
-
}
|
|
7436
|
-
const nodeStyle = this.styleResolver.resolveNodeStyle({
|
|
7437
|
-
node,
|
|
7438
|
-
isHovered: nodeState.isHovered,
|
|
7439
|
-
isSelected: nodeState.isSelected
|
|
7440
|
-
});
|
|
7441
|
-
ctx.beginPath();
|
|
7442
|
-
ctx.arc(node.x, node.y, nodeStyle.radius, 0, 2 * Math.PI);
|
|
7443
|
-
ctx.fillStyle = nodeStyle.fill;
|
|
7444
|
-
ctx.fill();
|
|
7445
|
-
if (nodeStyle.stroke && nodeStyle.strokeWidth > 0) {
|
|
7446
|
-
ctx.strokeStyle = nodeStyle.stroke;
|
|
7447
|
-
ctx.lineWidth = nodeStyle.strokeWidth;
|
|
7448
|
-
ctx.stroke();
|
|
7449
|
-
}
|
|
7450
|
-
if (isLargeGraph && isZoomedOutForNodes) {
|
|
7451
|
-
continue;
|
|
7452
|
-
}
|
|
7453
|
-
const nodeStyleWithLabel = nodeStyle;
|
|
7454
|
-
if (nodeStyleWithLabel.label && !nodeStyleWithLabel.label.enabled) continue;
|
|
7455
|
-
const fullLabel = node.label || node.id;
|
|
7456
|
-
const defaultStyle = {
|
|
7457
|
-
font: "9px sans-serif",
|
|
7458
|
-
textAlign: "center",
|
|
7459
|
-
textBaseline: "middle",
|
|
7460
|
-
fillStyle: "#ffffff",
|
|
7461
|
-
offsetY: 0
|
|
7462
|
-
};
|
|
7463
|
-
if (nodeStyleWithLabel.label) {
|
|
7464
|
-
ctx.font = nodeStyleWithLabel.label.font || defaultStyle.font;
|
|
7465
|
-
ctx.textAlign = nodeStyleWithLabel.label.textAlign || defaultStyle.textAlign;
|
|
7466
|
-
ctx.textBaseline = nodeStyleWithLabel.label.textBaseline || defaultStyle.textBaseline;
|
|
7467
|
-
ctx.fillStyle = nodeStyleWithLabel.label.textColor || defaultStyle.fillStyle;
|
|
7468
|
-
} else {
|
|
7469
|
-
ctx.font = defaultStyle.font;
|
|
7470
|
-
ctx.textAlign = defaultStyle.textAlign;
|
|
7471
|
-
ctx.textBaseline = defaultStyle.textBaseline;
|
|
7472
|
-
ctx.fillStyle = defaultStyle.fillStyle;
|
|
7473
|
-
}
|
|
7474
|
-
const maxWidth = nodeStyle.radius * 2 - 6;
|
|
7475
|
-
const truncatedLabel = this.truncateLabel(ctx, fullLabel, maxWidth);
|
|
7476
|
-
const labelY = node.y + (nodeStyleWithLabel.label?.offsetY || defaultStyle.offsetY);
|
|
7477
|
-
ctx.fillText(truncatedLabel, node.x, labelY);
|
|
7478
|
-
}
|
|
7479
|
-
}
|
|
7480
|
-
/**
|
|
7481
|
-
* Helper method to truncate labels (copied from NodeLabelsRenderer)
|
|
7482
|
-
*/
|
|
7483
|
-
truncateLabel(ctx, label, maxWidth) {
|
|
7484
|
-
let truncatedLabel = label;
|
|
7485
|
-
if (ctx.measureText(truncatedLabel).width <= maxWidth) {
|
|
7486
|
-
return truncatedLabel;
|
|
7487
|
-
}
|
|
7488
|
-
while (truncatedLabel.length > 1 && ctx.measureText(`${truncatedLabel}\u2026`).width > maxWidth) {
|
|
7489
|
-
truncatedLabel = truncatedLabel.slice(0, -1);
|
|
7490
|
-
}
|
|
7491
|
-
return truncatedLabel.length < label.length ? `${truncatedLabel}\u2026` : truncatedLabel;
|
|
8860
|
+
this.hitDetectionRenderer.debugShadowCanvas();
|
|
7492
8861
|
}
|
|
7493
8862
|
renderLinksLayer(ctx, links) {
|
|
7494
8863
|
if (!this.config || !this.styleResolver) return;
|
|
7495
8864
|
const linkStateCache = /* @__PURE__ */ new Map();
|
|
7496
8865
|
for (const link of links) {
|
|
7497
|
-
const sourceNode = typeof link.source === "string" ? this.
|
|
7498
|
-
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;
|
|
7499
8868
|
if (sourceNode && targetNode && sourceNode.x && sourceNode.y && targetNode.x && targetNode.y) {
|
|
7500
8869
|
const linkId = this.getLinkId(link);
|
|
7501
8870
|
let linkState = linkStateCache.get(linkId);
|
|
7502
8871
|
if (!linkState) {
|
|
7503
|
-
linkState =
|
|
7504
|
-
isHovered: this.isLinkHovered(link),
|
|
7505
|
-
isSelected: this.isLinkSelected(link)
|
|
7506
|
-
};
|
|
8872
|
+
linkState = this.interactionResolver.getLinkState(link);
|
|
7507
8873
|
linkStateCache.set(linkId, linkState);
|
|
7508
8874
|
}
|
|
7509
8875
|
const style = this.styleResolver.resolveLinkStyle({
|
|
@@ -7511,85 +8877,18 @@ var Renderer = class {
|
|
|
7511
8877
|
isHovered: linkState.isHovered,
|
|
7512
8878
|
isSelected: linkState.isSelected
|
|
7513
8879
|
});
|
|
7514
|
-
this.
|
|
7515
|
-
|
|
7516
|
-
|
|
7517
|
-
|
|
7518
|
-
|
|
7519
|
-
|
|
7520
|
-
|
|
7521
|
-
|
|
7522
|
-
|
|
7523
|
-
|
|
7524
|
-
this.hasLoggedLargeGraphOptimization = true;
|
|
7525
|
-
}
|
|
7526
|
-
if (isLargeGraph) {
|
|
7527
|
-
const currentZoom = this.canvasState ? transform(this.canvasState.canvas).k : 1;
|
|
7528
|
-
const isZoomedOut = currentZoom <= 1;
|
|
7529
|
-
if (isZoomedOut) {
|
|
7530
|
-
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
|
+
);
|
|
7531
8890
|
}
|
|
7532
8891
|
}
|
|
7533
|
-
const linkStateCache = /* @__PURE__ */ new Map();
|
|
7534
|
-
LinkLabelsRenderer.renderWithVisibility(
|
|
7535
|
-
ctx,
|
|
7536
|
-
links,
|
|
7537
|
-
(link) => {
|
|
7538
|
-
if (!link.label) {
|
|
7539
|
-
return null;
|
|
7540
|
-
}
|
|
7541
|
-
const linkId = this.getLinkId(link);
|
|
7542
|
-
let linkState = linkStateCache.get(linkId);
|
|
7543
|
-
if (!linkState) {
|
|
7544
|
-
linkState = {
|
|
7545
|
-
isHovered: this.isLinkHovered(link),
|
|
7546
|
-
isSelected: this.isLinkSelected(link)
|
|
7547
|
-
};
|
|
7548
|
-
linkStateCache.set(linkId, linkState);
|
|
7549
|
-
}
|
|
7550
|
-
if (isLargeGraph) {
|
|
7551
|
-
const isInteractive = linkState.isHovered || linkState.isSelected;
|
|
7552
|
-
if (!isInteractive) {
|
|
7553
|
-
return null;
|
|
7554
|
-
}
|
|
7555
|
-
}
|
|
7556
|
-
const style = this.styleResolver.resolveLinkStyle({
|
|
7557
|
-
link,
|
|
7558
|
-
isHovered: linkState.isHovered,
|
|
7559
|
-
isSelected: linkState.isSelected
|
|
7560
|
-
});
|
|
7561
|
-
return style.label || null;
|
|
7562
|
-
},
|
|
7563
|
-
(link) => this.getLinkMidpoint(link),
|
|
7564
|
-
(linkId) => {
|
|
7565
|
-
let linkState = linkStateCache.get(linkId);
|
|
7566
|
-
if (!linkState) {
|
|
7567
|
-
const link = links.find((l) => this.getLinkId(l) === linkId);
|
|
7568
|
-
if (link) {
|
|
7569
|
-
linkState = {
|
|
7570
|
-
isHovered: this.isLinkHovered(link),
|
|
7571
|
-
isSelected: this.isLinkSelected(link)
|
|
7572
|
-
};
|
|
7573
|
-
linkStateCache.set(linkId, linkState);
|
|
7574
|
-
}
|
|
7575
|
-
}
|
|
7576
|
-
return linkState?.isHovered || false;
|
|
7577
|
-
},
|
|
7578
|
-
(linkId) => {
|
|
7579
|
-
let linkState = linkStateCache.get(linkId);
|
|
7580
|
-
if (!linkState) {
|
|
7581
|
-
const link = links.find((l) => this.getLinkId(l) === linkId);
|
|
7582
|
-
if (link) {
|
|
7583
|
-
linkState = {
|
|
7584
|
-
isHovered: this.isLinkHovered(link),
|
|
7585
|
-
isSelected: this.isLinkSelected(link)
|
|
7586
|
-
};
|
|
7587
|
-
linkStateCache.set(linkId, linkState);
|
|
7588
|
-
}
|
|
7589
|
-
}
|
|
7590
|
-
return linkState?.isSelected || false;
|
|
7591
|
-
}
|
|
7592
|
-
);
|
|
7593
8892
|
}
|
|
7594
8893
|
renderNodesLayer(ctx, nodes) {
|
|
7595
8894
|
if (!this.config || !this.styleResolver) return;
|
|
@@ -7601,9 +8900,11 @@ var Renderer = class {
|
|
|
7601
8900
|
(nodeId) => {
|
|
7602
8901
|
let nodeState = nodeStateCache.get(nodeId);
|
|
7603
8902
|
if (!nodeState) {
|
|
8903
|
+
const interactionState = this.interactionResolver.getNodeState(nodeId);
|
|
7604
8904
|
nodeState = {
|
|
7605
|
-
isHovered:
|
|
7606
|
-
isSelected:
|
|
8905
|
+
isHovered: interactionState.isHovered,
|
|
8906
|
+
isSelected: interactionState.isSelected,
|
|
8907
|
+
isHighlighted: this.stateManager.isNodeHighlighted(nodeId)
|
|
7607
8908
|
};
|
|
7608
8909
|
nodeStateCache.set(nodeId, nodeState);
|
|
7609
8910
|
}
|
|
@@ -7612,13 +8913,28 @@ var Renderer = class {
|
|
|
7612
8913
|
(nodeId) => {
|
|
7613
8914
|
let nodeState = nodeStateCache.get(nodeId);
|
|
7614
8915
|
if (!nodeState) {
|
|
8916
|
+
const interactionState = this.interactionResolver.getNodeState(nodeId);
|
|
7615
8917
|
nodeState = {
|
|
7616
|
-
isHovered:
|
|
7617
|
-
isSelected:
|
|
8918
|
+
isHovered: interactionState.isHovered,
|
|
8919
|
+
isSelected: interactionState.isSelected,
|
|
8920
|
+
isHighlighted: this.stateManager.isNodeHighlighted(nodeId)
|
|
7618
8921
|
};
|
|
7619
8922
|
nodeStateCache.set(nodeId, nodeState);
|
|
7620
8923
|
}
|
|
7621
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;
|
|
7622
8938
|
}
|
|
7623
8939
|
);
|
|
7624
8940
|
}
|
|
@@ -7641,10 +8957,7 @@ var Renderer = class {
|
|
|
7641
8957
|
(nodeId) => {
|
|
7642
8958
|
let nodeState = nodeStateCache.get(nodeId);
|
|
7643
8959
|
if (!nodeState) {
|
|
7644
|
-
nodeState =
|
|
7645
|
-
isHovered: this.isNodeHovered(nodeId),
|
|
7646
|
-
isSelected: this.isNodeSelected(nodeId)
|
|
7647
|
-
};
|
|
8960
|
+
nodeState = this.interactionResolver.getNodeState(nodeId);
|
|
7648
8961
|
nodeStateCache.set(nodeId, nodeState);
|
|
7649
8962
|
}
|
|
7650
8963
|
return nodeState.isHovered;
|
|
@@ -7652,28 +8965,33 @@ var Renderer = class {
|
|
|
7652
8965
|
(nodeId) => {
|
|
7653
8966
|
let nodeState = nodeStateCache.get(nodeId);
|
|
7654
8967
|
if (!nodeState) {
|
|
7655
|
-
nodeState =
|
|
7656
|
-
isHovered: this.isNodeHovered(nodeId),
|
|
7657
|
-
isSelected: this.isNodeSelected(nodeId)
|
|
7658
|
-
};
|
|
8968
|
+
nodeState = this.interactionResolver.getNodeState(nodeId);
|
|
7659
8969
|
nodeStateCache.set(nodeId, nodeState);
|
|
7660
8970
|
}
|
|
7661
8971
|
return nodeState.isSelected;
|
|
7662
8972
|
}
|
|
7663
8973
|
);
|
|
7664
8974
|
}
|
|
8975
|
+
/**
|
|
8976
|
+
* Get the state manager for highlight operations
|
|
8977
|
+
*/
|
|
8978
|
+
getStateManager() {
|
|
8979
|
+
return this.stateManager;
|
|
8980
|
+
}
|
|
7665
8981
|
/**
|
|
7666
8982
|
* Destroy renderer and clean up resources
|
|
7667
8983
|
*/
|
|
7668
8984
|
destroy() {
|
|
7669
8985
|
try {
|
|
8986
|
+
this.zIndexRenderer.destroy();
|
|
8987
|
+
this.zoomRenderer.destroy();
|
|
8988
|
+
this.hitDetectionRenderer.destroy();
|
|
8989
|
+
this.dragOptimizer.destroy();
|
|
7670
8990
|
this.config = void 0;
|
|
7671
8991
|
this.canvasState = void 0;
|
|
7672
8992
|
this.hoverManager = void 0;
|
|
7673
8993
|
this.styleResolver = void 0;
|
|
7674
|
-
this.
|
|
7675
|
-
this.shadowCanvasDirty = false;
|
|
7676
|
-
this.lastShadowRenderTime = 0;
|
|
8994
|
+
this.stateManager.destroy();
|
|
7677
8995
|
} catch (error) {
|
|
7678
8996
|
ErrorHandler.logError(error);
|
|
7679
8997
|
}
|
|
@@ -8124,13 +9442,22 @@ var V2Graph = class {
|
|
|
8124
9442
|
// pointerManager: this.pointerManager,
|
|
8125
9443
|
physicsManager: this.physicsManager,
|
|
8126
9444
|
hoverManager: this.hoverManager,
|
|
9445
|
+
renderer: this.renderer,
|
|
9446
|
+
// Pass renderer for drag state management
|
|
8127
9447
|
onRender: () => this.renderer.renderWithTransform()
|
|
8128
9448
|
});
|
|
8129
9449
|
this.coordinateDragHover();
|
|
8130
9450
|
this.zoomManager.initialize({
|
|
8131
9451
|
canvas: canvasState.canvas,
|
|
8132
9452
|
canvasManager: this.canvasManager,
|
|
8133
|
-
|
|
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
|
+
},
|
|
8134
9461
|
isOverEntity: () => {
|
|
8135
9462
|
const hoverState = this.hoverManager.getHoverState();
|
|
8136
9463
|
return hoverState.currentHovered !== null;
|
|
@@ -8642,6 +9969,76 @@ var V2Graph = class {
|
|
|
8642
9969
|
}
|
|
8643
9970
|
});
|
|
8644
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
|
+
}
|
|
8645
10042
|
/**
|
|
8646
10043
|
* Destroy the graph and clean up resources
|
|
8647
10044
|
*/
|