spytial-core 1.5.1 → 1.5.2

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.
@@ -91601,6 +91601,7 @@ VVbdfjptxz|~\x80\x82\x84\xA6\xA8W\b
91601
91601
  this.initializeDOM();
91602
91602
  this.initializeD3();
91603
91603
  this.lineFunction = d32.line().x((d) => d.x).y((d) => d.y).curve(d32.curveBasis);
91604
+ this.gridLineFunction = d32.line().x((d) => d.x).y((d) => d.y).curve(d32.curveLinear);
91604
91605
  this.inputModeEnabled = isInputAllowed;
91605
91606
  this.initializeInputModeHandlers();
91606
91607
  }
@@ -91913,6 +91914,13 @@ VVbdfjptxz|~\x80\x82\x84\xA6\xA8W\b
91913
91914
  <button id="zoom-out" title="Zoom Out" aria-label="Zoom out">\u2212</button>
91914
91915
  <button id="zoom-fit" title="Fit to View" aria-label="Fit graph to view">\u2922</button>
91915
91916
  </div>
91917
+ <div id="routing-control">
91918
+ <label for="routing-mode">Routing:</label>
91919
+ <select id="routing-mode" title="Edge routing mode">
91920
+ <option value="default">Default</option>
91921
+ <option value="grid">Grid</option>
91922
+ </select>
91923
+ </div>
91916
91924
  </div>
91917
91925
  <div id="svg-container">
91918
91926
  <span id="error-icon" title="This graph is depicting an error state">\u26A0\uFE0F</span>
@@ -91986,8 +91994,42 @@ VVbdfjptxz|~\x80\x82\x84\xA6\xA8W\b
91986
91994
  this.resetViewToFitContent();
91987
91995
  });
91988
91996
  }
91997
+ const routingModeSelect = this.shadowRoot.querySelector("#routing-mode");
91998
+ if (routingModeSelect) {
91999
+ const currentFormat = this.layoutFormat || "default";
92000
+ routingModeSelect.value = currentFormat;
92001
+ routingModeSelect.addEventListener("change", () => {
92002
+ this.handleRoutingModeChange(routingModeSelect.value);
92003
+ });
92004
+ }
91989
92005
  this.updateZoomControlStates();
91990
92006
  }
92007
+ /**
92008
+ * Handle routing mode change from dropdown
92009
+ */
92010
+ handleRoutingModeChange(mode) {
92011
+ this.setAttribute("layoutFormat", mode);
92012
+ if (this.currentLayout && this.colaLayout) {
92013
+ if (mode === "grid") {
92014
+ this.gridify(10, 25, 10);
92015
+ } else {
92016
+ this.routeEdges();
92017
+ }
92018
+ this.dispatchEvent(new CustomEvent("routing-mode-changed", {
92019
+ detail: { mode }
92020
+ }));
92021
+ }
92022
+ }
92023
+ /**
92024
+ * Update the routing mode dropdown to match current layoutFormat attribute
92025
+ */
92026
+ updateRoutingModeDropdown() {
92027
+ const routingModeSelect = this.shadowRoot?.querySelector("#routing-mode");
92028
+ if (routingModeSelect) {
92029
+ const currentFormat = this.layoutFormat || "default";
92030
+ routingModeSelect.value = currentFormat;
92031
+ }
92032
+ }
91991
92033
  /**
91992
92034
  * Initialize keyboard event handlers for input mode activation
91993
92035
  */
@@ -92485,6 +92527,7 @@ VVbdfjptxz|~\x80\x82\x84\xA6\xA8W\b
92485
92527
  nodePositions: this.getNodePositions()
92486
92528
  }
92487
92529
  }));
92530
+ this.updateRoutingModeDropdown();
92488
92531
  this.hideLoading();
92489
92532
  });
92490
92533
  try {
@@ -93351,6 +93394,7 @@ VVbdfjptxz|~\x80\x82\x84\xA6\xA8W\b
93351
93394
  }).attr("opacity", this.isInputModeActive ? 0.8 : 0).style("pointer-events", this.isInputModeActive ? "all" : "none").raise();
93352
93395
  }
