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.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
- nodeMap = /* @__PURE__ */ new Map();
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.buildNodeIndex();
2035
- const linkDistance = nodeCount > 1e4 ? 220 : nodeCount > 5e3 ? 190 : nodeCount > 2e3 ? 170 : 150;
2036
- const chargeStrength = nodeCount > 1e4 ? -350 : nodeCount > 5e3 ? -400 : nodeCount > 2e3 ? -450 : -500;
2037
- const collisionRadius = nodeCount > 1e4 ? 1 : nodeCount > 5e3 ? 2 : 2;
2038
- const collisionIterations = nodeCount > 1e4 ? 1 : nodeCount > 5e3 ? 1 : 2;
2039
- const centerStrength = nodeCount > 5e3 ? 0.15 : 0.5;
2040
- const linkStrength = nodeCount > 1e4 ? 0.15 : nodeCount > 5e3 ? 0.25 : 0.4;
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 > 5e3 ? 1.2 : 0.9).distanceMax(nodeCount > 5e3 ? 500 : 1e3)
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 > 5e3 ? 0.65 : adaptiveVelocityDecay
2248
+ nodeCount > 1e4 ? 0.4 : nodeCount > 6e3 ? 0.5 : adaptiveVelocityDecay
2055
2249
  ).alphaDecay(
2056
- nodeCount > 5e3 ? 0.05 : adaptiveAlphaDecay
2250
+ nodeCount > 1e4 ? 0.01 : nodeCount > 6e3 ? 0.02 : adaptiveAlphaDecay
2057
2251
  ).alphaMin(
2058
- nodeCount > 5e3 ? 0.05 : 1e-3
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 ? 5e-3 : nodeCount > 5e3 ? 0.01 : nodeCount > 2e3 ? 0.02 : 0.1);
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.nodeMap.get(link.source) : link.source;
2235
- const targetNode = typeof link.target === "string" ? this.nodeMap.get(link.target) : link.target;
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 120;
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
- return Math.max(80, Math.min(200, 120 + nodeAreaRatio * 20));
2478
+ const baseDistance = nodeCount > 1e4 ? 300 : nodeCount > 6e3 ? 250 : nodeCount > 2e3 ? 200 : 150;
2479
+ const areaAdjustment = Math.min(100, nodeAreaRatio * 30);
2480
+ return Math.max(baseDistance, baseDistance + areaAdjustment);
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 if not set
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
- for (const node of this.config.nodes) {
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
- node.x = Math.random() * this.config.width;
2277
- node.y = Math.random() * this.config.height;
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.01 : nodeCount > 5e3 ? 0.02 : nodeCount > 2e3 ? 0.05 : 0.3;
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.nodeMap.clear();
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.config.onRender();
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.canvas.style.cursor = "grabbing";
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
- try {
5382
- event.sourceEvent?.stopPropagation();
5383
- event.sourceEvent?.preventDefault();
5384
- const transform2 = event.transform;
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
- const currentCursor = this.config.canvas.style.cursor;
5403
- if (currentCursor === "grabbing") {
5404
- this.config.canvas.style.cursor = "grab";
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
- this.container.addEventListener(evType, (ev) => {
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
- }, { passive: true });
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
- this.container.addEventListener("click", (event) => {
6235
+ const clickHandler = (event) => {
5912
6236
  this.handleSelectionClick(event);
5913
- }, { passive: false });
5914
- document.addEventListener("keydown", (event) => {
5915
- if (event.key === "Escape") {
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.handleSelectionClick.bind(this));
6479
+ if (this.container && this.boundHandlers.has("click")) {
6480
+ this.container.removeEventListener("click", this.boundHandlers.get("click"));
6152
6481
  }
6153
- document.removeEventListener("keydown", (event) => {
6154
- if (event.key === "Escape") {
6155
- this.clearSelection();
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 Renderer = class {
7444
+ // src/v2/rendering/hit-detection-renderer.ts
7445
+ var HitDetectionRenderer = class {
6608
7446
  config;
6609
- canvasState;
6610
- hoverManager;
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 the renderer
7454
+ * Initialize hit detection renderer
6639
7455
  */
6640
- initialize(config, canvasState, hoverManager, selectionManager) {
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
- this.config = config;
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 (Step 6 optimization)
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 (Step 6 optimization)
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.canvasState) return;
7498
+ if (!this.config) return;
6738
7499
  try {
6739
- const { width, height } = this.canvasState;
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
- throw new RenderError("Failed to clear canvas");
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 (force-graph pattern)
7521
+ * Render shadow links with __indexColor for hit detection
6804
7522
  */
6805
7523
  renderShadowLinks(shadowCtx) {
6806
- if (!this.config || !this.styleResolver) return;
7524
+ if (!this.config) return;
6807
7525
  try {
6808
- const { links } = this.config;
6809
- const defaultLinkStyle = this.styleResolver.resolveLinkStyle({
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" ? this.nodeMap.get(link.source) : link.source;
6815
- const targetNode = typeof link.target === "string" ? this.nodeMap.get(link.target) : link.target;
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 + this.linkHoverPrecision;
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 || !this.styleResolver) return;
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 this.config.links) {
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
- this.config.links,
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: this.isLinkHovered(link),
6856
- isSelected: this.isLinkSelected(link)
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 = this.styleResolver.resolveLinkStyle({
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
- (linkId) => {
6869
- let linkState = linkStateCache.get(linkId);
6870
- if (!linkState) {
6871
- const link = linkIdToLinkMap.get(linkId);
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 (force-graph pattern)
7608
+ * Render shadow nodes with __indexColor for hit detection
6912
7609
  */
6913
7610
  renderShadowNodes(shadowCtx) {
6914
- if (!this.config || !this.styleResolver) return;
7611
+ if (!this.config) return;
6915
7612
  try {
6916
- const { nodes } = this.config;
6917
- const defaultNodeStyle = this.styleResolver.resolveNodeStyle({
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 currently hovered node ID directly (Critical Performance Fix)
7627
+ * Get unique link ID for tracking (delegates to StateManager)
6931
7628
  */
6932
- getCurrentlyHoveredNodeId() {
6933
- if (!this.hoverManager) return null;
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
- * Get currently selected node ID directly (Critical Performance Fix)
7633
+ * Calculate midpoint of a link for label positioning (delegates to StateManager)
6943
7634
  */
6944
- getCurrentlySelectedNodeId() {
6945
- if (!this.selectionManager) return null;
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
- * Get currently hovered link (Critical Performance Fix)
7639
+ * Update configuration
6951
7640
  */
6952
- getCurrentlyHoveredLink() {
6953
- if (!this.hoverManager) return null;
6954
- const hoverState = this.hoverManager.getHoverState();
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
- * Get currently selected link (Critical Performance Fix)
7647
+ * Debug shadow canvas export
6962
7648
  */
6963
- getCurrentlySelectedLink() {
6964
- if (!this.selectionManager) return null;
6965
- const selectionState = this.selectionManager.getSelectionState();
6966
- return selectionState.selectedLink || null;
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
- * Log performance metrics for analysis
7663
+ * Get hit detection stats
6970
7664
  */
6971
- logPerformanceMetrics() {
6972
- const frames = this.performanceMetrics.frameCount;
6973
- const nodeCount = this.config?.nodes.length || 0;
6974
- const linkCount = this.config?.links.length || 0;
6975
- console.log("\u{1F50D} PERFORMANCE METRICS (avg per frame over", frames, "frames):");
6976
- console.log("\u{1F4CA} Graph size:", nodeCount, "nodes,", linkCount, "links");
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
- * Reset performance metrics
7673
+ * Destroy and clean up
6989
7674
  */
6990
- resetPerformanceMetrics() {
6991
- this.performanceMetrics = {
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
- * Get current performance metrics
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
- forceLogMetrics() {
7013
- this.logPerformanceMetrics();
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 a node is currently hovered
7748
+ * Check if it's time to log metrics (every N frames)
7017
7749
  */
7018
- isNodeHovered(nodeId) {
7019
- if (!this.hoverManager) return false;
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
- * Check if a link is currently hovered (either directly or through associated node hover)
7758
+ * Render a directed link with optional arrow head
7029
7759
  */
7030
- isLinkHovered(link) {
7031
- if (!this.hoverManager) return false;
7032
- const hoverState = this.hoverManager.getHoverState();
7033
- if (!hoverState.currentHovered) return false;
7034
- if (hoverState.currentHovered.d.entityType === "Link") {
7035
- const hoveredLink = hoverState.currentHovered.d;
7036
- if (!hoveredLink) return false;
7037
- const sourceId1 = typeof link.source === "string" ? link.source : link.source.id;
7038
- const targetId1 = typeof link.target === "string" ? link.target : link.target.id;
7039
- const sourceId2 = typeof hoveredLink.source === "string" ? hoveredLink.source : hoveredLink.source.id;
7040
- const targetId2 = typeof hoveredLink.target === "string" ? hoveredLink.target : hoveredLink.target.id;
7041
- return sourceId1 === sourceId2 && targetId1 === targetId2;
7042
- }
7043
- if (hoverState.currentHovered.d.entityType === "Node") {
7044
- const hoveredNode = hoverState.currentHovered.d;
7045
- if (!hoveredNode) return false;
7046
- const linkSourceId = typeof link.source === "string" ? link.source : link.source.id;
7047
- const linkTargetId = typeof link.target === "string" ? link.target : link.target.id;
7048
- return hoveredNode.id === linkSourceId || hoveredNode.id === linkTargetId;
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
- * Check if a node is currently selected
7794
+ * V1-compatible link shortening for source point
7054
7795
  */
7055
- isNodeSelected(nodeId) {
7056
- if (!this.selectionManager) return false;
7057
- const selectionState = this.selectionManager.getSelectionState();
7058
- return selectionState.selectedNode?.id === nodeId;
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
- * Check if a link is currently selected
7817
+ * V1-compatible link shortening for target point
7062
7818
  */
7063
- isLinkSelected(link) {
7064
- if (!this.selectionManager) return false;
7065
- const selectionState = this.selectionManager.getSelectionState();
7066
- if (!selectionState.selectedLink) return false;
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
- * Check if a link's label should be visible due to selection
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
- shouldShowLinkLabelForSelection(link) {
7079
- if (!this.selectionManager) return false;
7080
- const selectionState = this.selectionManager.getSelectionState();
7081
- if (selectionState.selectedLink) {
7082
- const sourceId1 = typeof link.source === "string" ? link.source : link.source.id;
7083
- const targetId1 = typeof link.target === "string" ? link.target : link.target.id;
7084
- const selectedLink = selectionState.selectedLink;
7085
- const sourceId2 = typeof selectedLink.source === "string" ? selectedLink.source : selectedLink.source.id;
7086
- const targetId2 = typeof selectedLink.target === "string" ? selectedLink.target : selectedLink.target.id;
7087
- if (sourceId1 === sourceId2 && targetId1 === targetId2) {
7088
- return true;
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
- if (selectionState.selectedNode) {
7092
- const selectedNode = selectionState.selectedNode;
7093
- const linkSourceId = typeof link.source === "string" ? link.source : link.source.id;
7094
- const linkTargetId = typeof link.target === "string" ? link.target : link.target.id;
7095
- return selectedNode.id === linkSourceId || selectedNode.id === linkTargetId;
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
- * Calculate midpoint of a link for label positioning
8009
+ * Pre-warm pool with initial objects
7101
8010
  */
7102
- getLinkMidpoint(link) {
7103
- if (!this.config) return null;
7104
- const sourceNode = typeof link.source === "string" ? this.nodeMap.get(link.source) : link.source;
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
- x: (sourceNode.x + targetNode.x) / 2,
7111
- y: (sourceNode.y + targetNode.y) / 2
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
- * Render directed link with arrow head
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
- renderDirectedLink(ctx, source, target, style) {
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
- const sourcePoint = this.getShortenedSourcePoint(source, target, style);
7120
- const targetPoint = this.getShortenedTargetPoint(source, target, style);
7121
- ctx.strokeStyle = style.stroke;
7122
- ctx.lineWidth = style.strokeWidth;
7123
- ctx.globalAlpha = style.opacity;
7124
- ctx.beginPath();
7125
- ctx.moveTo(sourcePoint.x, sourcePoint.y);
7126
- ctx.lineTo(targetPoint.x, targetPoint.y);
7127
- ctx.stroke();
7128
- if (style.arrow?.enabled) {
7129
- this.renderArrowAtPoint(ctx, sourcePoint, targetPoint, style.arrow);
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
- * V1-compatible link shortening for source point
8416
+ * Optimized label rendering with minimal object allocation
7138
8417
  */
7139
- getShortenedSourcePoint(source, target, _style) {
7140
- const sourceX = source.x ?? 0;
7141
- const sourceY = source.y ?? 0;
7142
- const targetX = target.x ?? 0;
7143
- const targetY = target.y ?? 0;
7144
- const dx = targetX - sourceX;
7145
- const dy = targetY - sourceY;
7146
- const distance = Math.sqrt(dx * dx + dy * dy) || 1;
7147
- const sourceNodeStyle = this.styleResolver.resolveNodeStyle({
7148
- node: source,
7149
- isHovered: this.isNodeHovered(source.id),
7150
- isSelected: this.isNodeSelected(source.id)
7151
- });
7152
- const visualRadius = sourceNodeStyle.radius + sourceNodeStyle.strokeWidth / 2;
7153
- const offset = visualRadius + 1;
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
- x: sourceX + dx / distance * offset,
7156
- y: sourceY + dy / distance * offset
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
- * V1-compatible link shortening for target point
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
- getShortenedTargetPoint(source, target, style) {
7163
- const sourceX = source.x ?? 0;
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
- * Render arrow head at specific points
8776
+ * Force log performance metrics immediately (for debugging)
7186
8777
  */
7187
- renderArrowAtPoint(ctx, sourcePoint, targetPoint, arrowStyle) {
7188
- try {
7189
- const dx = targetPoint.x - sourcePoint.x;
7190
- const dy = targetPoint.y - sourcePoint.y;
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
- * Render arrow head (legacy method for backward compatibility)
8784
+ * Calculate midpoint of a link for label positioning (delegates to StateManager)
7212
8785
  */
7213
- renderArrow(ctx, source, target, arrowStyle) {
7214
- this.renderArrowAtPoint(
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 (force-graph pattern)
8857
+ * Debug shadow canvas export (delegates to HitDetectionRenderer)
7267
8858
  */
7268
8859
  debugShadowCanvas() {
7269
- try {
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.nodeMap.get(link.source) : link.source;
7498
- const targetNode = typeof link.target === "string" ? this.nodeMap.get(link.target) : link.target;
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.renderDirectedLink(ctx, sourceNode, targetNode, style);
7515
- }
7516
- }
7517
- }
7518
- renderLinkLabelsLayer(ctx, links) {
7519
- if (!this.config || !this.styleResolver) return;
7520
- const nodeCount = this.config.nodes.length;
7521
- const isLargeGraph = nodeCount > 1e4;
7522
- if (isLargeGraph && !this.hasLoggedLargeGraphOptimization) {
7523
- console.log(`\u{1F680} Large graph optimization: ${nodeCount} nodes detected. Link labels will only show on hover/selection for better performance.`);
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: this.isNodeHovered(nodeId),
7606
- isSelected: this.isNodeSelected(nodeId)
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: this.isNodeHovered(nodeId),
7617
- isSelected: this.isNodeSelected(nodeId)
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.nodeMap.clear();
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
- onRender: () => this.renderer.renderWithTransform(),
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
  */