@diagrammo/dgmo 0.6.1 → 0.6.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.
package/dist/index.js CHANGED
@@ -10476,110 +10476,211 @@ function computeNodeDimensions2(table) {
10476
10476
  const height = headerHeight + columnsHeight + (columnsHeight === 0 ? 4 : 0);
10477
10477
  return { width, height, headerHeight, columnsHeight };
10478
10478
  }
10479
+ function findConnectedComponents(tableIds, relationships) {
10480
+ const adj = /* @__PURE__ */ new Map();
10481
+ for (const id of tableIds) adj.set(id, /* @__PURE__ */ new Set());
10482
+ for (const rel of relationships) {
10483
+ adj.get(rel.source)?.add(rel.target);
10484
+ adj.get(rel.target)?.add(rel.source);
10485
+ }
10486
+ const visited = /* @__PURE__ */ new Set();
10487
+ const components = [];
10488
+ for (const id of tableIds) {
10489
+ if (visited.has(id)) continue;
10490
+ const comp = [];
10491
+ const queue = [id];
10492
+ while (queue.length > 0) {
10493
+ const cur = queue.shift();
10494
+ if (visited.has(cur)) continue;
10495
+ visited.add(cur);
10496
+ comp.push(cur);
10497
+ for (const nb of adj.get(cur) ?? []) {
10498
+ if (!visited.has(nb)) queue.push(nb);
10499
+ }
10500
+ }
10501
+ components.push(comp);
10502
+ }
10503
+ return components;
10504
+ }
10505
+ function layoutComponent(tables, rels, dimMap) {
10506
+ const nodePositions = /* @__PURE__ */ new Map();
10507
+ const edgePoints = /* @__PURE__ */ new Map();
10508
+ if (tables.length === 1) {
10509
+ const dims = dimMap.get(tables[0].id);
10510
+ nodePositions.set(tables[0].id, { x: dims.width / 2, y: dims.height / 2, ...dims });
10511
+ return { nodePositions, edgePoints, width: dims.width, height: dims.height };
10512
+ }
10513
+ const g = new dagre3.graphlib.Graph({ multigraph: true });
10514
+ g.setGraph({ rankdir: "LR", nodesep: 40, ranksep: 80, edgesep: 20 });
10515
+ g.setDefaultEdgeLabel(() => ({}));
10516
+ for (const table of tables) {
10517
+ const dims = dimMap.get(table.id);
10518
+ g.setNode(table.id, { width: dims.width, height: dims.height });
10519
+ }
10520
+ for (const rel of rels) {
10521
+ g.setEdge(rel.source, rel.target, { label: rel.label ?? "" }, String(rel.lineNumber));
10522
+ }
10523
+ dagre3.layout(g);
10524
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
10525
+ for (const table of tables) {
10526
+ const pos = g.node(table.id);
10527
+ const dims = dimMap.get(table.id);
10528
+ minX = Math.min(minX, pos.x - dims.width / 2);
10529
+ minY = Math.min(minY, pos.y - dims.height / 2);
10530
+ maxX = Math.max(maxX, pos.x + dims.width / 2);
10531
+ maxY = Math.max(maxY, pos.y + dims.height / 2);
10532
+ }
10533
+ for (const rel of rels) {
10534
+ const ed = g.edge(rel.source, rel.target, String(rel.lineNumber));
10535
+ for (const pt of ed?.points ?? []) {
10536
+ minX = Math.min(minX, pt.x);
10537
+ minY = Math.min(minY, pt.y);
10538
+ maxX = Math.max(maxX, pt.x);
10539
+ maxY = Math.max(maxY, pt.y);
10540
+ }
10541
+ if (rel.label && (ed?.points ?? []).length > 0) {
10542
+ const pts = ed.points;
10543
+ const mid = pts[Math.floor(pts.length / 2)];
10544
+ const hw = (rel.label.length * 7 + 8) / 2;
10545
+ minX = Math.min(minX, mid.x - hw);
10546
+ maxX = Math.max(maxX, mid.x + hw);
10547
+ }
10548
+ }
10549
+ for (const table of tables) {
10550
+ const pos = g.node(table.id);
10551
+ const dims = dimMap.get(table.id);
10552
+ nodePositions.set(table.id, {
10553
+ x: pos.x - minX,
10554
+ y: pos.y - minY,
10555
+ ...dims
10556
+ });
10557
+ }
10558
+ for (const rel of rels) {
10559
+ const ed = g.edge(rel.source, rel.target, String(rel.lineNumber));
10560
+ edgePoints.set(
10561
+ rel.lineNumber,
10562
+ (ed?.points ?? []).map((pt) => ({ x: pt.x - minX, y: pt.y - minY }))
10563
+ );
10564
+ }
10565
+ return {
10566
+ nodePositions,
10567
+ edgePoints,
10568
+ width: Math.max(0, maxX - minX),
10569
+ height: Math.max(0, maxY - minY)
10570
+ };
10571
+ }
10572
+ function packComponents(items) {
10573
+ if (items.length === 0) return [];
10574
+ const sorted = [...items].sort((a, b) => {
10575
+ const aConnected = a.compIds.length > 1 ? 1 : 0;
10576
+ const bConnected = b.compIds.length > 1 ? 1 : 0;
10577
+ if (aConnected !== bConnected) return bConnected - aConnected;
10578
+ return b.compLayout.height - a.compLayout.height;
10579
+ });
10580
+ const totalArea = items.reduce(
10581
+ (s, c) => s + (c.compLayout.width || MIN_WIDTH2) * (c.compLayout.height || HEADER_BASE2),
10582
+ 0
10583
+ );
10584
+ const targetW = Math.max(
10585
+ Math.sqrt(totalArea) * 1.5,
10586
+ sorted[0].compLayout.width
10587
+ // at least as wide as the widest component
10588
+ );
10589
+ const placements = [];
10590
+ let curX = 0;
10591
+ let curY = 0;
10592
+ let rowH = 0;
10593
+ for (const item of sorted) {
10594
+ const w = item.compLayout.width || MIN_WIDTH2;
10595
+ const h = item.compLayout.height || HEADER_BASE2;
10596
+ if (curX > 0 && curX + w > targetW) {
10597
+ curY += rowH + COMP_GAP;
10598
+ curX = 0;
10599
+ rowH = 0;
10600
+ }
10601
+ placements.push({ compIds: item.compIds, compLayout: item.compLayout, offsetX: curX, offsetY: curY });
10602
+ curX += w + COMP_GAP;
10603
+ rowH = Math.max(rowH, h);
10604
+ }
10605
+ return placements;
10606
+ }
10479
10607
  function layoutERDiagram(parsed) {
10480
10608
  if (parsed.tables.length === 0) {
10481
10609
  return { nodes: [], edges: [], width: 0, height: 0 };
10482
10610
  }
10483
- const g = new dagre3.graphlib.Graph();
10484
- g.setGraph({
10485
- rankdir: "TB",
10486
- nodesep: 60,
10487
- ranksep: 80,
10488
- edgesep: 20
10489
- });
10490
- g.setDefaultEdgeLabel(() => ({}));
10491
10611
  const dimMap = /* @__PURE__ */ new Map();
10492
10612
  for (const table of parsed.tables) {
10493
- const dims = computeNodeDimensions2(table);
10494
- dimMap.set(table.id, dims);
10495
- g.setNode(table.id, {
10496
- label: table.name,
10497
- width: dims.width,
10498
- height: dims.height
10499
- });
10613
+ dimMap.set(table.id, computeNodeDimensions2(table));
10500
10614
  }
10501
- for (const rel of parsed.relationships) {
10502
- g.setEdge(rel.source, rel.target, { label: rel.label ?? "" });
10615
+ const compIdSets = findConnectedComponents(
10616
+ parsed.tables.map((t) => t.id),
10617
+ parsed.relationships
10618
+ );
10619
+ const tableById = new Map(parsed.tables.map((t) => [t.id, t]));
10620
+ const componentItems = compIdSets.map((ids) => {
10621
+ const tables = ids.map((id) => tableById.get(id));
10622
+ const rels = parsed.relationships.filter((r) => ids.includes(r.source));
10623
+ return { compIds: ids, compLayout: layoutComponent(tables, rels, dimMap) };
10624
+ });
10625
+ const packed = packComponents(componentItems);
10626
+ const placementByTableId = /* @__PURE__ */ new Map();
10627
+ for (const p of packed) {
10628
+ for (const id of p.compIds) placementByTableId.set(id, p);
10629
+ }
10630
+ const placementByRelLine = /* @__PURE__ */ new Map();
10631
+ for (const p of packed) {
10632
+ for (const lineNum of p.compLayout.edgePoints.keys()) {
10633
+ placementByRelLine.set(lineNum, p);
10634
+ }
10503
10635
  }
10504
- dagre3.layout(g);
10505
10636
  const layoutNodes = parsed.tables.map((table) => {
10506
- const pos = g.node(table.id);
10507
- const dims = dimMap.get(table.id);
10637
+ const p = placementByTableId.get(table.id);
10638
+ const pos = p.compLayout.nodePositions.get(table.id);
10508
10639
  return {
10509
10640
  ...table,
10510
- x: pos.x,
10511
- y: pos.y,
10512
- width: dims.width,
10513
- height: dims.height,
10514
- headerHeight: dims.headerHeight,
10515
- columnsHeight: dims.columnsHeight
10641
+ x: pos.x + p.offsetX + HALF_MARGIN,
10642
+ y: pos.y + p.offsetY + HALF_MARGIN,
10643
+ width: pos.width,
10644
+ height: pos.height,
10645
+ headerHeight: pos.headerHeight,
10646
+ columnsHeight: pos.columnsHeight
10516
10647
  };
10517
10648
  });
10518
10649
  const layoutEdges = parsed.relationships.map((rel) => {
10519
- const edgeData = g.edge(rel.source, rel.target);
10650
+ const p = placementByRelLine.get(rel.lineNumber);
10651
+ const pts = p?.compLayout.edgePoints.get(rel.lineNumber) ?? [];
10520
10652
  return {
10521
10653
  source: rel.source,
10522
10654
  target: rel.target,
10523
10655
  cardinality: rel.cardinality,
10524
- points: edgeData?.points ?? [],
10656
+ points: pts.map((pt) => ({
10657
+ x: pt.x + (p?.offsetX ?? 0) + HALF_MARGIN,
10658
+ y: pt.y + (p?.offsetY ?? 0) + HALF_MARGIN
10659
+ })),
10525
10660
  label: rel.label,
10526
10661
  lineNumber: rel.lineNumber
10527
10662
  };
10528
10663
  });
10529
- let minX = Infinity;
10530
- let minY = Infinity;
10531
10664
  let maxX = 0;
10532
10665
  let maxY = 0;
10533
10666
  for (const node of layoutNodes) {
10534
- const left = node.x - node.width / 2;
10535
- const right = node.x + node.width / 2;
10536
- const top = node.y - node.height / 2;
10537
- const bottom = node.y + node.height / 2;
10538
- if (left < minX) minX = left;
10539
- if (right > maxX) maxX = right;
10540
- if (top < minY) minY = top;
10541
- if (bottom > maxY) maxY = bottom;
10667
+ maxX = Math.max(maxX, node.x + node.width / 2);
10668
+ maxY = Math.max(maxY, node.y + node.height / 2);
10542
10669
  }
10543
10670
  for (const edge of layoutEdges) {
10544
10671
  for (const pt of edge.points) {
10545
- if (pt.x < minX) minX = pt.x;
10546
- if (pt.x > maxX) maxX = pt.x;
10547
- if (pt.y < minY) minY = pt.y;
10548
- if (pt.y > maxY) maxY = pt.y;
10549
- }
10550
- if (edge.label && edge.points.length > 0) {
10551
- const midPt = edge.points[Math.floor(edge.points.length / 2)];
10552
- const labelHalfW = (edge.label.length * 7 + 8) / 2;
10553
- if (midPt.x + labelHalfW > maxX) maxX = midPt.x + labelHalfW;
10554
- if (midPt.x - labelHalfW < minX) minX = midPt.x - labelHalfW;
10672
+ maxX = Math.max(maxX, pt.x);
10673
+ maxY = Math.max(maxY, pt.y);
10555
10674
  }
10556
10675
  }
10557
- const EDGE_MARGIN2 = 60;
10558
- const HALF_MARGIN = EDGE_MARGIN2 / 2;
10559
- const shiftX = -minX + HALF_MARGIN;
10560
- const shiftY = -minY + HALF_MARGIN;
10561
- for (const node of layoutNodes) {
10562
- node.x += shiftX;
10563
- node.y += shiftY;
10564
- }
10565
- for (const edge of layoutEdges) {
10566
- for (const pt of edge.points) {
10567
- pt.x += shiftX;
10568
- pt.y += shiftY;
10569
- }
10570
- }
10571
- maxX += shiftX;
10572
- maxY += shiftY;
10573
- const totalWidth = maxX + HALF_MARGIN;
10574
- const totalHeight = maxY + HALF_MARGIN;
10575
10676
  return {
10576
10677
  nodes: layoutNodes,
10577
10678
  edges: layoutEdges,
10578
- width: totalWidth,
10579
- height: totalHeight
10679
+ width: maxX + HALF_MARGIN,
10680
+ height: maxY + HALF_MARGIN
10580
10681
  };
10581
10682
  }
10582
- var MIN_WIDTH2, CHAR_WIDTH4, PADDING_X2, HEADER_BASE2, MEMBER_LINE_HEIGHT3, COMPARTMENT_PADDING_Y3, SEPARATOR_HEIGHT2;
10683
+ var MIN_WIDTH2, CHAR_WIDTH4, PADDING_X2, HEADER_BASE2, MEMBER_LINE_HEIGHT3, COMPARTMENT_PADDING_Y3, SEPARATOR_HEIGHT2, HALF_MARGIN, COMP_GAP;
10583
10684
  var init_layout4 = __esm({
10584
10685
  "src/er/layout.ts"() {
10585
10686
  "use strict";
@@ -10590,6 +10691,135 @@ var init_layout4 = __esm({
10590
10691
  MEMBER_LINE_HEIGHT3 = 18;
10591
10692
  COMPARTMENT_PADDING_Y3 = 8;
10592
10693
  SEPARATOR_HEIGHT2 = 1;
10694
+ HALF_MARGIN = 30;
10695
+ COMP_GAP = 60;
10696
+ }
10697
+ });
10698
+
10699
+ // src/er/classify.ts
10700
+ function classifyEREntities(tables, relationships) {
10701
+ const result = /* @__PURE__ */ new Map();
10702
+ if (tables.length === 0) return result;
10703
+ const indegreeMap = {};
10704
+ for (const t of tables) indegreeMap[t.id] = 0;
10705
+ for (const rel of relationships) {
10706
+ if (rel.source === rel.target) continue;
10707
+ if (rel.cardinality.from === "1" && rel.cardinality.to !== "1") {
10708
+ indegreeMap[rel.source] = (indegreeMap[rel.source] ?? 0) + 1;
10709
+ }
10710
+ if (rel.cardinality.to === "1" && rel.cardinality.from !== "1") {
10711
+ indegreeMap[rel.target] = (indegreeMap[rel.target] ?? 0) + 1;
10712
+ }
10713
+ }
10714
+ const tableStarNeighbors = /* @__PURE__ */ new Map();
10715
+ for (const rel of relationships) {
10716
+ if (rel.source === rel.target) continue;
10717
+ if (rel.cardinality.from === "*") {
10718
+ if (!tableStarNeighbors.has(rel.source)) tableStarNeighbors.set(rel.source, /* @__PURE__ */ new Set());
10719
+ tableStarNeighbors.get(rel.source).add(rel.target);
10720
+ }
10721
+ if (rel.cardinality.to === "*") {
10722
+ if (!tableStarNeighbors.has(rel.target)) tableStarNeighbors.set(rel.target, /* @__PURE__ */ new Set());
10723
+ tableStarNeighbors.get(rel.target).add(rel.source);
10724
+ }
10725
+ }
10726
+ const mmParticipants = /* @__PURE__ */ new Set();
10727
+ for (const [id, neighbors] of tableStarNeighbors) {
10728
+ if (neighbors.size >= 2) mmParticipants.add(id);
10729
+ }
10730
+ const indegreeValues = Object.values(indegreeMap);
10731
+ const mean = indegreeValues.reduce((a, b) => a + b, 0) / indegreeValues.length;
10732
+ const variance = indegreeValues.reduce((a, b) => a + (b - mean) ** 2, 0) / indegreeValues.length;
10733
+ const stddev = Math.sqrt(variance);
10734
+ const sorted = [...indegreeValues].sort((a, b) => a - b);
10735
+ const median = sorted.length % 2 === 0 ? (sorted[sorted.length / 2 - 1] + sorted[sorted.length / 2]) / 2 : sorted[Math.floor(sorted.length / 2)];
10736
+ for (const table of tables) {
10737
+ const id = table.id;
10738
+ const cols = table.columns;
10739
+ const fkCols = cols.filter((c) => c.constraints.includes("fk"));
10740
+ const pkFkCols = cols.filter(
10741
+ (c) => c.constraints.includes("pk") && c.constraints.includes("fk")
10742
+ );
10743
+ const fkCount = fkCols.length;
10744
+ const fkRatio = cols.length === 0 ? 0 : fkCount / cols.length;
10745
+ const indegree = indegreeMap[id] ?? 0;
10746
+ const nameLower = table.name.toLowerCase();
10747
+ const externalRels = relationships.filter(
10748
+ (r) => (r.source === id || r.target === id) && r.source !== r.target
10749
+ );
10750
+ const hasSelfRef = relationships.some((r) => r.source === id && r.target === id);
10751
+ const externalTargets = /* @__PURE__ */ new Set();
10752
+ for (const rel of externalRels) {
10753
+ externalTargets.add(rel.source === id ? rel.target : rel.source);
10754
+ }
10755
+ if (hasSelfRef && externalRels.length === 0) {
10756
+ result.set(id, "self-referential");
10757
+ continue;
10758
+ }
10759
+ const isInheritancePattern = pkFkCols.length >= 2 && externalTargets.size === 1;
10760
+ const junctionByRatio = fkRatio >= 0.6 && !isInheritancePattern;
10761
+ const junctionByCompositePk = pkFkCols.length >= 2 && externalTargets.size >= 2;
10762
+ const junctionByMm = mmParticipants.has(id);
10763
+ if (junctionByRatio || junctionByCompositePk || junctionByMm) {
10764
+ result.set(id, "junction");
10765
+ continue;
10766
+ }
10767
+ if (fkRatio >= 0.4 && fkRatio < 0.6 && pkFkCols.length < 2 && !mmParticipants.has(id)) {
10768
+ result.set(id, "ambiguous");
10769
+ continue;
10770
+ }
10771
+ const nameMatchesLookup = LOOKUP_NAME_SUFFIXES.some((s) => nameLower.endsWith(s));
10772
+ if (nameMatchesLookup && cols.length <= 6 && fkCount <= 1 && indegree > median) {
10773
+ result.set(id, "lookup");
10774
+ continue;
10775
+ }
10776
+ if (tables.length >= 6 && indegree > 0 && indegree > mean + 1.5 * stddev && indegree >= 2 * mean) {
10777
+ result.set(id, "hub");
10778
+ continue;
10779
+ }
10780
+ if (fkCount > 0) {
10781
+ result.set(id, "dependent");
10782
+ continue;
10783
+ }
10784
+ result.set(id, "core");
10785
+ }
10786
+ return result;
10787
+ }
10788
+ var ROLE_COLORS, ROLE_LABELS, ROLE_ORDER, LOOKUP_NAME_SUFFIXES;
10789
+ var init_classify = __esm({
10790
+ "src/er/classify.ts"() {
10791
+ "use strict";
10792
+ ROLE_COLORS = {
10793
+ core: "green",
10794
+ dependent: "blue",
10795
+ junction: "red",
10796
+ ambiguous: "purple",
10797
+ lookup: "yellow",
10798
+ hub: "orange",
10799
+ "self-referential": "teal",
10800
+ unclassified: "gray"
10801
+ };
10802
+ ROLE_LABELS = {
10803
+ core: "Core entity",
10804
+ dependent: "Dependent",
10805
+ junction: "Junction / M:M",
10806
+ ambiguous: "Bridge",
10807
+ lookup: "Lookup / Reference",
10808
+ hub: "Hub",
10809
+ "self-referential": "Self-referential",
10810
+ unclassified: "Unclassified"
10811
+ };
10812
+ ROLE_ORDER = [
10813
+ "core",
10814
+ "dependent",
10815
+ "junction",
10816
+ "ambiguous",
10817
+ "lookup",
10818
+ "hub",
10819
+ "self-referential",
10820
+ "unclassified"
10821
+ ];
10822
+ LOOKUP_NAME_SUFFIXES = ["_type", "_status", "_code", "_category"];
10593
10823
  }
10594
10824
  });
10595
10825
 
@@ -10661,25 +10891,41 @@ function drawCardinality(g, point, prevPoint, cardinality, color, useLabels) {
10661
10891
  g.append("line").attr("x1", bx + px * spread).attr("y1", by + py * spread).attr("x2", bx - px * spread).attr("y2", by - py * spread).attr("stroke", color).attr("stroke-width", sw);
10662
10892
  }
10663
10893
  }
10664
- function renderERDiagram(container, parsed, layout, palette, isDark, onClickItem, exportDims, activeTagGroup) {
10894
+ function renderERDiagram(container, parsed, layout, palette, isDark, onClickItem, exportDims, activeTagGroup, semanticColorsActive) {
10665
10895
  d3Selection5.select(container).selectAll(":not([data-d3-tooltip])").remove();
10666
- const width = exportDims?.width ?? container.clientWidth;
10667
- const height = exportDims?.height ?? container.clientHeight;
10668
- if (width <= 0 || height <= 0) return;
10896
+ const useSemanticColors = parsed.tagGroups.length === 0 && layout.nodes.every((n) => !n.color);
10897
+ const legendReserveH = useSemanticColors ? LEGEND_HEIGHT + DIAGRAM_PADDING5 : 0;
10669
10898
  const titleHeight = parsed.title ? 40 : 0;
10670
10899
  const diagramW = layout.width;
10671
10900
  const diagramH = layout.height;
10672
- const availH = height - titleHeight;
10673
- const scaleX = (width - DIAGRAM_PADDING5 * 2) / diagramW;
10674
- const scaleY = (availH - DIAGRAM_PADDING5 * 2) / diagramH;
10675
- const scale = Math.min(MAX_SCALE4, scaleX, scaleY);
10676
- const scaledW = diagramW * scale;
10677
- const scaledH = diagramH * scale;
10678
- const offsetX = (width - scaledW) / 2;
10679
- const offsetY = titleHeight + DIAGRAM_PADDING5;
10680
- const svg = d3Selection5.select(container).append("svg").attr("width", width).attr("height", height).style("font-family", FONT_FAMILY);
10901
+ const naturalW = diagramW + DIAGRAM_PADDING5 * 2;
10902
+ const naturalH = diagramH + titleHeight + legendReserveH + DIAGRAM_PADDING5 * 2;
10903
+ let viewW;
10904
+ let viewH;
10905
+ let scale;
10906
+ let offsetX;
10907
+ let offsetY;
10908
+ if (exportDims) {
10909
+ viewW = exportDims.width ?? naturalW;
10910
+ viewH = exportDims.height ?? naturalH;
10911
+ const availH = viewH - titleHeight - legendReserveH;
10912
+ const scaleX = (viewW - DIAGRAM_PADDING5 * 2) / diagramW;
10913
+ const scaleY = (availH - DIAGRAM_PADDING5 * 2) / diagramH;
10914
+ scale = Math.min(MAX_SCALE4, scaleX, scaleY);
10915
+ const scaledW = diagramW * scale;
10916
+ offsetX = (viewW - scaledW) / 2;
10917
+ offsetY = titleHeight + DIAGRAM_PADDING5;
10918
+ } else {
10919
+ viewW = naturalW;
10920
+ viewH = naturalH;
10921
+ scale = 1;
10922
+ offsetX = DIAGRAM_PADDING5;
10923
+ offsetY = titleHeight + DIAGRAM_PADDING5;
10924
+ }
10925
+ if (viewW <= 0 || viewH <= 0) return;
10926
+ const svg = d3Selection5.select(container).append("svg").attr("width", exportDims ? viewW : "100%").attr("height", exportDims ? viewH : "100%").attr("viewBox", `0 0 ${viewW} ${viewH}`).attr("preserveAspectRatio", "xMidYMid meet").style("font-family", FONT_FAMILY);
10681
10927
  if (parsed.title) {
10682
- const titleEl = svg.append("text").attr("class", "chart-title").attr("x", width / 2).attr("y", 30).attr("text-anchor", "middle").attr("fill", palette.text).attr("font-size", "20px").attr("font-weight", "700").style("cursor", onClickItem && parsed.titleLineNumber ? "pointer" : "default").text(parsed.title);
10928
+ const titleEl = svg.append("text").attr("class", "chart-title").attr("x", viewW / 2).attr("y", 30).attr("text-anchor", "middle").attr("fill", palette.text).attr("font-size", "20px").attr("font-weight", "700").style("cursor", onClickItem && parsed.titleLineNumber ? "pointer" : "default").text(parsed.title);
10683
10929
  if (parsed.titleLineNumber) {
10684
10930
  titleEl.attr("data-line-number", parsed.titleLineNumber);
10685
10931
  if (onClickItem) {
@@ -10693,6 +10939,8 @@ function renderERDiagram(container, parsed, layout, palette, isDark, onClickItem
10693
10939
  }
10694
10940
  const contentG = svg.append("g").attr("transform", `translate(${offsetX}, ${offsetY}) scale(${scale})`);
10695
10941
  const seriesColors2 = getSeriesColors(palette);
10942
+ const semanticRoles = useSemanticColors ? classifyEREntities(parsed.tables, parsed.relationships) : null;
10943
+ const semanticActive = semanticRoles !== null && (semanticColorsActive ?? true);
10696
10944
  const useLabels = parsed.options.notation === "labels";
10697
10945
  for (const edge of layout.edges) {
10698
10946
  if (edge.points.length < 2) continue;
@@ -10732,7 +10980,8 @@ function renderERDiagram(container, parsed, layout, palette, isDark, onClickItem
10732
10980
  for (let ni = 0; ni < layout.nodes.length; ni++) {
10733
10981
  const node = layout.nodes[ni];
10734
10982
  const tagColor = resolveTagColor(node.metadata, parsed.tagGroups, activeTagGroup ?? null);
10735
- const nodeColor2 = node.color ?? tagColor ?? seriesColors2[ni % seriesColors2.length];
10983
+ const semanticColor = semanticActive ? palette.colors[ROLE_COLORS[semanticRoles.get(node.id) ?? "unclassified"]] : semanticRoles ? palette.primary : void 0;
10984
+ const nodeColor2 = node.color ?? tagColor ?? semanticColor ?? seriesColors2[ni % seriesColors2.length];
10736
10985
  const nodeG = contentG.append("g").attr("transform", `translate(${node.x}, ${node.y})`).attr("class", "er-table").attr("data-line-number", String(node.lineNumber)).attr("data-node-id", node.id);
10737
10986
  if (activeTagGroup) {
10738
10987
  const tagKey = activeTagGroup.toLowerCase();
@@ -10741,6 +10990,10 @@ function renderERDiagram(container, parsed, layout, palette, isDark, onClickItem
10741
10990
  nodeG.attr(`data-tag-${tagKey}`, tagValue.toLowerCase());
10742
10991
  }
10743
10992
  }
10993
+ if (semanticRoles) {
10994
+ const role = semanticRoles.get(node.id);
10995
+ if (role) nodeG.attr("data-er-role", role);
10996
+ }
10744
10997
  if (onClickItem) {
10745
10998
  nodeG.style("cursor", "pointer").on("click", () => {
10746
10999
  onClickItem(node.lineNumber);
@@ -10781,7 +11034,7 @@ function renderERDiagram(container, parsed, layout, palette, isDark, onClickItem
10781
11034
  legendG.attr("data-legend-active", activeTagGroup.toLowerCase());
10782
11035
  }
10783
11036
  let legendX = DIAGRAM_PADDING5;
10784
- let legendY = height - DIAGRAM_PADDING5;
11037
+ let legendY = viewH - DIAGRAM_PADDING5;
10785
11038
  for (const group of parsed.tagGroups) {
10786
11039
  const groupG = legendG.append("g").attr("data-legend-group", group.name.toLowerCase());
10787
11040
  const labelText = groupG.append("text").attr("x", legendX).attr("y", legendY + LEGEND_PILL_H / 2).attr("dominant-baseline", "central").attr("fill", palette.textMuted).attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-family", FONT_FAMILY).text(`${group.name}:`);
@@ -10800,6 +11053,62 @@ function renderERDiagram(container, parsed, layout, palette, isDark, onClickItem
10800
11053
  legendX += LEGEND_GROUP_GAP;
10801
11054
  }
10802
11055
  }
11056
+ if (semanticRoles) {
11057
+ const presentRoles = ROLE_ORDER.filter((role) => {
11058
+ for (const r of semanticRoles.values()) {
11059
+ if (r === role) return true;
11060
+ }
11061
+ return false;
11062
+ });
11063
+ if (presentRoles.length > 0) {
11064
+ const measureLabelW = (text, fontSize) => {
11065
+ const dummy = svg.append("text").attr("font-size", fontSize).attr("font-family", FONT_FAMILY).attr("visibility", "hidden").text(text);
11066
+ const measured = dummy.node()?.getComputedTextLength?.() ?? 0;
11067
+ dummy.remove();
11068
+ return measured > 0 ? measured : text.length * fontSize * 0.6;
11069
+ };
11070
+ const labelWidths = /* @__PURE__ */ new Map();
11071
+ for (const role of presentRoles) {
11072
+ labelWidths.set(role, measureLabelW(ROLE_LABELS[role], LEGEND_ENTRY_FONT_SIZE));
11073
+ }
11074
+ const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
11075
+ const groupName = "Role";
11076
+ const pillWidth = groupName.length * LEGEND_PILL_FONT_W + LEGEND_PILL_PAD;
11077
+ const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
11078
+ let totalWidth;
11079
+ let entriesWidth = 0;
11080
+ if (semanticActive) {
11081
+ for (const role of presentRoles) {
11082
+ entriesWidth += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + labelWidths.get(role) + LEGEND_ENTRY_TRAIL;
11083
+ }
11084
+ totalWidth = LEGEND_CAPSULE_PAD * 2 + pillWidth + LEGEND_ENTRY_TRAIL + entriesWidth;
11085
+ } else {
11086
+ totalWidth = pillWidth;
11087
+ }
11088
+ const legendX = (viewW - totalWidth) / 2;
11089
+ const legendY = viewH - DIAGRAM_PADDING5 - LEGEND_HEIGHT;
11090
+ const semanticLegendG = svg.append("g").attr("class", "er-semantic-legend").attr("data-legend-group", "role").attr("transform", `translate(${legendX}, ${legendY})`).style("cursor", "pointer");
11091
+ if (semanticActive) {
11092
+ semanticLegendG.append("rect").attr("width", totalWidth).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
11093
+ semanticLegendG.append("rect").attr("x", LEGEND_CAPSULE_PAD).attr("y", LEGEND_CAPSULE_PAD).attr("width", pillWidth).attr("height", pillH).attr("rx", pillH / 2).attr("fill", palette.bg);
11094
+ semanticLegendG.append("rect").attr("x", LEGEND_CAPSULE_PAD).attr("y", LEGEND_CAPSULE_PAD).attr("width", pillWidth).attr("height", pillH).attr("rx", pillH / 2).attr("fill", "none").attr("stroke", mix(palette.textMuted, palette.bg, 50)).attr("stroke-width", 0.75);
11095
+ semanticLegendG.append("text").attr("x", LEGEND_CAPSULE_PAD + pillWidth / 2).attr("y", LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2).attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-weight", "500").attr("fill", palette.text).attr("text-anchor", "middle").attr("font-family", FONT_FAMILY).text(groupName);
11096
+ let entryX = LEGEND_CAPSULE_PAD + pillWidth + LEGEND_ENTRY_TRAIL;
11097
+ for (const role of presentRoles) {
11098
+ const label = ROLE_LABELS[role];
11099
+ const roleColor = palette.colors[ROLE_COLORS[role]];
11100
+ const entryG = semanticLegendG.append("g").attr("data-legend-entry", role);
11101
+ entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R).attr("cy", LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", roleColor);
11102
+ const textX = entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
11103
+ entryG.append("text").attr("x", textX).attr("y", LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1).attr("font-size", LEGEND_ENTRY_FONT_SIZE).attr("fill", palette.textMuted).attr("font-family", FONT_FAMILY).text(label);
11104
+ entryX = textX + labelWidths.get(role) + LEGEND_ENTRY_TRAIL;
11105
+ }
11106
+ } else {
11107
+ semanticLegendG.append("rect").attr("width", pillWidth).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
11108
+ semanticLegendG.append("text").attr("x", pillWidth / 2).attr("y", LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2).attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-weight", "500").attr("fill", palette.textMuted).attr("text-anchor", "middle").attr("font-family", FONT_FAMILY).text(groupName);
11109
+ }
11110
+ }
11111
+ }
10803
11112
  }
10804
11113
  function renderERDiagramForExport(content, theme, palette) {
10805
11114
  const parsed = parseERDiagram(content, palette);
@@ -10847,6 +11156,7 @@ var init_renderer5 = __esm({
10847
11156
  init_legend_constants();
10848
11157
  init_parser3();
10849
11158
  init_layout4();
11159
+ init_classify();
10850
11160
  DIAGRAM_PADDING5 = 20;
10851
11161
  MAX_SCALE4 = 3;
10852
11162
  TABLE_FONT_SIZE = 13;
@@ -10975,9 +11285,10 @@ function layoutInitiativeStatus(parsed, collapseResult) {
10975
11285
  const dagreEdge = g.edge(edge.source, edge.target, `e${i}`);
10976
11286
  const dagrePoints = dagreEdge?.points ?? [];
10977
11287
  const hasIntermediateRank = allNodeX.some((x) => x > src.x + 20 && x < tgt.x - 20);
10978
- const step = Math.min((enterX - exitX) * 0.15, 20);
11288
+ const step = Math.max(0, Math.min((enterX - exitX) * 0.15, 20));
10979
11289
  const isBackEdge = tgt.x < src.x - 5;
10980
- const isYDisplaced = !isBackEdge && Math.abs(tgt.y - src.y) > NODESEP;
11290
+ const isTopExit = !isBackEdge && tgt.x > src.x && !hasIntermediateRank && tgt.y < src.y - NODESEP;
11291
+ const isBottomExit = !isBackEdge && tgt.x > src.x && !hasIntermediateRank && tgt.y > src.y + NODESEP;
10981
11292
  let points;
10982
11293
  if (isBackEdge) {
10983
11294
  const routeAbove = Math.min(src.y, tgt.y) > avgNodeY;
@@ -10987,31 +11298,44 @@ function layoutInitiativeStatus(parsed, collapseResult) {
10987
11298
  const spreadDir = avgNodeX < rawMidX ? 1 : -1;
10988
11299
  const unclamped = Math.abs(src.x - tgt.x) < NODE_WIDTH ? rawMidX + spreadDir * BACK_EDGE_MIN_SPREAD : rawMidX;
10989
11300
  const midX = Math.min(src.x, Math.max(tgt.x, unclamped));
11301
+ const srcDepart = Math.max(midX + 1, src.x - TOP_EXIT_STEP);
11302
+ const tgtApproach = Math.min(midX - 1, tgt.x + TOP_EXIT_STEP);
10990
11303
  if (routeAbove) {
10991
11304
  const arcY = Math.min(src.y - srcHalfH, tgt.y - tgtHalfH) - BACK_EDGE_MARGIN;
10992
11305
  points = [
10993
11306
  { x: src.x, y: src.y - srcHalfH },
11307
+ { x: srcDepart, y: src.y - srcHalfH - TOP_EXIT_STEP },
10994
11308
  { x: midX, y: arcY },
11309
+ { x: tgtApproach, y: tgt.y - tgtHalfH - TOP_EXIT_STEP },
10995
11310
  { x: tgt.x, y: tgt.y - tgtHalfH }
10996
11311
  ];
10997
11312
  } else {
10998
11313
  const arcY = Math.max(src.y + srcHalfH, tgt.y + tgtHalfH) + BACK_EDGE_MARGIN;
10999
11314
  points = [
11000
11315
  { x: src.x, y: src.y + srcHalfH },
11316
+ { x: srcDepart, y: src.y + srcHalfH + TOP_EXIT_STEP },
11001
11317
  { x: midX, y: arcY },
11318
+ { x: tgtApproach, y: tgt.y + tgtHalfH + TOP_EXIT_STEP },
11002
11319
  { x: tgt.x, y: tgt.y + tgtHalfH }
11003
11320
  ];
11004
11321
  }
11005
- } else if (isYDisplaced) {
11006
- const exitY = tgt.y > src.y + NODESEP ? src.y + src.height / 2 : src.y - src.height / 2;
11007
- const spreadExitX = src.x + yOffset;
11008
- const spreadEntryY = tgt.y + yOffset;
11009
- const midX = (spreadExitX + enterX) / 2;
11010
- const midY = (exitY + spreadEntryY) / 2;
11322
+ } else if (isTopExit) {
11323
+ const exitY = src.y - src.height / 2;
11324
+ const p1x = Math.min(Math.max(src.x, src.x + yOffset + TOP_EXIT_STEP), (src.x + enterX) / 2 - 1);
11011
11325
  points = [
11012
- { x: spreadExitX, y: exitY },
11013
- { x: midX, y: midY },
11014
- { x: enterX, y: spreadEntryY }
11326
+ { x: src.x, y: exitY },
11327
+ { x: p1x, y: exitY - TOP_EXIT_STEP },
11328
+ { x: enterX - step, y: tgt.y + yOffset },
11329
+ { x: enterX, y: tgt.y }
11330
+ ];
11331
+ } else if (isBottomExit) {
11332
+ const exitY = src.y + src.height / 2;
11333
+ const p1x = Math.min(Math.max(src.x, src.x + yOffset + TOP_EXIT_STEP), (src.x + enterX) / 2 - 1);
11334
+ points = [
11335
+ { x: src.x, y: exitY },
11336
+ { x: p1x, y: exitY + TOP_EXIT_STEP },
11337
+ { x: enterX - step, y: tgt.y + yOffset },
11338
+ { x: enterX, y: tgt.y }
11015
11339
  ];
11016
11340
  } else if (tgt.x > src.x && !hasIntermediateRank) {
11017
11341
  points = [
@@ -11108,7 +11432,7 @@ function layoutInitiativeStatus(parsed, collapseResult) {
11108
11432
  totalHeight += 40;
11109
11433
  return { nodes: layoutNodes, edges: layoutEdges, groups: layoutGroups, width: totalWidth, height: totalHeight };
11110
11434
  }
11111
- var STATUS_PRIORITY, PHI, NODE_HEIGHT, NODE_WIDTH, GROUP_PADDING, NODESEP, RANKSEP, PARALLEL_SPACING, PARALLEL_EDGE_MARGIN, MAX_PARALLEL_EDGES, BACK_EDGE_MARGIN, BACK_EDGE_MIN_SPREAD, CHAR_WIDTH_RATIO, NODE_FONT_SIZE, NODE_TEXT_PADDING;
11435
+ var STATUS_PRIORITY, PHI, NODE_HEIGHT, NODE_WIDTH, GROUP_PADDING, NODESEP, RANKSEP, PARALLEL_SPACING, PARALLEL_EDGE_MARGIN, MAX_PARALLEL_EDGES, BACK_EDGE_MARGIN, BACK_EDGE_MIN_SPREAD, TOP_EXIT_STEP, CHAR_WIDTH_RATIO, NODE_FONT_SIZE, NODE_TEXT_PADDING;
11112
11436
  var init_layout5 = __esm({
11113
11437
  "src/initiative-status/layout.ts"() {
11114
11438
  "use strict";
@@ -11124,6 +11448,7 @@ var init_layout5 = __esm({
11124
11448
  MAX_PARALLEL_EDGES = 5;
11125
11449
  BACK_EDGE_MARGIN = 40;
11126
11450
  BACK_EDGE_MIN_SPREAD = Math.round(NODE_WIDTH * 0.75);
11451
+ TOP_EXIT_STEP = 10;
11127
11452
  CHAR_WIDTH_RATIO = 0.6;
11128
11453
  NODE_FONT_SIZE = 13;
11129
11454
  NODE_TEXT_PADDING = 12;
@@ -11623,7 +11948,7 @@ __export(layout_exports6, {
11623
11948
  rollUpContextRelationships: () => rollUpContextRelationships
11624
11949
  });
11625
11950
  import dagre5 from "@dagrejs/dagre";
11626
- function computeEdgePenalty(edgeList, nodePositions, degrees) {
11951
+ function computeEdgePenalty(edgeList, nodePositions, degrees, nodeGeometry) {
11627
11952
  let penalty = 0;
11628
11953
  for (const edge of edgeList) {
11629
11954
  const sx = nodePositions.get(edge.source);
@@ -11633,6 +11958,32 @@ function computeEdgePenalty(edgeList, nodePositions, degrees) {
11633
11958
  const weight = Math.min(degrees.get(edge.source) ?? 1, degrees.get(edge.target) ?? 1);
11634
11959
  penalty += dist * weight;
11635
11960
  }
11961
+ if (nodeGeometry) {
11962
+ for (const edge of edgeList) {
11963
+ const geomA = nodeGeometry.get(edge.source);
11964
+ const geomB = nodeGeometry.get(edge.target);
11965
+ if (!geomA || !geomB) continue;
11966
+ const ax = nodePositions.get(edge.source) ?? 0;
11967
+ const bx = nodePositions.get(edge.target) ?? 0;
11968
+ const ay = geomA.y;
11969
+ const by = geomB.y;
11970
+ if (ay === by) continue;
11971
+ const edgeMinX = Math.min(ax, bx);
11972
+ const edgeMaxX = Math.max(ax, bx);
11973
+ const edgeMinY = Math.min(ay, by);
11974
+ const edgeMaxY = Math.max(ay, by);
11975
+ for (const [name, geomC] of nodeGeometry) {
11976
+ if (name === edge.source || name === edge.target) continue;
11977
+ const cx = nodePositions.get(name) ?? 0;
11978
+ const cy = geomC.y;
11979
+ const hw = geomC.width / 2;
11980
+ const hh = geomC.height / 2;
11981
+ if (cx + hw > edgeMinX && cx - hw < edgeMaxX && cy + hh > edgeMinY && cy - hh < edgeMaxY) {
11982
+ penalty += EDGE_NODE_COLLISION_WEIGHT;
11983
+ }
11984
+ }
11985
+ }
11986
+ }
11636
11987
  return penalty;
11637
11988
  }
11638
11989
  function reduceCrossings(g, edgeList, nodeGroupMap) {
@@ -11642,6 +11993,11 @@ function reduceCrossings(g, edgeList, nodeGroupMap) {
11642
11993
  degrees.set(edge.source, (degrees.get(edge.source) ?? 0) + 1);
11643
11994
  degrees.set(edge.target, (degrees.get(edge.target) ?? 0) + 1);
11644
11995
  }
11996
+ const nodeGeometry = /* @__PURE__ */ new Map();
11997
+ for (const name of g.nodes()) {
11998
+ const pos = g.node(name);
11999
+ if (pos) nodeGeometry.set(name, { y: pos.y, width: pos.width, height: pos.height });
12000
+ }
11645
12001
  const rankMap = /* @__PURE__ */ new Map();
11646
12002
  for (const name of g.nodes()) {
11647
12003
  const pos = g.node(name);
@@ -11684,7 +12040,7 @@ function reduceCrossings(g, edgeList, nodeGroupMap) {
11684
12040
  const pos = g.node(name);
11685
12041
  if (pos) basePositions.set(name, pos.x);
11686
12042
  }
11687
- const currentPenalty = computeEdgePenalty(edgeList, basePositions, degrees);
12043
+ const currentPenalty = computeEdgePenalty(edgeList, basePositions, degrees, nodeGeometry);
11688
12044
  let bestPerm = [...partition];
11689
12045
  let bestPenalty = currentPenalty;
11690
12046
  if (partition.length <= 8) {
@@ -11694,7 +12050,7 @@ function reduceCrossings(g, edgeList, nodeGroupMap) {
11694
12050
  for (let i = 0; i < perm.length; i++) {
11695
12051
  testPositions.set(perm[i], xSlots[i]);
11696
12052
  }
11697
- const penalty = computeEdgePenalty(edgeList, testPositions, degrees);
12053
+ const penalty = computeEdgePenalty(edgeList, testPositions, degrees, nodeGeometry);
11698
12054
  if (penalty < bestPenalty) {
11699
12055
  bestPenalty = penalty;
11700
12056
  bestPerm = [...perm];
@@ -11712,13 +12068,13 @@ function reduceCrossings(g, edgeList, nodeGroupMap) {
11712
12068
  for (let k = 0; k < workingOrder.length; k++) {
11713
12069
  testPositions.set(workingOrder[k], xSlots[k]);
11714
12070
  }
11715
- const before = computeEdgePenalty(edgeList, testPositions, degrees);
12071
+ const before = computeEdgePenalty(edgeList, testPositions, degrees, nodeGeometry);
11716
12072
  [workingOrder[i], workingOrder[i + 1]] = [workingOrder[i + 1], workingOrder[i]];
11717
12073
  const testPositions2 = new Map(basePositions);
11718
12074
  for (let k = 0; k < workingOrder.length; k++) {
11719
12075
  testPositions2.set(workingOrder[k], xSlots[k]);
11720
12076
  }
11721
- const after = computeEdgePenalty(edgeList, testPositions2, degrees);
12077
+ const after = computeEdgePenalty(edgeList, testPositions2, degrees, nodeGeometry);
11722
12078
  if (after < before) {
11723
12079
  improved = true;
11724
12080
  if (after < bestPenalty) {
@@ -11957,7 +12313,7 @@ function computeLegendGroups3(tagGroups) {
11957
12313
  const nameW = group.name.length * LEGEND_PILL_FONT_W4 + LEGEND_PILL_PAD4 * 2;
11958
12314
  let capsuleW = LEGEND_CAPSULE_PAD4;
11959
12315
  for (const e of entries) {
11960
- capsuleW += LEGEND_DOT_R4 * 2 + LEGEND_ENTRY_DOT_GAP4 + e.value.length * LEGEND_ENTRY_FONT_W4 + LEGEND_ENTRY_TRAIL4;
12316
+ capsuleW += LEGEND_DOT_R4 * 2 + LEGEND_ENTRY_DOT_GAP4 + e.value.length * LEGEND_ENTRY_FONT_W5 + LEGEND_ENTRY_TRAIL4;
11961
12317
  }
11962
12318
  capsuleW += LEGEND_CAPSULE_PAD4;
11963
12319
  result.push({
@@ -13038,7 +13394,7 @@ function layoutC4Deployment(parsed, activeTagGroup) {
13038
13394
  }
13039
13395
  return { nodes, edges, legend: legendGroups, groupBoundaries, width: totalWidth, height: totalHeight };
13040
13396
  }
13041
- var CHAR_WIDTH5, MIN_NODE_WIDTH, MAX_NODE_WIDTH, TYPE_LABEL_HEIGHT, DIVIDER_GAP, NAME_HEIGHT, DESC_LINE_HEIGHT, DESC_CHAR_WIDTH, CARD_V_PAD3, CARD_H_PAD3, META_LINE_HEIGHT5, META_CHAR_WIDTH, MARGIN3, BOUNDARY_PAD, GROUP_BOUNDARY_PAD, LEGEND_HEIGHT4, LEGEND_PILL_FONT_SIZE2, LEGEND_PILL_FONT_W4, LEGEND_PILL_PAD4, LEGEND_DOT_R4, LEGEND_ENTRY_FONT_SIZE2, LEGEND_ENTRY_FONT_W4, LEGEND_ENTRY_DOT_GAP4, LEGEND_ENTRY_TRAIL4, LEGEND_CAPSULE_PAD4, META_EXCLUDE_KEYS;
13397
+ var CHAR_WIDTH5, MIN_NODE_WIDTH, MAX_NODE_WIDTH, TYPE_LABEL_HEIGHT, DIVIDER_GAP, NAME_HEIGHT, DESC_LINE_HEIGHT, DESC_CHAR_WIDTH, CARD_V_PAD3, CARD_H_PAD3, META_LINE_HEIGHT5, META_CHAR_WIDTH, MARGIN3, BOUNDARY_PAD, GROUP_BOUNDARY_PAD, LEGEND_HEIGHT4, LEGEND_PILL_FONT_SIZE2, LEGEND_PILL_FONT_W4, LEGEND_PILL_PAD4, LEGEND_DOT_R4, LEGEND_ENTRY_FONT_SIZE2, LEGEND_ENTRY_FONT_W5, LEGEND_ENTRY_DOT_GAP4, LEGEND_ENTRY_TRAIL4, LEGEND_CAPSULE_PAD4, EDGE_NODE_COLLISION_WEIGHT, META_EXCLUDE_KEYS;
13042
13398
  var init_layout6 = __esm({
13043
13399
  "src/c4/layout.ts"() {
13044
13400
  "use strict";
@@ -13063,10 +13419,11 @@ var init_layout6 = __esm({
13063
13419
  LEGEND_PILL_PAD4 = 16;
13064
13420
  LEGEND_DOT_R4 = 4;
13065
13421
  LEGEND_ENTRY_FONT_SIZE2 = 10;
13066
- LEGEND_ENTRY_FONT_W4 = LEGEND_ENTRY_FONT_SIZE2 * 0.6;
13422
+ LEGEND_ENTRY_FONT_W5 = LEGEND_ENTRY_FONT_SIZE2 * 0.6;
13067
13423
  LEGEND_ENTRY_DOT_GAP4 = 4;
13068
13424
  LEGEND_ENTRY_TRAIL4 = 8;
13069
13425
  LEGEND_CAPSULE_PAD4 = 4;
13426
+ EDGE_NODE_COLLISION_WEIGHT = 5e3;
13070
13427
  META_EXCLUDE_KEYS = /* @__PURE__ */ new Set(["description", "tech", "technology", "is a"]);
13071
13428
  }
13072
13429
  });
@@ -15251,6 +15608,7 @@ var init_compute = __esm({
15251
15608
  // src/infra/layout.ts
15252
15609
  var layout_exports8 = {};
15253
15610
  __export(layout_exports8, {
15611
+ fixEdgeWaypoints: () => fixEdgeWaypoints,
15254
15612
  layoutInfra: () => layoutInfra,
15255
15613
  separateGroups: () => separateGroups
15256
15614
  });
@@ -15435,6 +15793,8 @@ function formatUptime(fraction) {
15435
15793
  return `${pct.toFixed(1)}%`;
15436
15794
  }
15437
15795
  function separateGroups(groups, nodes, isLR, maxIterations = 20) {
15796
+ const groupDeltas = /* @__PURE__ */ new Map();
15797
+ let converged = false;
15438
15798
  for (let iter = 0; iter < maxIterations; iter++) {
15439
15799
  let anyOverlap = false;
15440
15800
  for (let i = 0; i < groups.length; i++) {
@@ -15452,6 +15812,9 @@ function separateGroups(groups, nodes, isLR, maxIterations = 20) {
15452
15812
  const groupToShift = aCenter <= bCenter ? gb : ga;
15453
15813
  if (isLR) groupToShift.y += shift;
15454
15814
  else groupToShift.x += shift;
15815
+ const prev = groupDeltas.get(groupToShift.id) ?? { dx: 0, dy: 0 };
15816
+ if (isLR) groupDeltas.set(groupToShift.id, { dx: prev.dx, dy: prev.dy + shift });
15817
+ else groupDeltas.set(groupToShift.id, { dx: prev.dx + shift, dy: prev.dy });
15455
15818
  for (const node of nodes) {
15456
15819
  if (node.groupId === groupToShift.id) {
15457
15820
  if (isLR) node.y += shift;
@@ -15460,19 +15823,48 @@ function separateGroups(groups, nodes, isLR, maxIterations = 20) {
15460
15823
  }
15461
15824
  }
15462
15825
  }
15463
- if (!anyOverlap) break;
15826
+ if (!anyOverlap) {
15827
+ converged = true;
15828
+ break;
15829
+ }
15830
+ }
15831
+ if (!converged && maxIterations > 0) {
15832
+ console.warn(`separateGroups: hit maxIterations (${maxIterations}) without fully resolving all group overlaps`);
15833
+ }
15834
+ return groupDeltas;
15835
+ }
15836
+ function fixEdgeWaypoints(edges, nodes, groupDeltas) {
15837
+ if (groupDeltas.size === 0) return;
15838
+ const nodeToGroup = /* @__PURE__ */ new Map();
15839
+ for (const node of nodes) nodeToGroup.set(node.id, node.groupId);
15840
+ for (const edge of edges) {
15841
+ const srcGroup = nodeToGroup.get(edge.sourceId) ?? null;
15842
+ const tgtGroup = nodeToGroup.get(edge.targetId) ?? null;
15843
+ const srcDelta = srcGroup ? groupDeltas.get(srcGroup) : void 0;
15844
+ const tgtDelta = tgtGroup ? groupDeltas.get(tgtGroup) : void 0;
15845
+ if (!srcDelta && !tgtDelta) continue;
15846
+ if (srcDelta && tgtDelta && srcGroup !== tgtGroup) {
15847
+ edge.points = [];
15848
+ continue;
15849
+ }
15850
+ const delta = srcDelta ?? tgtDelta;
15851
+ for (const pt of edge.points) {
15852
+ pt.x += delta.dx;
15853
+ pt.y += delta.dy;
15854
+ }
15464
15855
  }
15465
15856
  }
15466
15857
  function layoutInfra(computed, expandedNodeIds, collapsedNodes) {
15467
15858
  if (computed.nodes.length === 0) {
15468
- return { nodes: [], edges: [], groups: [], options: {}, width: 0, height: 0 };
15859
+ return { nodes: [], edges: [], groups: [], options: {}, direction: computed.direction, width: 0, height: 0 };
15469
15860
  }
15861
+ const isLR = computed.direction !== "TB";
15470
15862
  const g = new dagre7.graphlib.Graph();
15471
15863
  g.setGraph({
15472
15864
  rankdir: computed.direction === "TB" ? "TB" : "LR",
15473
- nodesep: 50,
15474
- ranksep: 100,
15475
- edgesep: 20
15865
+ nodesep: isLR ? 70 : 60,
15866
+ ranksep: isLR ? 150 : 120,
15867
+ edgesep: 30
15476
15868
  });
15477
15869
  g.setDefaultEdgeLabel(() => ({}));
15478
15870
  const groupedNodeIds = /* @__PURE__ */ new Set();
@@ -15480,7 +15872,6 @@ function layoutInfra(computed, expandedNodeIds, collapsedNodes) {
15480
15872
  if (node.groupId) groupedNodeIds.add(node.id);
15481
15873
  }
15482
15874
  const GROUP_INFLATE = GROUP_PADDING3 * 2 + GROUP_HEADER_HEIGHT;
15483
- const isLR = computed.direction !== "TB";
15484
15875
  const widthMap = /* @__PURE__ */ new Map();
15485
15876
  const heightMap = /* @__PURE__ */ new Map();
15486
15877
  for (const node of computed.nodes) {
@@ -15615,7 +16006,8 @@ function layoutInfra(computed, expandedNodeIds, collapsedNodes) {
15615
16006
  lineNumber: group.lineNumber
15616
16007
  };
15617
16008
  });
15618
- separateGroups(layoutGroups, layoutNodes, isLR);
16009
+ const groupDeltas = separateGroups(layoutGroups, layoutNodes, isLR);
16010
+ fixEdgeWaypoints(layoutEdges, layoutNodes, groupDeltas);
15619
16011
  let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
15620
16012
  for (const node of layoutNodes) {
15621
16013
  const left = node.x - node.width / 2;
@@ -15673,6 +16065,7 @@ function layoutInfra(computed, expandedNodeIds, collapsedNodes) {
15673
16065
  edges: layoutEdges,
15674
16066
  groups: layoutGroups,
15675
16067
  options: computed.options,
16068
+ direction: computed.direction,
15676
16069
  width: totalWidth,
15677
16070
  height: totalHeight
15678
16071
  };
@@ -15714,23 +16107,23 @@ var init_layout8 = __esm({
15714
16107
  ]);
15715
16108
  DISPLAY_NAMES = {
15716
16109
  "cache-hit": "cache hit",
15717
- "firewall-block": "fw block",
16110
+ "firewall-block": "firewall block",
15718
16111
  "ratelimit-rps": "rate limit RPS",
15719
16112
  "latency-ms": "latency",
15720
16113
  "uptime": "uptime",
15721
16114
  "instances": "instances",
15722
16115
  "max-rps": "max RPS",
15723
- "cb-error-threshold": "CB error",
15724
- "cb-latency-threshold-ms": "CB latency",
16116
+ "cb-error-threshold": "CB error threshold",
16117
+ "cb-latency-threshold-ms": "CB latency threshold",
15725
16118
  "concurrency": "concurrency",
15726
16119
  "duration-ms": "duration",
15727
16120
  "cold-start-ms": "cold start",
15728
16121
  "buffer": "buffer",
15729
- "drain-rate": "drain",
16122
+ "drain-rate": "drain rate",
15730
16123
  "retention-hours": "retention",
15731
16124
  "partitions": "partitions"
15732
16125
  };
15733
- GROUP_GAP = 24;
16126
+ GROUP_GAP = GROUP_PADDING3 * 2 + GROUP_HEADER_HEIGHT;
15734
16127
  }
15735
16128
  });
15736
16129
 
@@ -15805,6 +16198,236 @@ function resolveNodeSlo(node, diagramOptions) {
15805
16198
  if (availThreshold == null && latencyP90 == null) return null;
15806
16199
  return { availThreshold, latencyP90, warningMargin };
15807
16200
  }
16201
+ function buildPathD(pts, direction) {
16202
+ const gen = d3Shape7.line().x((d) => d.x).y((d) => d.y);
16203
+ if (pts.length <= 2) {
16204
+ gen.curve(direction === "TB" ? d3Shape7.curveBumpY : d3Shape7.curveBumpX);
16205
+ } else {
16206
+ gen.curve(d3Shape7.curveCatmullRom.alpha(0.5));
16207
+ }
16208
+ return gen(pts) ?? "";
16209
+ }
16210
+ function computePortPts(edges, nodeMap, direction) {
16211
+ const srcPts = /* @__PURE__ */ new Map();
16212
+ const tgtPts = /* @__PURE__ */ new Map();
16213
+ const PAD = 0.1;
16214
+ const activeEdges = edges.filter((e) => e.points.length > 0);
16215
+ const bySource = /* @__PURE__ */ new Map();
16216
+ for (const e of activeEdges) {
16217
+ if (!bySource.has(e.sourceId)) bySource.set(e.sourceId, []);
16218
+ bySource.get(e.sourceId).push(e);
16219
+ }
16220
+ for (const [sourceId, es] of bySource) {
16221
+ if (es.length < 2) continue;
16222
+ const source = nodeMap.get(sourceId);
16223
+ if (!source) continue;
16224
+ const sorted = es.map((e) => ({ e, t: nodeMap.get(e.targetId) })).filter((x) => x.t != null).sort((a, b) => direction === "LR" ? a.t.y - b.t.y : a.t.x - b.t.x);
16225
+ const n = sorted.length;
16226
+ for (let i = 0; i < n; i++) {
16227
+ const frac = n === 1 ? 0.5 : PAD + (1 - 2 * PAD) * i / (n - 1);
16228
+ const { e, t } = sorted[i];
16229
+ const isBackward = direction === "LR" ? t.x < source.x : t.y < source.y;
16230
+ if (direction === "LR") {
16231
+ srcPts.set(`${e.sourceId}:${e.targetId}`, {
16232
+ x: isBackward ? source.x - source.width / 2 : source.x + source.width / 2,
16233
+ y: source.y - source.height / 2 + frac * source.height
16234
+ });
16235
+ } else {
16236
+ srcPts.set(`${e.sourceId}:${e.targetId}`, {
16237
+ x: source.x - source.width / 2 + frac * source.width,
16238
+ y: isBackward ? source.y - source.height / 2 : source.y + source.height / 2
16239
+ });
16240
+ }
16241
+ }
16242
+ }
16243
+ const byTarget = /* @__PURE__ */ new Map();
16244
+ for (const e of activeEdges) {
16245
+ if (!byTarget.has(e.targetId)) byTarget.set(e.targetId, []);
16246
+ byTarget.get(e.targetId).push(e);
16247
+ }
16248
+ for (const [targetId, es] of byTarget) {
16249
+ if (es.length < 2) continue;
16250
+ const target = nodeMap.get(targetId);
16251
+ if (!target) continue;
16252
+ const sorted = es.map((e) => ({ e, s: nodeMap.get(e.sourceId) })).filter((x) => x.s != null).sort((a, b) => direction === "LR" ? a.s.y - b.s.y : a.s.x - b.s.x);
16253
+ const n = sorted.length;
16254
+ for (let i = 0; i < n; i++) {
16255
+ const frac = n === 1 ? 0.5 : PAD + (1 - 2 * PAD) * i / (n - 1);
16256
+ const { e, s } = sorted[i];
16257
+ const isBackward = direction === "LR" ? target.x < s.x : target.y < s.y;
16258
+ if (direction === "LR") {
16259
+ tgtPts.set(`${e.sourceId}:${e.targetId}`, {
16260
+ x: isBackward ? target.x + target.width / 2 : target.x - target.width / 2,
16261
+ y: target.y - target.height / 2 + frac * target.height
16262
+ });
16263
+ } else {
16264
+ tgtPts.set(`${e.sourceId}:${e.targetId}`, {
16265
+ x: target.x - target.width / 2 + frac * target.width,
16266
+ y: isBackward ? target.y + target.height / 2 : target.y - target.height / 2
16267
+ });
16268
+ }
16269
+ }
16270
+ }
16271
+ return { srcPts, tgtPts };
16272
+ }
16273
+ function findRoutingLane(blocking, targetY, margin) {
16274
+ const MERGE_SLOP = 4;
16275
+ const sorted = [...blocking].sort((a, b) => a.y + a.height / 2 - (b.y + b.height / 2));
16276
+ const merged = [];
16277
+ for (const r of sorted) {
16278
+ const lo = r.y - MERGE_SLOP;
16279
+ const hi = r.y + r.height + MERGE_SLOP;
16280
+ if (merged.length && lo <= merged[merged.length - 1][1]) {
16281
+ merged[merged.length - 1][1] = Math.max(merged[merged.length - 1][1], hi);
16282
+ } else {
16283
+ merged.push([lo, hi]);
16284
+ }
16285
+ }
16286
+ if (merged.length === 0) return targetY;
16287
+ const MIN_GAP = 10;
16288
+ const candidates = [
16289
+ merged[0][0] - margin,
16290
+ // above all blocking rects
16291
+ merged[merged.length - 1][1] + margin
16292
+ // below all blocking rects
16293
+ ];
16294
+ for (let i = 0; i < merged.length - 1; i++) {
16295
+ const gapLo = merged[i][1];
16296
+ const gapHi = merged[i + 1][0];
16297
+ if (gapHi - gapLo >= MIN_GAP) {
16298
+ candidates.push((gapLo + gapHi) / 2);
16299
+ }
16300
+ }
16301
+ return candidates.reduce(
16302
+ (best, c) => Math.abs(c - targetY) < Math.abs(best - targetY) ? c : best,
16303
+ candidates[0]
16304
+ );
16305
+ }
16306
+ function segmentIntersectsRect(p1, p2, rect) {
16307
+ const { x: rx, y: ry, width: rw, height: rh } = rect;
16308
+ const rr = rx + rw;
16309
+ const rb = ry + rh;
16310
+ const inRect = (p) => p.x >= rx && p.x <= rr && p.y >= ry && p.y <= rb;
16311
+ if (inRect(p1) || inRect(p2)) return true;
16312
+ if (Math.max(p1.x, p2.x) < rx || Math.min(p1.x, p2.x) > rr) return false;
16313
+ if (Math.max(p1.y, p2.y) < ry || Math.min(p1.y, p2.y) > rb) return false;
16314
+ const cross = (o, a, b) => (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
16315
+ const crosses = (a, b) => {
16316
+ const d1 = cross(a, b, p1);
16317
+ const d2 = cross(a, b, p2);
16318
+ const d3 = cross(p1, p2, a);
16319
+ const d4 = cross(p1, p2, b);
16320
+ return (d1 > 0 && d2 < 0 || d1 < 0 && d2 > 0) && (d3 > 0 && d4 < 0 || d3 < 0 && d4 > 0);
16321
+ };
16322
+ const tl = { x: rx, y: ry };
16323
+ const tr = { x: rr, y: ry };
16324
+ const br = { x: rr, y: rb };
16325
+ const bl = { x: rx, y: rb };
16326
+ return crosses(tl, tr) || crosses(tr, br) || crosses(br, bl) || crosses(bl, tl);
16327
+ }
16328
+ function curveIntersectsRect(sc, tc, rect, direction) {
16329
+ if (direction === "LR") {
16330
+ const midX = (sc.x + tc.x) / 2;
16331
+ const m1 = { x: midX, y: sc.y };
16332
+ const m2 = { x: midX, y: tc.y };
16333
+ return segmentIntersectsRect(sc, m1, rect) || segmentIntersectsRect(m1, m2, rect) || segmentIntersectsRect(m2, tc, rect);
16334
+ } else {
16335
+ const midY = (sc.y + tc.y) / 2;
16336
+ const m1 = { x: sc.x, y: midY };
16337
+ const m2 = { x: tc.x, y: midY };
16338
+ return segmentIntersectsRect(sc, m1, rect) || segmentIntersectsRect(m1, m2, rect) || segmentIntersectsRect(m2, tc, rect);
16339
+ }
16340
+ }
16341
+ function edgeWaypoints(source, target, groups, nodes, direction, margin = 30, srcExitPt, tgtEnterPt) {
16342
+ const sc = { x: source.x, y: source.y };
16343
+ const tc = { x: target.x, y: target.y };
16344
+ const isBackward = direction === "LR" ? tc.x < sc.x : tc.y < sc.y;
16345
+ if (isBackward) {
16346
+ if (direction === "LR") {
16347
+ const xBandObs = [];
16348
+ for (const g of groups) {
16349
+ if (g.x + g.width < tc.x - margin || g.x > sc.x + margin) continue;
16350
+ xBandObs.push({ x: g.x, y: g.y, width: g.width, height: g.height });
16351
+ }
16352
+ for (const n of nodes) {
16353
+ if (n.id === source.id || n.id === target.id) continue;
16354
+ const nLeft = n.x - n.width / 2;
16355
+ const nRight = n.x + n.width / 2;
16356
+ if (nRight < tc.x - margin || nLeft > sc.x + margin) continue;
16357
+ xBandObs.push({ x: nLeft, y: n.y - n.height / 2, width: n.width, height: n.height });
16358
+ }
16359
+ const midY = (sc.y + tc.y) / 2;
16360
+ const routeY2 = xBandObs.length > 0 ? findRoutingLane(xBandObs, midY, margin) : midY;
16361
+ const exitBorder = srcExitPt ?? nodeBorderPoint(source, { x: sc.x, y: routeY2 });
16362
+ const exitPt2 = { x: exitBorder.x, y: routeY2 };
16363
+ const enterPt2 = { x: tc.x, y: routeY2 };
16364
+ const tp2 = tgtEnterPt ?? nodeBorderPoint(target, enterPt2);
16365
+ return srcExitPt ? [srcExitPt, exitPt2, enterPt2, tp2] : [exitBorder, exitPt2, enterPt2, tp2];
16366
+ } else {
16367
+ const yBandObs = [];
16368
+ for (const g of groups) {
16369
+ if (g.y + g.height < tc.y - margin || g.y > sc.y + margin) continue;
16370
+ yBandObs.push({ x: g.x, y: g.y, width: g.width, height: g.height });
16371
+ }
16372
+ for (const n of nodes) {
16373
+ if (n.id === source.id || n.id === target.id) continue;
16374
+ const nTop = n.y - n.height / 2;
16375
+ const nBot = n.y + n.height / 2;
16376
+ if (nBot < tc.y - margin || nTop > sc.y + margin) continue;
16377
+ yBandObs.push({ x: n.x - n.width / 2, y: nTop, width: n.width, height: n.height });
16378
+ }
16379
+ const rotated = yBandObs.map((r) => ({ x: r.y, y: r.x, width: r.height, height: r.width }));
16380
+ const midX = (sc.x + tc.x) / 2;
16381
+ const routeX = rotated.length > 0 ? findRoutingLane(rotated, midX, margin) : midX;
16382
+ const exitPt2 = srcExitPt ?? { x: routeX, y: sc.y };
16383
+ const enterPt2 = { x: routeX, y: tc.y };
16384
+ return [
16385
+ srcExitPt ?? nodeBorderPoint(source, exitPt2),
16386
+ exitPt2,
16387
+ enterPt2,
16388
+ tgtEnterPt ?? nodeBorderPoint(target, enterPt2)
16389
+ ];
16390
+ }
16391
+ }
16392
+ const blocking = [];
16393
+ const blockingGroupIds = /* @__PURE__ */ new Set();
16394
+ const pathSrc = srcExitPt ?? sc;
16395
+ const pathTgt = tgtEnterPt ?? tc;
16396
+ for (const g of groups) {
16397
+ if (g.id === source.groupId || g.id === target.groupId) continue;
16398
+ const gRect = { x: g.x, y: g.y, width: g.width, height: g.height };
16399
+ if (curveIntersectsRect(pathSrc, pathTgt, gRect, direction)) {
16400
+ blocking.push(gRect);
16401
+ blockingGroupIds.add(g.id);
16402
+ }
16403
+ }
16404
+ for (const n of nodes) {
16405
+ if (n.id === source.id || n.id === target.id) continue;
16406
+ if (n.groupId && (n.groupId === source.groupId || n.groupId === target.groupId)) continue;
16407
+ if (n.groupId && blockingGroupIds.has(n.groupId)) continue;
16408
+ const nodeRect = { x: n.x - n.width / 2, y: n.y - n.height / 2, width: n.width, height: n.height };
16409
+ if (curveIntersectsRect(pathSrc, pathTgt, nodeRect, direction)) {
16410
+ blocking.push(nodeRect);
16411
+ }
16412
+ }
16413
+ if (blocking.length === 0) {
16414
+ const sp = srcExitPt ?? nodeBorderPoint(source, tc);
16415
+ const tp2 = tgtEnterPt ?? nodeBorderPoint(target, sp);
16416
+ return [sp, tp2];
16417
+ }
16418
+ const obsLeft = Math.min(...blocking.map((o) => o.x));
16419
+ const obsRight = Math.max(...blocking.map((o) => o.x + o.width));
16420
+ const routeY = findRoutingLane(blocking, tc.y, margin);
16421
+ const exitX = direction === "LR" ? Math.max(sc.x, obsLeft - margin) : obsLeft - margin;
16422
+ const enterX = direction === "LR" ? Math.min(tc.x, obsRight + margin) : obsRight + margin;
16423
+ const exitPt = { x: exitX, y: routeY };
16424
+ const enterPt = { x: enterX, y: routeY };
16425
+ const tp = tgtEnterPt ?? nodeBorderPoint(target, enterPt);
16426
+ if (srcExitPt) {
16427
+ return [srcExitPt, exitPt, enterPt, tp];
16428
+ }
16429
+ return [nodeBorderPoint(source, exitPt), exitPt, enterPt, tp];
16430
+ }
15808
16431
  function nodeBorderPoint(node, target) {
15809
16432
  const hw = node.width / 2;
15810
16433
  const hh = node.height / 2;
@@ -16088,33 +16711,29 @@ function renderGroups(svg, groups, palette, isDark) {
16088
16711
  }
16089
16712
  }
16090
16713
  }
16091
- function renderEdgePaths(svg, edges, nodes, palette, isDark, animate) {
16714
+ function renderEdgePaths(svg, edges, nodes, groups, palette, isDark, animate, direction) {
16092
16715
  const nodeMap = new Map(nodes.map((n) => [n.id, n]));
16093
16716
  const maxRps = Math.max(...edges.map((e) => e.computedRps), 1);
16717
+ const { srcPts, tgtPts } = computePortPts(edges, nodeMap, direction);
16094
16718
  for (const edge of edges) {
16095
16719
  if (edge.points.length === 0) continue;
16096
16720
  const targetNode = nodeMap.get(edge.targetId);
16097
16721
  const sourceNode = nodeMap.get(edge.sourceId);
16098
16722
  const color = edgeColor(edge, palette);
16099
16723
  const strokeW = edgeWidth();
16100
- let pts = edge.points;
16101
- if (sourceNode && targetNode && pts.length >= 2) {
16102
- const first = pts[0];
16103
- const distFirstToSource = (first.x - sourceNode.x) ** 2 + (first.y - sourceNode.y) ** 2;
16104
- const distFirstToTarget = (first.x - targetNode.x) ** 2 + (first.y - targetNode.y) ** 2;
16105
- if (distFirstToTarget < distFirstToSource) {
16106
- pts = [...pts].reverse();
16107
- }
16108
- }
16109
- if (sourceNode && pts.length > 0) {
16110
- const bp = nodeBorderPoint(sourceNode, pts[0]);
16111
- pts = [bp, ...pts];
16112
- }
16113
- if (targetNode && pts.length > 0) {
16114
- const bp = nodeBorderPoint(targetNode, pts[pts.length - 1]);
16115
- pts = [...pts, bp];
16116
- }
16117
- const pathD = lineGenerator7(pts) ?? "";
16724
+ if (!sourceNode || !targetNode) continue;
16725
+ const key = `${edge.sourceId}:${edge.targetId}`;
16726
+ const pts = edgeWaypoints(
16727
+ sourceNode,
16728
+ targetNode,
16729
+ groups,
16730
+ nodes,
16731
+ direction,
16732
+ 30,
16733
+ srcPts.get(key),
16734
+ tgtPts.get(key)
16735
+ );
16736
+ const pathD = buildPathD(pts, direction);
16118
16737
  const edgeG = svg.append("g").attr("class", "infra-edge").attr("data-line-number", edge.lineNumber);
16119
16738
  edgeG.append("path").attr("d", pathD).attr("fill", "none").attr("stroke", color).attr("stroke-width", strokeW);
16120
16739
  if (animate && edge.computedRps > 0) {
@@ -16129,19 +16748,34 @@ function renderEdgePaths(svg, edges, nodes, palette, isDark, animate) {
16129
16748
  }
16130
16749
  }
16131
16750
  }
16132
- function renderEdgeLabels(svg, edges, palette, isDark, animate) {
16751
+ function renderEdgeLabels(svg, edges, nodes, groups, palette, isDark, animate, direction) {
16752
+ const nodeMap = new Map(nodes.map((n) => [n.id, n]));
16753
+ const { srcPts, tgtPts } = computePortPts(edges, nodeMap, direction);
16133
16754
  for (const edge of edges) {
16134
16755
  if (edge.points.length === 0) continue;
16135
16756
  if (!edge.label) continue;
16136
- const midIdx = Math.floor(edge.points.length / 2);
16137
- const midPt = edge.points[midIdx];
16757
+ const sourceNode = nodeMap.get(edge.sourceId);
16758
+ const targetNode = nodeMap.get(edge.targetId);
16759
+ if (!sourceNode || !targetNode) continue;
16760
+ const key = `${edge.sourceId}:${edge.targetId}`;
16761
+ const wps = edgeWaypoints(
16762
+ sourceNode,
16763
+ targetNode,
16764
+ groups,
16765
+ nodes,
16766
+ direction,
16767
+ 30,
16768
+ srcPts.get(key),
16769
+ tgtPts.get(key)
16770
+ );
16771
+ const midPt = wps[Math.floor(wps.length / 2)];
16138
16772
  const labelText = edge.label;
16139
16773
  const g = svg.append("g").attr("class", animate ? "infra-edge-label" : "");
16140
16774
  const textWidth = labelText.length * 6.5 + 8;
16141
16775
  g.append("rect").attr("x", midPt.x - textWidth / 2).attr("y", midPt.y - 8).attr("width", textWidth).attr("height", 16).attr("rx", 3).attr("fill", palette.bg).attr("opacity", 0.9);
16142
16776
  g.append("text").attr("x", midPt.x).attr("y", midPt.y + 4).attr("text-anchor", "middle").attr("font-family", FONT_FAMILY).attr("font-size", EDGE_LABEL_FONT_SIZE7).attr("fill", palette.textMuted).text(labelText);
16143
16777
  if (animate) {
16144
- const pathD = lineGenerator7(edge.points) ?? "";
16778
+ const pathD = buildPathD(wps, direction);
16145
16779
  g.insert("path", ":first-child").attr("d", pathD).attr("fill", "none").attr("stroke", "transparent").attr("stroke-width", 20);
16146
16780
  }
16147
16781
  }
@@ -16551,7 +17185,7 @@ function renderInfra(container, layout, palette, isDark, title, titleLineNumber,
16551
17185
  rootSvg.append("text").attr("class", "chart-title").attr("x", totalWidth / 2).attr("y", 28).attr("text-anchor", "middle").attr("font-family", FONT_FAMILY).attr("font-size", 18).attr("font-weight", "700").attr("fill", palette.text).attr("data-line-number", titleLineNumber != null ? titleLineNumber : "").text(title);
16552
17186
  }
16553
17187
  renderGroups(svg, layout.groups, palette, isDark);
16554
- renderEdgePaths(svg, layout.edges, layout.nodes, palette, isDark, shouldAnimate);
17188
+ renderEdgePaths(svg, layout.edges, layout.nodes, layout.groups, palette, isDark, shouldAnimate, layout.direction);
16555
17189
  const fanoutSourceIds = collectFanoutSourceIds(layout.edges);
16556
17190
  const scaledGroupIds = new Set(
16557
17191
  layout.groups.filter((g) => {
@@ -16563,7 +17197,7 @@ function renderInfra(container, layout, palette, isDark, title, titleLineNumber,
16563
17197
  if (shouldAnimate) {
16564
17198
  renderRejectParticles(svg, layout.nodes);
16565
17199
  }
16566
- renderEdgeLabels(svg, layout.edges, palette, isDark, shouldAnimate);
17200
+ renderEdgeLabels(svg, layout.edges, layout.nodes, layout.groups, palette, isDark, shouldAnimate, layout.direction);
16567
17201
  if (hasLegend) {
16568
17202
  if (fixedLegend) {
16569
17203
  const containerWidth = container.clientWidth || totalWidth;
@@ -16581,7 +17215,7 @@ function parseAndLayoutInfra(content) {
16581
17215
  const layout = layoutInfra(computed);
16582
17216
  return { parsed, computed, layout };
16583
17217
  }
16584
- var NODE_FONT_SIZE4, META_FONT_SIZE4, META_LINE_HEIGHT8, EDGE_LABEL_FONT_SIZE7, GROUP_LABEL_FONT_SIZE2, NODE_BORDER_RADIUS, EDGE_STROKE_WIDTH8, NODE_STROKE_WIDTH8, OVERLOAD_STROKE_WIDTH, ROLE_DOT_RADIUS, NODE_HEADER_HEIGHT2, NODE_SEPARATOR_GAP2, NODE_PAD_BOTTOM2, COLLAPSE_BAR_HEIGHT5, COLLAPSE_BAR_INSET2, LEGEND_FIXED_GAP3, COLOR_HEALTHY, COLOR_WARNING, COLOR_OVERLOADED, FLOW_SPEED_MIN, FLOW_SPEED_MAX, PARTICLE_R, PARTICLE_COUNT_MIN, PARTICLE_COUNT_MAX, NODE_PULSE_SPEED, NODE_PULSE_OVERLOAD, REJECT_PARTICLE_R, REJECT_DROP_DISTANCE, REJECT_DURATION_MIN, REJECT_DURATION_MAX, REJECT_COUNT_MIN, REJECT_COUNT_MAX, lineGenerator7, PROP_DISPLAY, DESC_MAX_CHARS, RPS_FORMAT_KEYS, MS_FORMAT_KEYS, PCT_FORMAT_KEYS;
17218
+ var NODE_FONT_SIZE4, META_FONT_SIZE4, META_LINE_HEIGHT8, EDGE_LABEL_FONT_SIZE7, GROUP_LABEL_FONT_SIZE2, NODE_BORDER_RADIUS, EDGE_STROKE_WIDTH8, NODE_STROKE_WIDTH8, OVERLOAD_STROKE_WIDTH, ROLE_DOT_RADIUS, NODE_HEADER_HEIGHT2, NODE_SEPARATOR_GAP2, NODE_PAD_BOTTOM2, COLLAPSE_BAR_HEIGHT5, COLLAPSE_BAR_INSET2, LEGEND_FIXED_GAP3, COLOR_HEALTHY, COLOR_WARNING, COLOR_OVERLOADED, FLOW_SPEED_MIN, FLOW_SPEED_MAX, PARTICLE_R, PARTICLE_COUNT_MIN, PARTICLE_COUNT_MAX, NODE_PULSE_SPEED, NODE_PULSE_OVERLOAD, REJECT_PARTICLE_R, REJECT_DROP_DISTANCE, REJECT_DURATION_MIN, REJECT_DURATION_MAX, REJECT_COUNT_MIN, REJECT_COUNT_MAX, PROP_DISPLAY, DESC_MAX_CHARS, RPS_FORMAT_KEYS, MS_FORMAT_KEYS, PCT_FORMAT_KEYS;
16585
17219
  var init_renderer8 = __esm({
16586
17220
  "src/infra/renderer.ts"() {
16587
17221
  "use strict";
@@ -16625,7 +17259,6 @@ var init_renderer8 = __esm({
16625
17259
  REJECT_DURATION_MAX = 3;
16626
17260
  REJECT_COUNT_MIN = 1;
16627
17261
  REJECT_COUNT_MAX = 3;
16628
- lineGenerator7 = d3Shape7.line().x((d) => d.x).y((d) => d.y).curve(d3Shape7.curveBasis);
16629
17262
  PROP_DISPLAY = {
16630
17263
  "cache-hit": "cache hit",
16631
17264
  "firewall-block": "firewall block",
@@ -16810,7 +17443,7 @@ function renderState(container, graph, layout, palette, isDark, onClickItem, exp
16810
17443
  }
16811
17444
  }
16812
17445
  } else if (edge.points.length >= 2) {
16813
- const pathD = lineGenerator8(edge.points);
17446
+ const pathD = lineGenerator7(edge.points);
16814
17447
  if (pathD) {
16815
17448
  edgeG.append("path").attr("d", pathD).attr("fill", "none").attr("stroke", edgeColor2).attr("stroke-width", EDGE_STROKE_WIDTH9).attr("marker-end", `url(#${markerId})`).attr("class", "st-edge");
16816
17449
  }
@@ -16874,7 +17507,7 @@ function renderStateForExport(content, theme, palette) {
16874
17507
  document.body.removeChild(container);
16875
17508
  }
16876
17509
  }
16877
- var DIAGRAM_PADDING9, MAX_SCALE8, NODE_FONT_SIZE5, EDGE_LABEL_FONT_SIZE8, GROUP_LABEL_FONT_SIZE3, EDGE_STROKE_WIDTH9, NODE_STROKE_WIDTH9, ARROWHEAD_W4, ARROWHEAD_H4, PSEUDOSTATE_RADIUS, STATE_CORNER_RADIUS, GROUP_EXTRA_PADDING2, lineGenerator8;
17510
+ var DIAGRAM_PADDING9, MAX_SCALE8, NODE_FONT_SIZE5, EDGE_LABEL_FONT_SIZE8, GROUP_LABEL_FONT_SIZE3, EDGE_STROKE_WIDTH9, NODE_STROKE_WIDTH9, ARROWHEAD_W4, ARROWHEAD_H4, PSEUDOSTATE_RADIUS, STATE_CORNER_RADIUS, GROUP_EXTRA_PADDING2, lineGenerator7;
16878
17511
  var init_state_renderer = __esm({
16879
17512
  "src/graph/state-renderer.ts"() {
16880
17513
  "use strict";
@@ -16894,7 +17527,7 @@ var init_state_renderer = __esm({
16894
17527
  PSEUDOSTATE_RADIUS = 10;
16895
17528
  STATE_CORNER_RADIUS = 10;
16896
17529
  GROUP_EXTRA_PADDING2 = 12;
16897
- lineGenerator8 = d3Shape8.line().x((d) => d.x).y((d) => d.y).curve(d3Shape8.curveBasis);
17530
+ lineGenerator7 = d3Shape8.line().x((d) => d.x).y((d) => d.y).curve(d3Shape8.curveBasis);
16898
17531
  }
16899
17532
  });
16900
17533