93353
93396
  gridUpdatePositions() {
93397
+ this.ensureNodeBounds(true);
93354
93398
  const node = this.container.selectAll(".node");
93355
93399
  const mostSpecificTypeLabel = this.container.selectAll(".mostSpecificTypeLabel");
93356
93400
  const label = this.container.selectAll(".label");
@@ -93406,8 +93450,53 @@ VVbdfjptxz|~\x80\x82\x84\xA6\xA8W\b
93406
93450
  }).attr("y", function(d) {
93407
93451
  return d.bounds.y + 12;
93408
93452
  }).attr("text-anchor", "middle").raise();
93409
- const linkGroups = this.container.selectAll(".linkGroup");
93410
- linkGroups.select("text.linklabel").raise();
93453
+ const linkGroups = this.container.selectAll(".link-group");
93454
+ linkGroups.select("path").attr("d", (d) => {
93455
+ const sourceX = d.source?.x ?? d.source?.bounds?.cx() ?? 0;
93456
+ const sourceY = d.source?.y ?? d.source?.bounds?.cy() ?? 0;
93457
+ const targetX = d.target?.x ?? d.target?.bounds?.cx() ?? 0;
93458
+ const targetY = d.target?.y ?? d.target?.bounds?.cy() ?? 0;
93459
+ const dx = targetX - sourceX;
93460
+ const dy = targetY - sourceY;
93461
+ if (Math.abs(dx) > Math.abs(dy)) {
93462
+ const midX = sourceX + dx / 2;
93463
+ return this.gridLineFunction([
93464
+ { x: sourceX, y: sourceY },
93465
+ { x: midX, y: sourceY },
93466
+ { x: midX, y: targetY },
93467
+ { x: targetX, y: targetY }
93468
+ ]);
93469
+ } else {
93470
+ const midY = sourceY + dy / 2;
93471
+ return this.gridLineFunction([
93472
+ { x: sourceX, y: sourceY },
93473
+ { x: sourceX, y: midY },
93474
+ { x: targetX, y: midY },
93475
+ { x: targetX, y: targetY }
93476
+ ]);
93477
+ }
93478
+ });
93479
+ linkGroups.select("text.linklabel").attr("x", (d) => {
93480
+ const pathElement = this.shadowRoot?.querySelector(`path[data-link-id="${d.id}"]`);
93481
+ if (pathElement) {
93482
+ const pathLength = pathElement.getTotalLength();
93483
+ const midpoint = pathElement.getPointAtLength(pathLength / 2);
93484
+ return midpoint.x;
93485
+ }
93486
+ const sourceX = d.source?.x ?? d.source?.bounds?.cx() ?? 0;
93487
+ const targetX = d.target?.x ?? d.target?.bounds?.cx() ?? 0;
93488
+ return (sourceX + targetX) / 2;
93489
+ }).attr("y", (d) => {
93490
+ const pathElement = this.shadowRoot?.querySelector(`path[data-link-id="${d.id}"]`);
93491
+ if (pathElement) {
93492
+ const pathLength = pathElement.getTotalLength();
93493
+ const midpoint = pathElement.getPointAtLength(pathLength / 2);
93494
+ return midpoint.y;
93495
+ }
93496
+ const sourceY = d.source?.y ?? d.source?.bounds?.cy() ?? 0;
93497
+ const targetY = d.target?.y ?? d.target?.bounds?.cy() ?? 0;
93498
+ return (sourceY + targetY) / 2;
93499
+ }).raise();
93411
93500
  }
93412
93501
  /**
93413
93502
  * Advanced edge routing with curvature calculation and overlap handling.
@@ -93438,12 +93527,19 @@ VVbdfjptxz|~\x80\x82\x84\xA6\xA8W\b
93438
93527
  * When using prior positions with minimal iterations, WebCola may not
93439
93528
  * have computed bounds for nodes. This method manually creates Rectangle
93440
93529
  * bounds based on node x, y, width, height properties.
93530
+ *
93531
+ * @param forceRecompute - If true, always recompute bounds even if they exist
93441
93532
  */
