From 7986b66a88fa8d53d0f76f5c39190fbcddcc60cf Mon Sep 17 00:00:00 2001 From: Knut Sveidqvist Date: Wed, 10 Sep 2025 14:39:08 +0200 Subject: [PATCH] Fix for edge calculation to subgraphs --- cypress/platform/knsv2.html | 91 +++++++++++++++++++++ packages/mermaid-layout-elk/src/geometry.ts | 15 ++++ packages/mermaid-layout-elk/src/render.ts | 72 ++++++++++++++-- 3 files changed, 171 insertions(+), 7 deletions(-) diff --git a/cypress/platform/knsv2.html b/cypress/platform/knsv2.html index f3356f531..59601533e 100644 --- a/cypress/platform/knsv2.html +++ b/cypress/platform/knsv2.html @@ -105,6 +105,97 @@ +
+      ---
+      config:
+        layout: elk
+      ---
+      flowchart TB
+      subgraph container_Beta
+        process_C
+      end
+      subgraph container_Alpha
+        subgraph process_B
+          pppB
+        end
+        subgraph process_A
+          pppA
+        end
+        process_B-->|via_AWSBatch|container_Beta
+        process_A-->|messages|container_Beta
+      end
+
+
+
+      ---
+      config:
+        layout: elk
+      ---
+      flowchart TB
+      subgraph container_Beta
+        process_C
+      end
+
+        process_B-->|via_AWSBatch|container_Beta
+
+
+
+
+      ---
+      config:
+        layout: elk
+      ---
+      classDiagram
+      note "I love this diagram!\nDo you love it?"
+      Class01 <|-- AveryLongClass : Cool
+      <<interface>> Class01
+      Class03 "1" *-- "*" Class04
+      Class05 "1" o-- "many" Class06
+      Class07 "1" .. "*" Class08
+      Class09 "1" --> "*" C2 : Where am i?
+      Class09 "*" --* "*" C3
+      Class09 "1" --|> "1" Class07
+      Class12 <|.. Class08
+      Class11 ..>Class12
+      Class07 : equals()
+      Class07 : Object[] elementData
+      Class01 : size()
+      Class01 : int chimp
+      Class01 : int gorilla
+      Class01 : -int privateChimp
+      Class01 : +int publicGorilla
+      Class01 : #int protectedMarmoset
+      Class08 <--> C2: Cool label
+      class Class10 {
+        <<service>>
+        int id
+        test()
+      }
+      note for Class10 "Cool class\nI said it's very cool class!"
+
+
+      ---
+      config:
+        layout: elk
+      ---
+      requirementDiagram
+        requirement test_req {
+        id: 1
+        text: the test text.
+        risk: high
+        verifymethod: test
+        }
+
+        element test_entity {
+        type: simulation
+        }
+
+        test_entity - satisfies -> test_req
+
       ---
       config:
