polly-graph 0.2.4 → 0.2.5

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