93442
- ensureNodeBounds() {
93533
+ ensureNodeBounds(forceRecompute = false) {
93443
93534
  if (!this.currentLayout?.nodes || !cola?.Rectangle) return;
93444
93535
  for (const node of this.currentLayout.nodes) {
93445
- if (node.bounds && typeof node.bounds.rayIntersection === "function") {
93446
- continue;
93536
+ if (!forceRecompute && node.bounds && typeof node.bounds.rayIntersection === "function") {
93537
+ const currentCx = node.bounds.cx();
93538
+ const currentCy = node.bounds.cy();
93539
+ const tolerance = 1;
93540
+ if (Math.abs(currentCx - (node.x || 0)) < tolerance && Math.abs(currentCy - (node.y || 0)) < tolerance) {
93541
+ continue;
93542
+ }
93447
93543
  }
93448
93544
  const halfWidth = (node.width || 50) / 2;
93449
93545
  const halfHeight = (node.height || 30) / 2;
@@ -93489,23 +93585,30 @@ VVbdfjptxz|~\x80\x82\x84\xA6\xA8W\b
93489
93585
  getNodePairKey(sourceId, targetId) {
93490
93586
  return sourceId < targetId ? `${sourceId}:${targetId}` : `${targetId}:${sourceId}`;
93491
93587
  }
93492
- route(nodes, groups, margin, groupMargin) {
93588
+ route(nodes = [], groups = [], margin, groupMargin) {
93493
93589
  nodes.forEach((d) => {
93590
+ const bounds = d.bounds || d.innerBounds || this.createFallbackBounds(d);
93494
93591
  d.routerNode = {
93495
93592
  name: d.name,
93496
- bounds: d.bounds || d.innerBounds
93593
+ bounds
93497
93594
  };
93498
93595
  });
93499
93596
  groups.forEach((d) => {
93597
+ if (!d.bounds) {
93598
+ console.warn("Grid routing group missing bounds; routing may be degraded.", d);
93599
+ }
93500
93600
  d.routerNode = {
93501
- bounds: d.bounds.inflate(-groupMargin),
93601
+ bounds: d.bounds?.inflate(-groupMargin) ?? d.bounds,
93502
93602
  children: (typeof d.groups !== "undefined" ? d.groups.map((c) => nodes.length + c.id) : []).concat(typeof d.leaves !== "undefined" ? d.leaves.map((c) => c.index) : [])
93503
93603
  };
93504
93604
  });
93505
93605
  let gridRouterNodes = nodes.concat(groups).map((d, i) => {
93606
+ if (!d.routerNode) {
93607
+ return null;
93608
+ }
93506
93609
  d.routerNode.id = i;
93507
93610
  return d.routerNode;
93508
- });
93611
+ }).filter(Boolean);
93509
93612
  return new cola.GridRouter(gridRouterNodes, {
93510
93613
  getChildren: (v) => v.children,
93511
93614
  getBounds: (v) => v.bounds
@@ -93513,57 +93616,127 @@ VVbdfjptxz|~\x80\x82\x84\xA6\xA8W\b
93513
93616
  }
93514
93617
  gridify(nudgeGap, margin, groupMargin) {
93515
93618
  try {
93516
- const gridrouter = this.route(this.currentLayout?.nodes, this.currentLayout?.groups, margin, groupMargin);
93517
- let routes = [];
93518
- const edges = this.currentLayout?.links || [];
93519
- if (!edges || edges.length === 0) {
93619
+ const nodes = this.currentLayout?.nodes ?? [];
93620
+ const groups = this.currentLayout?.groups ?? [];
93621
+ const edges = this.currentLayout?.links ?? [];
93622
+ if (nodes.length === 0) {
93623
+ console.warn("No nodes available for GridRouter; skipping gridify.");
93624
+ return;
93625
+ }
93626
+ if (edges.length === 0) {
93520
93627
  console.warn("No edges to route in GridRouter");
93521
93628
  return;
93522
93629
  }
93523
- routes = gridrouter.routeEdges(edges, nudgeGap, function(e) {
93524
- return e.source.routerNode.id;
93525
- }, function(e) {
93526
- return e.target.routerNode.id;
93630
+ console.log("[gridify] Node positions BEFORE ensureNodeBounds:");
93631
+ nodes.slice(0, 3).forEach((n) => {
93632
+ console.log(` ${n.id}: x=${n.x?.toFixed(2)}, y=${n.y?.toFixed(2)}, bounds.cx=${n.bounds?.cx?.()?.toFixed(2)}, bounds.x=${n.bounds?.x?.toFixed(2)}`);
93633
+ });
93634
+ this.ensureNodeBounds(true);
93635
+ console.log("[gridify] Node positions AFTER ensureNodeBounds:");
93636
+ nodes.slice(0, 3).forEach((n) => {
93637
+ console.log(` ${n.id}: x=${n.x?.toFixed(2)}, y=${n.y?.toFixed(2)}, bounds.cx=${n.bounds?.cx?.()?.toFixed(2)}, bounds.x=${n.bounds?.x?.toFixed(2)}`);
93527
93638
  });
93528
- this.container.selectAll(".link-group").remove();
93529
- routes.forEach((route, index) => {
93639
+ const gridrouter = this.route(nodes, groups, margin, groupMargin);
93640
+ let routes = [];
93641
+ const routableEdges = edges.filter((edge) => edge?.source?.routerNode && edge?.target?.routerNode);
93642
+ console.log("[gridify] Total edges:", edges.length, "Routable:", routableEdges.length);
93643
+ if (routableEdges.length !== edges.length) {
93644
+ const unroutableEdges = edges.filter((edge) => !edge?.source?.routerNode || !edge?.target?.routerNode);
93645
+ unroutableEdges.forEach((e) => {
93646
+ console.warn(
93647
+ "[gridify] Unroutable edge:",
93648
+ e.id,
93649
+ "source routerNode:",
93650
+ !!e?.source?.routerNode,
93651
+ "target routerNode:",
93652
+ !!e?.target?.routerNode,
93653
+ "source:",
93654
+ e?.source?.id,
93655
+ "x:",
93656
+ e?.source?.x,
93657
+ "y:",
93658
+ e?.source?.y,
93659
+ "target:",
93660
+ e?.target?.id,
93661
+ "x:",
93662
+ e?.target?.x,
93663
+ "y:",
93664
+ e?.target?.y
93665
+ );
93666
+ });
93667
+ }
93668
+ routes = gridrouter.routeEdges(
93669
+ routableEdges,
93670
+ nudgeGap,
93671
+ function(e) {
93672
+ return e.source.routerNode.id;
93673
+ },
93674
+ function(e) {
93675
+ return e.target.routerNode.id;
93676
+ }
93677
+ );
93678
+ const routesByEdgeId = /* @__PURE__ */ new Map();
93679
+ routableEdges.forEach((edge, index) => {
93680
+ const route = routes[index];
93681
+ if (edge?.id && route) {
93682
+ routesByEdgeId.set(edge.id, this.adjustGridRouteForEdge(edge, route));
93683
+ }
93684
+ });
93685
+ console.log("[gridify] Routes generated:", routesByEdgeId.size, "out of", routableEdges.length);
93686
+ const linkGroups = this.container.selectAll(".link-group").data(edges, (d) => d.id ?? d);
93687
+ linkGroups.select("path").attr("d", (edgeData) => {
93688
+ const route = routesByEdgeId.get(edgeData.id);
93689
+ if (!route) {
93690
+ const sourceX = edgeData.source?.x ?? edgeData.source?.bounds?.cx() ?? 0;
93691
+ const sourceY = edgeData.source?.y ?? edgeData.source?.bounds?.cy() ?? 0;
93692
+ const targetX = edgeData.target?.x ?? edgeData.target?.bounds?.cx() ?? 0;
93693
+ const targetY = edgeData.target?.y ?? edgeData.target?.bounds?.cy() ?? 0;
93694
+ console.log(
93695
+ "[gridify] Fallback path for edge:",
93696
+ edgeData.id,
93697
+ "from",
93698
+ edgeData.source?.id,
93699
+ "(",
93700
+ sourceX,
93701
+ ",",
93702
+ sourceY,
93703
+ ")",
93704
+ "to",
93705
+ edgeData.target?.id,
93706
+ "(",
93707
+ targetX,
93708
+ ",",
93709
+ targetY,
93710
+ ")"
93711
+ );
93712
+ const dx = targetX - sourceX;
93713
+ const dy = targetY - sourceY;
93714
+ if (Math.abs(dx) > Math.abs(dy)) {
93715
+ const midX = sourceX + dx / 2;
93716
+ return this.gridLineFunction([
93717
+ { x: sourceX, y: sourceY },
93718
+ { x: midX, y: sourceY },
93719
+ { x: midX, y: targetY },
93720
+ { x: targetX, y: targetY }
93721
+ ]);
93722
+ } else {
93723
+ const midY = sourceY + dy / 2;
93724
+ return this.gridLineFunction([
93725
+ { x: sourceX, y: sourceY },
93726
+ { x: sourceX, y: midY },
93727
+ { x: targetX, y: midY },
93728
+ { x: targetX, y: targetY }
93729
+ ]);
93730
+ }
93731
+ }
93530
93732
  const cornerradius = 5;
93531
93733
  const arrowwidth = 3;
93532
93734
  const arrowheight = 7;
93533
- const edgeData = edges[index];
93534
93735
  const p = cola.GridRouter.getRoutePath(route, cornerradius, arrowwidth, arrowheight);
93535
- const linkGroup = this.container.append("g").attr("class", "link-group").datum(edgeData);
93536
- linkGroup.append("path").attr("class", () => {
93537
- if (this.isAlignmentEdge(edgeData)) return "alignmentLink";
93538
- if (this.isInferredEdge(edgeData)) return "inferredLink";
93539
- return "link";
93540
- }).attr("data-link-id", edgeData.id).attr("stroke", (d) => d.color).attr("d", p.routepath).lower();
93541
- linkGroup.filter((d) => !this.isAlignmentEdge(d)).append("text").attr("class", "linklabel").text((d) => d.label);
93736
+ const adjustedPath = this.adjustGridRouteForArrowPositioning(edgeData, p.routepath, route);
93737
+ return adjustedPath || p.routepath;
93542
93738
  });
93543
- this.container.selectAll(".node").transition().attr("x", function(d) {
93544
- return d.bounds.x;
93545
- }).attr("y", function(d) {
93546
- return d.bounds.y;
93547
- }).attr("width", function(d) {
93548
- return d.bounds.width();
93549
- }).attr("height", function(d) {
93550
- return d.bounds.height();
93551
- });
93552
- this.container.selectAll(".group").transition().attr("x", function(d) {
93553
- return d.bounds.x;
93554
- }).attr("y", function(d) {
93555
- return d.bounds.y;
93556
- }).attr("width", function(d) {
93557
- return d.bounds.width();
93558
- }).attr("height", function(d) {
93559
- return d.bounds.height();
93560
- });
93561
- this.container.selectAll(".label").transition().attr("x", function(d) {
93562
- return d.bounds.cx();
93563
- }).attr("y", function(d) {
93564
- return d.bounds.cy();
93565
- });
93566
- this.gridUpdateLinkLabels(routes, edges);
93739
+ this.gridUpdateLinkLabels(edges, routesByEdgeId);
93567
93740
  this.fitViewportToContent();
93568
93741
  this.dispatchEvent(new Event("relationsAvailable"));
93569
93742
  } catch (e) {
@@ -93584,23 +93757,224 @@ VVbdfjptxz|~\x80\x82\x84\xA6\xA8W\b
93584
93757
  return;
93585
93758
  }
93586
93759
  }
93587
- gridUpdateLinkLabels(routes, edges) {
93588
- routes.forEach((route, index) => {
93589
- var edgeData = edges[index];
93590
- let combinedSegment = [];
93591
- route.forEach((segment) => {
93592
- combinedSegment = combinedSegment.concat(segment);
93593
- });
93594
- const midpointIndex = Math.floor(combinedSegment.length / 2);
93595
- const midpoint = {
93596
- x: (combinedSegment[midpointIndex - 1].x + combinedSegment[midpointIndex].x) / 2,
93597
- y: (combinedSegment[midpointIndex - 1].y + combinedSegment[midpointIndex].y) / 2
93760
+ gridUpdateLinkLabels(edges, routesByEdgeId) {
93761
+ const linkGroups = this.container.selectAll(".link-group");
93762
+ linkGroups.filter((d) => !this.isAlignmentEdge(d)).select("text.linklabel").attr("x", (edgeData) => {
93763
+ const pathElement = this.shadowRoot?.querySelector(`path[data-link-id="${edgeData.id}"]`);
93764
+ if (pathElement) {
93765
+ try {
93766
+ const pathLength = pathElement.getTotalLength();
93767
+ const midpoint2 = pathElement.getPointAtLength(pathLength / 2);
93768
+ return midpoint2.x;
93769
+ } catch (e) {
93770
+ }
93771
+ }
93772
+ const midpoint = this.getGridRouteMidpoint(edgeData, routesByEdgeId);
93773
+ return midpoint?.x ?? (edgeData.source?.x ?? edgeData.source?.bounds?.cx() ?? 0);
93774
+ }).attr("y", (edgeData) => {
93775
+ const pathElement = this.shadowRoot?.querySelector(`path[data-link-id="${edgeData.id}"]`);
93776
+ if (pathElement) {
93777
+ try {
93778
+ const pathLength = pathElement.getTotalLength();
93779
+ const midpoint2 = pathElement.getPointAtLength(pathLength / 2);
93780
+ return midpoint2.y;
93781
+ } catch (e) {
93782
+ }
93783
+ }
93784
+ const midpoint = this.getGridRouteMidpoint(edgeData, routesByEdgeId);
93785
+ return midpoint?.y ?? (edgeData.source?.y ?? edgeData.source?.bounds?.cy() ?? 0);
93786
+ }).attr("text-anchor", "middle").attr("dominant-baseline", "middle");
93787
+ }
93788
+ getGridRouteMidpoint(edgeData, routesByEdgeId) {
93789
+ const route = routesByEdgeId.get(edgeData.id);
93790
+ if (!route) {
93791
+ const sourceX = edgeData.source?.x ?? edgeData.source?.bounds?.cx() ?? 0;
93792
+ const sourceY = edgeData.source?.y ?? edgeData.source?.bounds?.cy() ?? 0;
93793
+ const targetX = edgeData.target?.x ?? edgeData.target?.bounds?.cx() ?? 0;
93794
+ const targetY = edgeData.target?.y ?? edgeData.target?.bounds?.cy() ?? 0;
93795
+ return {
93796
+ x: (sourceX + targetX) / 2,
93797
+ y: (sourceY + targetY) / 2
93598
93798
  };
93599
- const linkGroups = this.container.selectAll(".link-group");
93600
- linkGroups.filter(function(d) {
93601
- return d.id === edgeData.id;
93602
- }).select("text.linklabel").attr("x", midpoint.x).attr("y", midpoint.y).attr("text-anchor", "middle");
93799
+ }
93800
+ const points = [];
93801
+ route.forEach((segment) => {
93802
+ if (points.length === 0 && segment.length > 0) {
93803
+ points.push(segment[0]);
93804
+ }
93805
+ if (segment.length > 1) {
93806
+ points.push(segment[1]);
93807
+ }
93808
+ });
93809
+ if (points.length < 2) {
93810
+ return null;
93811
+ }
93812
+ let totalLength = 0;
93813
+ const segmentLengths = [];
93814
+ for (let i = 0; i < points.length - 1; i++) {
93815
+ const dx = points[i + 1].x - points[i].x;
93816
+ const dy = points[i + 1].y - points[i].y;
93817
+ const length = Math.sqrt(dx * dx + dy * dy);
93818
+ segmentLengths.push(length);
93819
+ totalLength += length;
93820
+ }
93821
+ const targetLength = totalLength / 2;
93822
+ let accumulatedLength = 0;
93823
+ for (let i = 0; i < segmentLengths.length; i++) {
93824
+ const segmentLength = segmentLengths[i];
93825
+ if (accumulatedLength + segmentLength >= targetLength) {
93826
+ const remainingLength = targetLength - accumulatedLength;
93827
+ const t = segmentLength > 0 ? remainingLength / segmentLength : 0;
93828
+ return {
93829
+ x: points[i].x + t * (points[i + 1].x - points[i].x),
93830
+ y: points[i].y + t * (points[i + 1].y - points[i].y)
93831
+ };
93832
+ }
93833
+ accumulatedLength += segmentLength;
93834
+ }
93835
+ const midIndex = Math.floor(points.length / 2);
93836
+ return points[midIndex];
93837
+ }
93838
+ adjustGridRouteForEdge(edgeData, route) {
93839
+ if (!edgeData?.id?.startsWith("_g_")) {
93840
+ return route;
93841
+ }
93842
+ const points = this.gridRouteToPoints(route);
93843
+ if (points.length < 2) {
93844
+ return route;
93845
+ }
93846
+ const adjustedPoints = this.routeGroupEdge(edgeData, points);
93847
+ return this.pointsToGridRoute(adjustedPoints);
93848
+ }
93849
+ /**
93850
+ * Adjust grid route path for proper arrow positioning at node boundaries.
93851
+ * Ensures the path terminates at the node boundary rather than center.
93852
+ */
93853
+ adjustGridRouteForArrowPositioning(edgeData, routePath, route) {
93854
+ if (!routePath || !edgeData.source || !edgeData.target) {
93855
+ return null;
93856
+ }
93857
+ try {
93858
+ const points = this.gridRouteToPoints(route);
93859
+ if (points.length < 2) {
93860
+ return null;
93861
+ }
93862
+ const source = edgeData.source;
93863
+ const target = edgeData.target;
93864
+ const sourceBounds = source.bounds || {
93865
+ x: source.x - (source.width || 0) / 2,
93866
+ y: source.y - (source.height || 0) / 2,
93867
+ width: () => source.width || 0,
93868
+ height: () => source.height || 0
93869
+ };
93870
+ const targetBounds = target.bounds || {
93871
+ x: target.x - (target.width || 0) / 2,
93872
+ y: target.y - (target.height || 0) / 2,
93873
+ width: () => target.width || 0,
93874
+ height: () => target.height || 0
93875
+ };
93876
+ const firstPoint = points[0];
93877
+ const secondPoint = points.length > 1 ? points[1] : points[0];
93878
+ const sourceIntersection = this.getRectangleIntersection(
93879
+ sourceBounds.x + sourceBounds.width() / 2,
93880
+ sourceBounds.y + sourceBounds.height() / 2,
93881
+ secondPoint.x,
93882
+ secondPoint.y,
93883
+ sourceBounds
93884
+ );
93885
+ if (sourceIntersection) {
93886
+ points[0] = sourceIntersection;
93887
+ }
93888
+ const lastPoint = points[points.length - 1];
93889
+ const secondLastPoint = points.length > 1 ? points[points.length - 2] : lastPoint;
93890
+ const targetIntersection = this.getRectangleIntersection(
93891
+ targetBounds.x + targetBounds.width() / 2,
93892
+ targetBounds.y + targetBounds.height() / 2,
93893
+ secondLastPoint.x,
93894
+ secondLastPoint.y,
93895
+ targetBounds
93896
+ );
93897
+ if (targetIntersection) {
93898
+ points[points.length - 1] = targetIntersection;
93899
+ }
93900
+ return this.gridLineFunction(points);
93901
+ } catch (error) {
93902
+ console.warn("Error adjusting grid route for arrow positioning:", error);
93903
+ return null;
93904
+ }
93905
+ }
93906
+ gridRouteToPoints(route) {
93907
+ const points = [];
93908
+ route.forEach((segment, index) => {
93909
+ if (index === 0) {
93910
+ points.push({ x: segment[0].x, y: segment[0].y });
93911
+ }
93912
+ points.push({ x: segment[1].x, y: segment[1].y });
93603
93913
  });
93914
+ return points;
93915
+ }
93916
+ pointsToGridRoute(points) {
93917
+ const segments = [];
93918
+ for (let i = 0; i < points.length - 1; i += 1) {
93919
+ segments.push([points[i], points[i + 1]]);
93920
+ }
93921
+ return segments;
93922
+ }
93923
+ createFallbackBounds(node) {
93924
+ if (!cola?.Rectangle) {
93925
+ return null;
93926
+ }
93927
+ const halfWidth = (node.width || 50) / 2;
93928
+ const halfHeight = (node.height || 30) / 2;
93929
+ const x = (node.x || 0) - halfWidth;
93930
+ const X = (node.x || 0) + halfWidth;
93931
+ const y = (node.y || 0) - halfHeight;
93932
+ const Y = (node.y || 0) + halfHeight;
93933
+ return new cola.Rectangle(x, X, y, Y);
93934
+ }
93935
+ /**
93936
+ * Calculate the intersection point between a line and a rectangle.
93937
+ * Used for positioning arrow heads at node boundaries in grid mode.
93938
+ *
93939
+ * @param x1 - Start x coordinate (usually center of node)
93940
+ * @param y1 - Start y coordinate (usually center of node)
93941
+ * @param x2 - End x coordinate (next point in path)
93942
+ * @param y2 - End y coordinate (next point in path)
93943
+ * @param bounds - Rectangle bounds with x, y, width(), height()
93944
+ * @returns Intersection point or null if no intersection
93945
+ */
93946
+ getRectangleIntersection(x1, y1, x2, y2, bounds) {
93947
+ const rectLeft = bounds.x;
93948
+ const rectRight = bounds.x + bounds.width();
93949
+ const rectTop = bounds.y;
93950
+ const rectBottom = bounds.y + bounds.height();
93951
+ const dx = x2 - x1;
93952
+ const dy = y2 - y1;
93953
+ if (dx === 0 && dy === 0) {
93954
+ return { x: x1, y: y1 };
93955
+ }
93956
+ let tMin = 0;
93957
+ let tMax = 1;
93958
+ if (dx !== 0) {
93959
+ const t1 = (rectLeft - x1) / dx;
93960
+ const t2 = (rectRight - x1) / dx;
93961
+ tMin = Math.max(tMin, Math.min(t1, t2));
93962
+ tMax = Math.min(tMax, Math.max(t1, t2));
93963
+ }
93964
+ if (dy !== 0) {
93965
+ const t1 = (rectTop - y1) / dy;
93966
+ const t2 = (rectBottom - y1) / dy;
93967
+ tMin = Math.max(tMin, Math.min(t1, t2));
93968
+ tMax = Math.min(tMax, Math.max(t1, t2));
93969
+ }
93970
+ if (tMin > tMax) {
93971
+ return null;
93972
+ }
93973
+ const t = tMin > 0 ? tMin : tMax;
93974
+ return {
93975
+ x: x1 + t * dx,
93976
+ y: y1 + t * dy
93977
+ };
93604
93978
  }
93605
93979
  /**
93606
93980
  * Routes all link paths with advanced curvature and collision handling.
@@ -94702,6 +95076,45 @@ VVbdfjptxz|~\x80\x82\x84\xA6\xA8W\b
94702
95076
  transform: none;
94703
95077
  }
94704
95078
 
95079
+ /* Routing control styling */
95080
+ #routing-control {
95081
+ display: flex;
95082
+ align-items: center;
95083
+ gap: 6px;
95084
+ margin-left: 16px;
95085
+ padding-left: 16px;
95086
+ border-left: 1px solid #e5e7eb;
95087
+ }
95088
+
95089
+ #routing-control label {
95090
+ font-size: 12px;
95091
+ font-weight: 500;
95092
+ color: #6b7280;
95093
+ user-select: none;
95094
+ }
95095
+
95096
+ #routing-mode {
95097
+ padding: 4px 8px;
95098
+ border: 1px solid #d1d5db;
95099
+ background: #f9fafb;
95100
+ color: #374151;
95101
+ border-radius: 4px;
95102
+ font-size: 12px;
95103
+ cursor: pointer;
95104
+ transition: all 0.15s ease;
95105
+ outline: none;
95106
+ }
95107
+
95108
+ #routing-mode:hover {
95109
+ background: #f3f4f6;
95110
+ border-color: #9ca3af;
95111
+ }
95112
+
95113
+ #routing-mode:focus {
95114
+ border-color: #3b82f6;
95115
+ box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1);
95116
+ }
95117
+
94705
95118
  /* Modal Overlay and Dialog */
94706
95119
  .modal-overlay {
94707
95120
  position: fixed;