diff --git a/packages/mermaid-layout-elk/src/geometry.ts b/packages/mermaid-layout-elk/src/geometry.ts
index 3255b2e8b..5cda865de 100644
--- a/packages/mermaid-layout-elk/src/geometry.ts
+++ b/packages/mermaid-layout-elk/src/geometry.ts
@@ -20,6 +20,21 @@ export interface NodeLike {
 export const EPS = 1;
 export const PUSH_OUT = 10;
 
+export const onBorder = (bounds: RectLike, p: P, tol = 0.5): boolean => {
+  const halfW = bounds.width / 2;
+  const halfH = bounds.height / 2;
+  const left = bounds.x - halfW;
+  const right = bounds.x + halfW;
+  const top = bounds.y - halfH;
+  const bottom = bounds.y + halfH;
+
+  const onLeft = Math.abs(p.x - left) <= tol && p.y >= top - tol && p.y <= bottom + tol;
+  const onRight = Math.abs(p.x - right) <= tol && p.y >= top - tol && p.y <= bottom + tol;
+  const onTop = Math.abs(p.y - top) <= tol && p.x >= left - tol && p.x <= right + tol;
+  const onBottom = Math.abs(p.y - bottom) <= tol && p.x >= left - tol && p.x <= right + tol;
+  return onLeft || onRight || onTop || onBottom;
+};
+
 /**
  * Compute intersection between a rectangle (center x/y, width/height) and the line
  * segment from insidePoint -\> outsidePoint. Returns the point on the rectangle border.
diff --git a/packages/mermaid-layout-elk/src/render.ts b/packages/mermaid-layout-elk/src/render.ts
index aeaa865e2..24a740433 100644
--- a/packages/mermaid-layout-elk/src/render.ts
+++ b/packages/mermaid-layout-elk/src/render.ts
@@ -10,6 +10,7 @@ import {
   outsideNode,
   computeNodeIntersection,
   replaceEndpoint,
+  onBorder,
 } from './geometry.js';
 
 type Node = LayoutData['nodes'][number];
@@ -527,8 +528,8 @@ export const render = async (
     const endCenter = points[points.length - 1];
 
     // Minimal, structured logging for diagnostics
-    log.debug('UIO cutter2: bounds', { startBounds, endBounds });
-    log.debug('UIO cutter2: original points', _points);
+    log.debug('PPP cutter2: bounds', { startBounds, endBounds });
+    log.debug('PPP cutter2: original points', _points);
 
     let firstOutsideStartIndex = -1;
 
@@ -867,13 +868,19 @@ export const render = async (
         startNode.y = startNode.offset.posY + startNode.height / 2;
         endNode.x = endNode.offset.posX + endNode.width / 2;
         endNode.y = endNode.offset.posY + endNode.height / 2;
-        if (startNode.shape !== 'rect33') {
+
+        // Only add center points for non-subgraph nodes or when the edge path doesn't already end near the target
+        const shouldAddStartCenter = startNode.shape !== 'rect33';
+        const shouldAddEndCenter = endNode.shape !== 'rect33';
+
+        if (shouldAddStartCenter) {
           edge.points.unshift({
             x: startNode.x,
             y: startNode.y,
           });
         }
-        if (endNode.shape !== 'rect33') {
+
+        if (shouldAddEndCenter) {
           edge.points.push({
             x: endNode.x,
             y: endNode.y,
@@ -882,13 +889,64 @@ export const render = async (
 
         // Debug and sanitize points around cutter2
         const prevPoints = Array.isArray(edge.points) ? [...edge.points] : [];
-        log.debug('UIO cutter2: Points before cutter2:', prevPoints);
-        edge.points = cutter2(startNode, endNode, prevPoints);
+        const endBounds = boundsFor(endNode);
+        log.debug(
+          'PPP cutter2: Points before cutter2:',
+          JSON.stringify(edge.points),
+          'endBounds:',
+          endBounds,
+          onBorder(endBounds, edge.points[edge.points.length - 1])
+        );
+        {
+          const endIsGroup = !!endNode?.isGroup;
+          const lastIdx = prevPoints.length - 1;
+          const lastPt = prevPoints[lastIdx];
+          const endCenterApprox =
+            Math.abs(lastPt.x - endNode.x) < 1e-6 && Math.abs(lastPt.y - endNode.y) < 1e-6;
+          const candidatePt =
+            endCenterApprox && prevPoints.length > 1 ? prevPoints[lastIdx - 1] : lastPt;
+          const lastOnEndBorder = onBorder(endBounds, candidatePt);
+          if (endIsGroup && lastOnEndBorder) {
+            if (endCenterApprox) {
+              // drop the appended end-center so the path truly ends at the border
+              prevPoints.pop();
+            }
+            // still compute start intersection to avoid starting at center
+            const startBounds = boundsFor(startNode);
+            let firstOutsideStartIndex = -1;
+            for (const [i, prevPoint] of prevPoints.entries()) {
+              if (outsideNode(startBounds, prevPoint)) {
+                firstOutsideStartIndex = i;
+                break;
+              }
+            }
+            if (firstOutsideStartIndex !== -1) {
+              const outsidePointForStart = prevPoints[firstOutsideStartIndex];
+              const startCenter = prevPoints[0];
+              const startIntersection = computeNodeIntersection(
+                startNode,
+                startBounds,
+                outsidePointForStart,
+                startCenter
+              );
+              replaceEndpoint(prevPoints, 'start', startIntersection);
+              log.debug('UIO cutter2: start-only intersection applied', { startIntersection });
+            }
+            log.debug(
+              'PPP cutter2: skipping cutter2 because last point on end border and end is group',
+              { endCenterApprox, candidatePt }
+            );
+            edge.points = prevPoints;
+          } else {
+            edge.points = cutter2(startNode, endNode, prevPoints);
+          }
+        }
+        log.debug('PPP cutter2: Points after cutter2:', JSON.stringify(edge.points));
         const hasNaN = (pts: { x: number; y: number }[]) =>
           pts?.some((p) => !Number.isFinite(p?.x) || !Number.isFinite(p?.y));
         if (!Array.isArray(edge.points) || edge.points.length < 2 || hasNaN(edge.points)) {
           log.warn(
-            'UIO cutter2: Invalid points from cutter2, falling back to prevPoints',
+            'POI cutter2: Invalid points from cutter2, falling back to prevPoints',
             edge.points
           );
           // Fallback to previous points and strip any invalid ones just in case