@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.cjs CHANGED
@@ -10497,110 +10497,211 @@ function computeNodeDimensions2(table) {
10497
10497
  const height = headerHeight + columnsHeight + (columnsHeight === 0 ? 4 : 0);
10498
10498
  return { width, height, headerHeight, columnsHeight };
10499
10499
  }
10500
+ function findConnectedComponents(tableIds, relationships) {
10501
+ const adj = /* @__PURE__ */ new Map();
10502
+ for (const id of tableIds) adj.set(id, /* @__PURE__ */ new Set());
10503
+ for (const rel of relationships) {
10504
+ adj.get(rel.source)?.add(rel.target);
10505
+ adj.get(rel.target)?.add(rel.source);
10506
+ }
10507
+ const visited = /* @__PURE__ */ new Set();
10508
+ const components = [];
10509
+ for (const id of tableIds) {
10510
+ if (visited.has(id)) continue;
10511
+ const comp = [];
10512
+ const queue = [id];
10513
+ while (queue.length > 0) {
10514
+ const cur = queue.shift();
10515
+ if (visited.has(cur)) continue;
10516
+ visited.add(cur);
10517
+ comp.push(cur);
10518
+ for (const nb of adj.get(cur) ?? []) {
10519
+ if (!visited.has(nb)) queue.push(nb);
10520
+ }
10521
+ }
10522
+ components.push(comp);
10523
+ }
10524
+ return components;
10525
+ }
10526
+ function layoutComponent(tables, rels, dimMap) {
10527
+ const nodePositions = /* @__PURE__ */ new Map();
10528
+ const edgePoints = /* @__PURE__ */ new Map();
10529
+ if (tables.length === 1) {
10530
+ const dims = dimMap.get(tables[0].id);
10531
+ nodePositions.set(tables[0].id, { x: dims.width / 2, y: dims.height / 2, ...dims });
10532
+ return { nodePositions, edgePoints, width: dims.width, height: dims.height };
10533
+ }
10534
+ const g = new import_dagre3.default.graphlib.Graph({ multigraph: true });
10535
+ g.setGraph({ rankdir: "LR", nodesep: 40, ranksep: 80, edgesep: 20 });
10536
+ g.setDefaultEdgeLabel(() => ({}));
10537
+ for (const table of tables) {
10538
+ const dims = dimMap.get(table.id);
10539
+ g.setNode(table.id, { width: dims.width, height: dims.height });
10540
+ }
10541
+ for (const rel of rels) {
10542
+ g.setEdge(rel.source, rel.target, { label: rel.label ?? "" }, String(rel.lineNumber));
10543
+ }
10544
+ import_dagre3.default.layout(g);
10545
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
10546
+ for (const table of tables) {
10547
+ const pos = g.node(table.id);
10548
+ const dims = dimMap.get(table.id);
10549
+ minX = Math.min(minX, pos.x - dims.width / 2);
10550
+ minY = Math.min(minY, pos.y - dims.height / 2);
10551
+ maxX = Math.max(maxX, pos.x + dims.width / 2);
10552
+ maxY = Math.max(maxY, pos.y + dims.height / 2);
10553
+ }
10554
+ for (const rel of rels) {
10555
+ const ed = g.edge(rel.source, rel.target, String(rel.lineNumber));
10556
+ for (const pt of ed?.points ?? []) {
10557
+ minX = Math.min(minX, pt.x);
10558
+ minY = Math.min(minY, pt.y);
10559
+ maxX = Math.max(maxX, pt.x);
10560
+ maxY = Math.max(maxY, pt.y);
10561
+ }
10562
+ if (rel.label && (ed?.points ?? []).length > 0) {
10563
+ const pts = ed.points;
10564
+ const mid = pts[Math.floor(pts.length / 2)];
10565
+ const hw = (rel.label.length * 7 + 8) / 2;
10566
+ minX = Math.min(minX, mid.x - hw);
10567
+ maxX = Math.max(maxX, mid.x + hw);
10568
+ }
10569
+ }
10570
+ for (const table of tables) {
10571
+ const pos = g.node(table.id);
10572
+ const dims = dimMap.get(table.id);
10573
+ nodePositions.set(table.id, {
10574
+ x: pos.x - minX,
10575
+ y: pos.y - minY,
10576
+ ...dims
10577
+ });
10578
+ }
10579
+ for (const rel of rels) {
10580
+ const ed = g.edge(rel.source, rel.target, String(rel.lineNumber));
10581
+ edgePoints.set(
10582
+ rel.lineNumber,
10583
+ (ed?.points ?? []).map((pt) => ({ x: pt.x - minX, y: pt.y - minY }))
10584
+ );
10585
+ }
10586
+ return {
10587
+ nodePositions,
10588
+ edgePoints,
10589
+ width: Math.max(0, maxX - minX),
10590
+ height: Math.max(0, maxY - minY)
10591
+ };
10592
+ }
10593
+ function packComponents(items) {
10594
+ if (items.length === 0) return [];
10595
+ const sorted = [...items].sort((a, b) => {
10596
+ const aConnected = a.compIds.length > 1 ? 1 : 0;
10597
+ const bConnected = b.compIds.length > 1 ? 1 : 0;
10598
+ if (aConnected !== bConnected) return bConnected - aConnected;
10599
+ return b.compLayout.height - a.compLayout.height;
10600
+ });
10601
+ const totalArea = items.reduce(
10602
+ (s, c) => s + (c.compLayout.width || MIN_WIDTH2) * (c.compLayout.height || HEADER_BASE2),
10603
+ 0
10604
+ );
10605
+ const targetW = Math.max(
10606
+ Math.sqrt(totalArea) * 1.5,
10607
+ sorted[0].compLayout.width
10608
+ // at least as wide as the widest component
10609
+ );
10610
+ const placements = [];
10611
+ let curX = 0;
10612
+ let curY = 0;
10613
+ let rowH = 0;
10614
+ for (const item of sorted) {
10615
+ const w = item.compLayout.width || MIN_WIDTH2;
10616
+ const h = item.compLayout.height || HEADER_BASE2;
10617
+ if (curX > 0 && curX + w > targetW) {
10618
+ curY += rowH + COMP_GAP;
10619
+ curX = 0;
10620
+ rowH = 0;
10621
+ }
10622
+ placements.push({ compIds: item.compIds, compLayout: item.compLayout, offsetX: curX, offsetY: curY });
10623
+ curX += w + COMP_GAP;
10624
+ rowH = Math.max(rowH, h);
10625
+ }
10626
+ return placements;
10627
+ }
10500
10628
  function layoutERDiagram(parsed) {
10501
10629
  if (parsed.tables.length === 0) {
10502
10630
  return { nodes: [], edges: [], width: 0, height: 0 };
10503
10631
  }
10504
- const g = new import_dagre3.default.graphlib.Graph();
10505
- g.setGraph({
10506
- rankdir: "TB",
10507
- nodesep: 60,
10508
- ranksep: 80,
10509
- edgesep: 20
10510
- });
10511
- g.setDefaultEdgeLabel(() => ({}));
10512
10632
  const dimMap = /* @__PURE__ */ new Map();
10513
10633
  for (const table of parsed.tables) {
10514
- const dims = computeNodeDimensions2(table);
10515
- dimMap.set(table.id, dims);
10516
- g.setNode(table.id, {
10517
- label: table.name,
10518
- width: dims.width,
10519
- height: dims.height
10520
- });
10634
+ dimMap.set(table.id, computeNodeDimensions2(table));
10521
10635
  }
10522
- for (const rel of parsed.relationships) {
10523
- g.setEdge(rel.source, rel.target, { label: rel.label ?? "" });
10636
+ const compIdSets = findConnectedComponents(
10637
+ parsed.tables.map((t) => t.id),
10638
+ parsed.relationships
10639
+ );
10640
+ const tableById = new Map(parsed.tables.map((t) => [t.id, t]));
10641
+ const componentItems = compIdSets.map((ids) => {
10642
+ const tables = ids.map((id) => tableById.get(id));
10643
+ const rels = parsed.relationships.filter((r) => ids.includes(r.source));
10644
+ return { compIds: ids, compLayout: layoutComponent(tables, rels, dimMap) };
10645
+ });
10646
+ const packed = packComponents(componentItems);
10647
+ const placementByTableId = /* @__PURE__ */ new Map();
10648
+ for (const p of packed) {
10649
+ for (const id of p.compIds) placementByTableId.set(id, p);
10650
+ }
10651
+ const placementByRelLine = /* @__PURE__ */ new Map();
10652
+ for (const p of packed) {
10653
+ for (const lineNum of p.compLayout.edgePoints.keys()) {
10654
+ placementByRelLine.set(lineNum, p);
10655
+ }
10524
10656
  }
10525
- import_dagre3.default.layout(g);
10526
10657
  const layoutNodes = parsed.tables.map((table) => {
10527
- const pos = g.node(table.id);
10528
- const dims = dimMap.get(table.id);
10658
+ const p = placementByTableId.get(table.id);
10659
+ const pos = p.compLayout.nodePositions.get(table.id);
10529
10660
  return {
10530
10661
  ...table,
10531
- x: pos.x,
10532
- y: pos.y,
10533
- width: dims.width,
10534
- height: dims.height,
10535
- headerHeight: dims.headerHeight,
10536
- columnsHeight: dims.columnsHeight
10662
+ x: pos.x + p.offsetX + HALF_MARGIN,
10663
+ y: pos.y + p.offsetY + HALF_MARGIN,
10664
+ width: pos.width,
10665
+ height: pos.height,
10666
+ headerHeight: pos.headerHeight,
10667
+ columnsHeight: pos.columnsHeight
10537
10668
  };
10538
10669
  });
10539
10670
  const layoutEdges = parsed.relationships.map((rel) => {
10540
- const edgeData = g.edge(rel.source, rel.target);
10671
+ const p = placementByRelLine.get(rel.lineNumber);
10672
+ const pts = p?.compLayout.edgePoints.get(rel.lineNumber) ?? [];
10541
10673
  return {
10542
10674
  source: rel.source,
10543
10675
  target: rel.target,
10544
10676
  cardinality: rel.cardinality,
10545
- points: edgeData?.points ?? [],
10677
+ points: pts.map((pt) => ({
10678
+ x: pt.x + (p?.offsetX ?? 0) + HALF_MARGIN,
10679
+ y: pt.y + (p?.offsetY ?? 0) + HALF_MARGIN
10680
+ })),
10546
10681
  label: rel.label,
10547
10682
  lineNumber: rel.lineNumber
10548
10683
  };
10549
10684
  });
10550
- let minX = Infinity;
10551
- let minY = Infinity;
10552
10685
  let maxX = 0;
10553
10686
  let maxY = 0;
10554
10687
  for (const node of layoutNodes) {
10555
- const left = node.x - node.width / 2;
10556
- const right = node.x + node.width / 2;
10557
- const top = node.y - node.height / 2;
10558
- const bottom = node.y + node.height / 2;
10559
- if (left < minX) minX = left;
10560
- if (right > maxX) maxX = right;
10561
- if (top < minY) minY = top;
10562
- if (bottom > maxY) maxY = bottom;
10688
+ maxX = Math.max(maxX, node.x + node.width / 2);
10689
+ maxY = Math.max(maxY, node.y + node.height / 2);
10563
10690
  }
10564
10691
  for (const edge of layoutEdges) {
10565
10692
  for (const pt of edge.points) {
10566
- if (pt.x < minX) minX = pt.x;
10567
- if (pt.x > maxX) maxX = pt.x;
10568
- if (pt.y < minY) minY = pt.y;
10569
- if (pt.y > maxY) maxY = pt.y;
10570
- }
10571
- if (edge.label && edge.points.length > 0) {
10572
- const midPt = edge.points[Math.floor(edge.points.length / 2)];
10573
- const labelHalfW = (edge.label.length * 7 + 8) / 2;
10574
- if (midPt.x + labelHalfW > maxX) maxX = midPt.x + labelHalfW;
10575
- if (midPt.x - labelHalfW < minX) minX = midPt.x - labelHalfW;
10693
+ maxX = Math.max(maxX, pt.x);
10694
+ maxY = Math.max(maxY, pt.y);
10576
10695
  }
10577
10696
  }
10578
- const EDGE_MARGIN2 = 60;
10579
- const HALF_MARGIN = EDGE_MARGIN2 / 2;
10580
- const shiftX = -minX + HALF_MARGIN;
10581
- const shiftY = -minY + HALF_MARGIN;
10582
- for (const node of layoutNodes) {
10583
- node.x += shiftX;
10584
- node.y += shiftY;
10585
- }
10586
- for (const edge of layoutEdges) {
10587
- for (const pt of edge.points) {
10588
- pt.x += shiftX;
10589
- pt.y += shiftY;
10590
- }
10591
- }
10592
- maxX += shiftX;
10593
- maxY += shiftY;
10594
- const totalWidth = maxX + HALF_MARGIN;
10595
- const totalHeight = maxY + HALF_MARGIN;
10596
10697
  return {
10597
10698
  nodes: layoutNodes,
10598
10699
  edges: layoutEdges,
10599
- width: totalWidth,
10600
- height: totalHeight
10700
+ width: maxX + HALF_MARGIN,
10701
+ height: maxY + HALF_MARGIN
10601
10702
  };
10602
10703
  }
10603
- var import_dagre3, MIN_WIDTH2, CHAR_WIDTH4, PADDING_X2, HEADER_BASE2, MEMBER_LINE_HEIGHT3, COMPARTMENT_PADDING_Y3, SEPARATOR_HEIGHT2;
10704
+ var import_dagre3, MIN_WIDTH2, CHAR_WIDTH4, PADDING_X2, HEADER_BASE2, MEMBER_LINE_HEIGHT3, COMPARTMENT_PADDING_Y3, SEPARATOR_HEIGHT2, HALF_MARGIN, COMP_GAP;
10604
10705
  var init_layout4 = __esm({
10605
10706
  "src/er/layout.ts"() {
10606
10707
  "use strict";
@@ -10612,6 +10713,135 @@ var init_layout4 = __esm({
10612
10713
  MEMBER_LINE_HEIGHT3 = 18;
10613
10714
  COMPARTMENT_PADDING_Y3 = 8;
10614
10715
  SEPARATOR_HEIGHT2 = 1;
10716
+ HALF_MARGIN = 30;
10717
+ COMP_GAP = 60;
10718
+ }
10719
+ });
10720
+
10721
+ // src/er/classify.ts
10722
+ function classifyEREntities(tables, relationships) {
10723
+ const result = /* @__PURE__ */ new Map();
10724
+ if (tables.length === 0) return result;
10725
+ const indegreeMap = {};
10726
+ for (const t of tables) indegreeMap[t.id] = 0;
10727
+ for (const rel of relationships) {
10728
+ if (rel.source === rel.target) continue;
10729
+ if (rel.cardinality.from === "1" && rel.cardinality.to !== "1") {
10730
+ indegreeMap[rel.source] = (indegreeMap[rel.source] ?? 0) + 1;
10731
+ }
10732
+ if (rel.cardinality.to === "1" && rel.cardinality.from !== "1") {
10733
+ indegreeMap[rel.target] = (indegreeMap[rel.target] ?? 0) + 1;
10734
+ }
10735
+ }
10736
+ const tableStarNeighbors = /* @__PURE__ */ new Map();
10737
+ for (const rel of relationships) {
10738
+ if (rel.source === rel.target) continue;
10739
+ if (rel.cardinality.from === "*") {
10740
+ if (!tableStarNeighbors.has(rel.source)) tableStarNeighbors.set(rel.source, /* @__PURE__ */ new Set());
10741
+ tableStarNeighbors.get(rel.source).add(rel.target);
10742
+ }
10743
+ if (rel.cardinality.to === "*") {
10744
+ if (!tableStarNeighbors.has(rel.target)) tableStarNeighbors.set(rel.target, /* @__PURE__ */ new Set());
10745
+ tableStarNeighbors.get(rel.target).add(rel.source);
10746
+ }
10747
+ }
10748
+ const mmParticipants = /* @__PURE__ */ new Set();
10749
+ for (const [id, neighbors] of tableStarNeighbors) {
10750
+ if (neighbors.size >= 2) mmParticipants.add(id);
10751
+ }
10752
+ const indegreeValues = Object.values(indegreeMap);
10753
+ const mean = indegreeValues.reduce((a, b) => a + b, 0) / indegreeValues.length;
10754
+ const variance = indegreeValues.reduce((a, b) => a + (b - mean) ** 2, 0) / indegreeValues.length;
10755
+ const stddev = Math.sqrt(variance);
10756
+ const sorted = [...indegreeValues].sort((a, b) => a - b);
10757
+ const median = sorted.length % 2 === 0 ? (sorted[sorted.length / 2 - 1] + sorted[sorted.length / 2]) / 2 : sorted[Math.floor(sorted.length / 2)];
10758
+ for (const table of tables) {
10759
+ const id = table.id;
10760
+ const cols = table.columns;
10761
+ const fkCols = cols.filter((c) => c.constraints.includes("fk"));
10762
+ const pkFkCols = cols.filter(
10763
+ (c) => c.constraints.includes("pk") && c.constraints.includes("fk")
10764
+ );
10765
+ const fkCount = fkCols.length;
10766
+ const fkRatio = cols.length === 0 ? 0 : fkCount / cols.length;
10767
+ const indegree = indegreeMap[id] ?? 0;
10768
+ const nameLower = table.name.toLowerCase();
10769
+ const externalRels = relationships.filter(
10770
+ (r) => (r.source === id || r.target === id) && r.source !== r.target
10771
+ );
10772
+ const hasSelfRef = relationships.some((r) => r.source === id && r.target === id);
10773
+ const externalTargets = /* @__PURE__ */ new Set();
10774
+ for (const rel of externalRels) {
10775
+ externalTargets.add(rel.source === id ? rel.target : rel.source);
10776
+ }
10777
+ if (hasSelfRef && externalRels.length === 0) {
10778
+ result.set(id, "self-referential");
10779
+ continue;
10780
+ }
10781
+ const isInheritancePattern = pkFkCols.length >= 2 && externalTargets.size === 1;
10782
+ const junctionByRatio = fkRatio >= 0.6 && !isInheritancePattern;
10783
+ const junctionByCompositePk = pkFkCols.length >= 2 && externalTargets.size >= 2;
10784
+ const junctionByMm = mmParticipants.has(id);
10785
+ if (junctionByRatio || junctionByCompositePk || junctionByMm) {
10786
+ result.set(id, "junction");
10787
+ continue;
10788
+ }
10789
+ if (fkRatio >= 0.4 && fkRatio < 0.6 && pkFkCols.length < 2 && !mmParticipants.has(id)) {
10790
+ result.set(id, "ambiguous");
10791
+ continue;
10792
+ }
10793
+ const nameMatchesLookup = LOOKUP_NAME_SUFFIXES.some((s) => nameLower.endsWith(s));
10794
+ if (nameMatchesLookup && cols.length <= 6 && fkCount <= 1 && indegree > median) {
10795
+ result.set(id, "lookup");
10796
+ continue;
10797
+ }
10798
+ if (tables.length >= 6 && indegree > 0 && indegree > mean + 1.5 * stddev && indegree >= 2 * mean) {
10799
+ result.set(id, "hub");
10800
+ continue;
10801
+ }
10802
+ if (fkCount > 0) {
10803
+ result.set(id, "dependent");
10804
+ continue;
10805
+ }
10806
+ result.set(id, "core");
10807
+ }
10808
+ return result;
10809
+ }
10810
+ var ROLE_COLORS, ROLE_LABELS, ROLE_ORDER, LOOKUP_NAME_SUFFIXES;
10811
+ var init_classify = __esm({
10812
+ "src/er/classify.ts"() {
10813
+ "use strict";
10814
+ ROLE_COLORS = {
10815
+ core: "green",
10816
+ dependent: "blue",
10817
+ junction: "red",
10818
+ ambiguous: "purple",
10819
+ lookup: "yellow",
10820
+ hub: "orange",
10821
+ "self-referential": "teal",
10822
+ unclassified: "gray"
10823
+ };
10824
+ ROLE_LABELS = {
10825
+ core: "Core entity",
10826
+ dependent: "Dependent",
10827
+ junction: "Junction / M:M",
10828
+ ambiguous: "Bridge",
10829
+ lookup: "Lookup / Reference",
10830
+ hub: "Hub",
10831
+ "self-referential": "Self-referential",
10832
+ unclassified: "Unclassified"
10833
+ };
10834
+ ROLE_ORDER = [
10835
+ "core",
10836
+ "dependent",
10837
+ "junction",
10838
+ "ambiguous",
10839
+ "lookup",
10840
+ "hub",
10841
+ "self-referential",
10842
+ "unclassified"
10843
+ ];
10844
+ LOOKUP_NAME_SUFFIXES = ["_type", "_status", "_code", "_category"];
10615
10845
  }
10616
10846
  });
10617
10847
 
@@ -10681,25 +10911,41 @@ function drawCardinality(g, point, prevPoint, cardinality, color, useLabels) {
10681
10911
  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);
10682
10912
  }
10683
10913
  }
10684
- function renderERDiagram(container, parsed, layout, palette, isDark, onClickItem, exportDims, activeTagGroup) {
10914
+ function renderERDiagram(container, parsed, layout, palette, isDark, onClickItem, exportDims, activeTagGroup, semanticColorsActive) {
10685
10915
  d3Selection5.select(container).selectAll(":not([data-d3-tooltip])").remove();
10686
- const width = exportDims?.width ?? container.clientWidth;
10687
- const height = exportDims?.height ?? container.clientHeight;
10688
- if (width <= 0 || height <= 0) return;
10916
+ const useSemanticColors = parsed.tagGroups.length === 0 && layout.nodes.every((n) => !n.color);
10917
+ const legendReserveH = useSemanticColors ? LEGEND_HEIGHT + DIAGRAM_PADDING5 : 0;
10689
10918
  const titleHeight = parsed.title ? 40 : 0;
10690
10919
  const diagramW = layout.width;
10691
10920
  const diagramH = layout.height;
10692
- const availH = height - titleHeight;
10693
- const scaleX = (width - DIAGRAM_PADDING5 * 2) / diagramW;
10694
- const scaleY = (availH - DIAGRAM_PADDING5 * 2) / diagramH;
10695
- const scale = Math.min(MAX_SCALE4, scaleX, scaleY);
10696
- const scaledW = diagramW * scale;
10697
- const scaledH = diagramH * scale;
10698
- const offsetX = (width - scaledW) / 2;
10699
- const offsetY = titleHeight + DIAGRAM_PADDING5;
10700
- const svg = d3Selection5.select(container).append("svg").attr("width", width).attr("height", height).style("font-family", FONT_FAMILY);
10921
+ const naturalW = diagramW + DIAGRAM_PADDING5 * 2;
10922
+ const naturalH = diagramH + titleHeight + legendReserveH + DIAGRAM_PADDING5 * 2;
10923
+ let viewW;
10924
+ let viewH;
10925
+ let scale;
10926
+ let offsetX;
10927
+ let offsetY;
10928
+ if (exportDims) {
10929
+ viewW = exportDims.width ?? naturalW;
10930
+ viewH = exportDims.height ?? naturalH;
10931
+ const availH = viewH - titleHeight - legendReserveH;
10932
+ const scaleX = (viewW - DIAGRAM_PADDING5 * 2) / diagramW;
10933
+ const scaleY = (availH - DIAGRAM_PADDING5 * 2) / diagramH;
10934
+ scale = Math.min(MAX_SCALE4, scaleX, scaleY);
10935
+ const scaledW = diagramW * scale;
10936
+ offsetX = (viewW - scaledW) / 2;
10937
+ offsetY = titleHeight + DIAGRAM_PADDING5;
10938
+ } else {
10939
+ viewW = naturalW;
10940
+ viewH = naturalH;
10941
+ scale = 1;
10942
+ offsetX = DIAGRAM_PADDING5;
10943
+ offsetY = titleHeight + DIAGRAM_PADDING5;
10944
+ }
10945
+ if (viewW <= 0 || viewH <= 0) return;
10946
+ 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);
10701
10947
  if (parsed.title) {
10702
- 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);
10948
+ 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);
10703
10949
  if (parsed.titleLineNumber) {
10704
10950
  titleEl.attr("data-line-number", parsed.titleLineNumber);
10705
10951
  if (onClickItem) {
@@ -10713,6 +10959,8 @@ function renderERDiagram(container, parsed, layout, palette, isDark, onClickItem
10713
10959
  }
10714
10960
  const contentG = svg.append("g").attr("transform", `translate(${offsetX}, ${offsetY}) scale(${scale})`);
10715
10961
  const seriesColors2 = getSeriesColors(palette);
10962
+ const semanticRoles = useSemanticColors ? classifyEREntities(parsed.tables, parsed.relationships) : null;
10963
+ const semanticActive = semanticRoles !== null && (semanticColorsActive ?? true);
10716
10964
  const useLabels = parsed.options.notation === "labels";
10717
10965
  for (const edge of layout.edges) {
10718
10966
  if (edge.points.length < 2) continue;
@@ -10752,7 +11000,8 @@ function renderERDiagram(container, parsed, layout, palette, isDark, onClickItem
10752
11000
  for (let ni = 0; ni < layout.nodes.length; ni++) {
10753
11001
  const node = layout.nodes[ni];
10754
11002
  const tagColor = resolveTagColor(node.metadata, parsed.tagGroups, activeTagGroup ?? null);
10755
- const nodeColor2 = node.color ?? tagColor ?? seriesColors2[ni % seriesColors2.length];
11003
+ const semanticColor = semanticActive ? palette.colors[ROLE_COLORS[semanticRoles.get(node.id) ?? "unclassified"]] : semanticRoles ? palette.primary : void 0;
11004
+ const nodeColor2 = node.color ?? tagColor ?? semanticColor ?? seriesColors2[ni % seriesColors2.length];
10756
11005
  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);
10757
11006
  if (activeTagGroup) {
10758
11007
  const tagKey = activeTagGroup.toLowerCase();
@@ -10761,6 +11010,10 @@ function renderERDiagram(container, parsed, layout, palette, isDark, onClickItem
10761
11010
  nodeG.attr(`data-tag-${tagKey}`, tagValue.toLowerCase());
10762
11011
  }
10763
11012
  }
11013
+ if (semanticRoles) {
11014
+ const role = semanticRoles.get(node.id);
11015
+ if (role) nodeG.attr("data-er-role", role);
11016
+ }
10764
11017
  if (onClickItem) {
10765
11018
  nodeG.style("cursor", "pointer").on("click", () => {
10766
11019
  onClickItem(node.lineNumber);
@@ -10801,7 +11054,7 @@ function renderERDiagram(container, parsed, layout, palette, isDark, onClickItem
10801
11054
  legendG.attr("data-legend-active", activeTagGroup.toLowerCase());
10802
11055
  }
10803
11056
  let legendX = DIAGRAM_PADDING5;
10804
- let legendY = height - DIAGRAM_PADDING5;
11057
+ let legendY = viewH - DIAGRAM_PADDING5;
10805
11058
  for (const group of parsed.tagGroups) {
10806
11059
  const groupG = legendG.append("g").attr("data-legend-group", group.name.toLowerCase());
10807
11060
  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}:`);
@@ -10820,6 +11073,62 @@ function renderERDiagram(container, parsed, layout, palette, isDark, onClickItem
10820
11073
  legendX += LEGEND_GROUP_GAP;
10821
11074
  }
10822
11075
  }
11076
+ if (semanticRoles) {
11077
+ const presentRoles = ROLE_ORDER.filter((role) => {
11078
+ for (const r of semanticRoles.values()) {
11079
+ if (r === role) return true;
11080
+ }
11081
+ return false;
11082
+ });
11083
+ if (presentRoles.length > 0) {
11084
+ const measureLabelW = (text, fontSize) => {
11085
+ const dummy = svg.append("text").attr("font-size", fontSize).attr("font-family", FONT_FAMILY).attr("visibility", "hidden").text(text);
11086
+ const measured = dummy.node()?.getComputedTextLength?.() ?? 0;
11087
+ dummy.remove();
11088
+ return measured > 0 ? measured : text.length * fontSize * 0.6;
11089
+ };
11090
+ const labelWidths = /* @__PURE__ */ new Map();
11091
+ for (const role of presentRoles) {
11092
+ labelWidths.set(role, measureLabelW(ROLE_LABELS[role], LEGEND_ENTRY_FONT_SIZE));
11093
+ }
11094
+ const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
11095
+ const groupName = "Role";
11096
+ const pillWidth = groupName.length * LEGEND_PILL_FONT_W + LEGEND_PILL_PAD;
11097
+ const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
11098
+ let totalWidth;
11099
+ let entriesWidth = 0;
11100
+ if (semanticActive) {
11101
+ for (const role of presentRoles) {
11102
+ entriesWidth += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + labelWidths.get(role) + LEGEND_ENTRY_TRAIL;
11103
+ }
11104
+ totalWidth = LEGEND_CAPSULE_PAD * 2 + pillWidth + LEGEND_ENTRY_TRAIL + entriesWidth;
11105
+ } else {
11106
+ totalWidth = pillWidth;
11107
+ }
11108
+ const legendX = (viewW - totalWidth) / 2;
11109
+ const legendY = viewH - DIAGRAM_PADDING5 - LEGEND_HEIGHT;
11110
+ const semanticLegendG = svg.append("g").attr("class", "er-semantic-legend").attr("data-legend-group", "role").attr("transform", `translate(${legendX}, ${legendY})`).style("cursor", "pointer");
11111
+ if (semanticActive) {
11112
+ semanticLegendG.append("rect").attr("width", totalWidth).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
11113
+ 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);
11114
+ 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);
11115
+ 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);
11116
+ let entryX = LEGEND_CAPSULE_PAD + pillWidth + LEGEND_ENTRY_TRAIL;
11117
+ for (const role of presentRoles) {
11118
+ const label = ROLE_LABELS[role];
11119
+ const roleColor = palette.colors[ROLE_COLORS[role]];
11120
+ const entryG = semanticLegendG.append("g").attr("data-legend-entry", role);
11121
+ entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R).attr("cy", LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", roleColor);
11122
+ const textX = entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
11123
+ 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);
11124
+ entryX = textX + labelWidths.get(role) + LEGEND_ENTRY_TRAIL;
11125
+ }
11126
+ } else {
11127
+ semanticLegendG.append("rect").attr("width", pillWidth).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
11128
+ 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);
11129
+ }
11130
+ }
11131
+ }
10823
11132
  }
10824
11133
  function renderERDiagramForExport(content, theme, palette) {
10825
11134
  const parsed = parseERDiagram(content, palette);
@@ -10869,6 +11178,7 @@ var init_renderer5 = __esm({
10869
11178
  init_legend_constants();
10870
11179
  init_parser3();
10871
11180
  init_layout4();
11181
+ init_classify();
10872
11182
  DIAGRAM_PADDING5 = 20;
10873
11183
  MAX_SCALE4 = 3;
10874
11184
  TABLE_FONT_SIZE = 13;
@@ -10996,9 +11306,10 @@ function layoutInitiativeStatus(parsed, collapseResult) {
10996
11306
  const dagreEdge = g.edge(edge.source, edge.target, `e${i}`);
10997
11307
  const dagrePoints = dagreEdge?.points ?? [];
10998
11308
  const hasIntermediateRank = allNodeX.some((x) => x > src.x + 20 && x < tgt.x - 20);
10999
- const step = Math.min((enterX - exitX) * 0.15, 20);
11309
+ const step = Math.max(0, Math.min((enterX - exitX) * 0.15, 20));
11000
11310
  const isBackEdge = tgt.x < src.x - 5;
11001
- const isYDisplaced = !isBackEdge && Math.abs(tgt.y - src.y) > NODESEP;
11311
+ const isTopExit = !isBackEdge && tgt.x > src.x && !hasIntermediateRank && tgt.y < src.y - NODESEP;
11312
+ const isBottomExit = !isBackEdge && tgt.x > src.x && !hasIntermediateRank && tgt.y > src.y + NODESEP;
11002
11313
  let points;
11003
11314
  if (isBackEdge) {
11004
11315
  const routeAbove = Math.min(src.y, tgt.y) > avgNodeY;
@@ -11008,31 +11319,44 @@ function layoutInitiativeStatus(parsed, collapseResult) {
11008
11319
  const spreadDir = avgNodeX < rawMidX ? 1 : -1;
11009
11320
  const unclamped = Math.abs(src.x - tgt.x) < NODE_WIDTH ? rawMidX + spreadDir * BACK_EDGE_MIN_SPREAD : rawMidX;
11010
11321
  const midX = Math.min(src.x, Math.max(tgt.x, unclamped));
11322
+ const srcDepart = Math.max(midX + 1, src.x - TOP_EXIT_STEP);
11323
+ const tgtApproach = Math.min(midX - 1, tgt.x + TOP_EXIT_STEP);
11011
11324
  if (routeAbove) {
11012
11325
  const arcY = Math.min(src.y - srcHalfH, tgt.y - tgtHalfH) - BACK_EDGE_MARGIN;
11013
11326
  points = [
11014
11327
  { x: src.x, y: src.y - srcHalfH },
11328
+ { x: srcDepart, y: src.y - srcHalfH - TOP_EXIT_STEP },
11015
11329
  { x: midX, y: arcY },
11330
+ { x: tgtApproach, y: tgt.y - tgtHalfH - TOP_EXIT_STEP },
11016
11331
  { x: tgt.x, y: tgt.y - tgtHalfH }
11017
11332
  ];
11018
11333
  } else {
11019
11334
  const arcY = Math.max(src.y + srcHalfH, tgt.y + tgtHalfH) + BACK_EDGE_MARGIN;
11020
11335
  points = [
11021
11336
  { x: src.x, y: src.y + srcHalfH },
11337
+ { x: srcDepart, y: src.y + srcHalfH + TOP_EXIT_STEP },
11022
11338
  { x: midX, y: arcY },
11339
+ { x: tgtApproach, y: tgt.y + tgtHalfH + TOP_EXIT_STEP },
11023
11340
  { x: tgt.x, y: tgt.y + tgtHalfH }
11024
11341
  ];
11025
11342
  }
11026
- } else if (isYDisplaced) {
11027
- const exitY = tgt.y > src.y + NODESEP ? src.y + src.height / 2 : src.y - src.height / 2;
11028
- const spreadExitX = src.x + yOffset;
11029
- const spreadEntryY = tgt.y + yOffset;
11030
- const midX = (spreadExitX + enterX) / 2;
11031
- const midY = (exitY + spreadEntryY) / 2;
11343
+ } else if (isTopExit) {
11344
+ const exitY = src.y - src.height / 2;
11345
+ const p1x = Math.min(Math.max(src.x, src.x + yOffset + TOP_EXIT_STEP), (src.x + enterX) / 2 - 1);
11032
11346
  points = [
11033
- { x: spreadExitX, y: exitY },
11034
- { x: midX, y: midY },
11035
- { x: enterX, y: spreadEntryY }
11347
+ { x: src.x, y: exitY },
11348
+ { x: p1x, y: exitY - TOP_EXIT_STEP },
11349
+ { x: enterX - step, y: tgt.y + yOffset },
11350
+ { x: enterX, y: tgt.y }
11351
+ ];
11352
+ } else if (isBottomExit) {
11353
+ const exitY = src.y + src.height / 2;
11354
+ const p1x = Math.min(Math.max(src.x, src.x + yOffset + TOP_EXIT_STEP), (src.x + enterX) / 2 - 1);
11355
+ points = [
11356
+ { x: src.x, y: exitY },
11357
+ { x: p1x, y: exitY + TOP_EXIT_STEP },
11358
+ { x: enterX - step, y: tgt.y + yOffset },
11359
+ { x: enterX, y: tgt.y }
11036
11360
  ];
11037
11361
  } else if (tgt.x > src.x && !hasIntermediateRank) {
11038
11362
  points = [
@@ -11129,7 +11453,7 @@ function layoutInitiativeStatus(parsed, collapseResult) {
11129
11453
  totalHeight += 40;
11130
11454
  return { nodes: layoutNodes, edges: layoutEdges, groups: layoutGroups, width: totalWidth, height: totalHeight };
11131
11455
  }
11132
- var import_dagre4, 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;
11456
+ var import_dagre4, 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;
11133
11457
  var init_layout5 = __esm({
11134
11458
  "src/initiative-status/layout.ts"() {
11135
11459
  "use strict";
@@ -11146,6 +11470,7 @@ var init_layout5 = __esm({
11146
11470
  MAX_PARALLEL_EDGES = 5;
11147
11471
  BACK_EDGE_MARGIN = 40;
11148
11472
  BACK_EDGE_MIN_SPREAD = Math.round(NODE_WIDTH * 0.75);
11473
+ TOP_EXIT_STEP = 10;
11149
11474
  CHAR_WIDTH_RATIO = 0.6;
11150
11475
  NODE_FONT_SIZE = 13;
11151
11476
  NODE_TEXT_PADDING = 12;
@@ -11644,7 +11969,7 @@ __export(layout_exports6, {
11644
11969
  layoutC4Deployment: () => layoutC4Deployment,
11645
11970
  rollUpContextRelationships: () => rollUpContextRelationships
11646
11971
  });
11647
- function computeEdgePenalty(edgeList, nodePositions, degrees) {
11972
+ function computeEdgePenalty(edgeList, nodePositions, degrees, nodeGeometry) {
11648
11973
  let penalty = 0;
11649
11974
  for (const edge of edgeList) {
11650
11975
  const sx = nodePositions.get(edge.source);
@@ -11654,6 +11979,32 @@ function computeEdgePenalty(edgeList, nodePositions, degrees) {
11654
11979
  const weight = Math.min(degrees.get(edge.source) ?? 1, degrees.get(edge.target) ?? 1);
11655
11980
  penalty += dist * weight;
11656
11981
  }
11982
+ if (nodeGeometry) {
11983
+ for (const edge of edgeList) {
11984
+ const geomA = nodeGeometry.get(edge.source);
11985
+ const geomB = nodeGeometry.get(edge.target);
11986
+ if (!geomA || !geomB) continue;
11987
+ const ax = nodePositions.get(edge.source) ?? 0;
11988
+ const bx = nodePositions.get(edge.target) ?? 0;
11989
+ const ay = geomA.y;
11990
+ const by = geomB.y;
11991
+ if (ay === by) continue;
11992
+ const edgeMinX = Math.min(ax, bx);
11993
+ const edgeMaxX = Math.max(ax, bx);
11994
+ const edgeMinY = Math.min(ay, by);
11995
+ const edgeMaxY = Math.max(ay, by);
11996
+ for (const [name, geomC] of nodeGeometry) {
11997
+ if (name === edge.source || name === edge.target) continue;
11998
+ const cx = nodePositions.get(name) ?? 0;
11999
+ const cy = geomC.y;
12000
+ const hw = geomC.width / 2;
12001
+ const hh = geomC.height / 2;
12002
+ if (cx + hw > edgeMinX && cx - hw < edgeMaxX && cy + hh > edgeMinY && cy - hh < edgeMaxY) {
12003
+ penalty += EDGE_NODE_COLLISION_WEIGHT;
12004
+ }
12005
+ }
12006
+ }
12007
+ }
11657
12008
  return penalty;
11658
12009
  }
11659
12010
  function reduceCrossings(g, edgeList, nodeGroupMap) {
@@ -11663,6 +12014,11 @@ function reduceCrossings(g, edgeList, nodeGroupMap) {
11663
12014
  degrees.set(edge.source, (degrees.get(edge.source) ?? 0) + 1);
11664
12015
  degrees.set(edge.target, (degrees.get(edge.target) ?? 0) + 1);
11665
12016
  }
12017
+ const nodeGeometry = /* @__PURE__ */ new Map();
12018
+ for (const name of g.nodes()) {
12019
+ const pos = g.node(name);
12020
+ if (pos) nodeGeometry.set(name, { y: pos.y, width: pos.width, height: pos.height });
12021
+ }
11666
12022
  const rankMap = /* @__PURE__ */ new Map();
11667
12023
  for (const name of g.nodes()) {
11668
12024
  const pos = g.node(name);
@@ -11705,7 +12061,7 @@ function reduceCrossings(g, edgeList, nodeGroupMap) {
11705
12061
  const pos = g.node(name);
11706
12062
  if (pos) basePositions.set(name, pos.x);
11707
12063
  }
11708
- const currentPenalty = computeEdgePenalty(edgeList, basePositions, degrees);
12064
+ const currentPenalty = computeEdgePenalty(edgeList, basePositions, degrees, nodeGeometry);
11709
12065
  let bestPerm = [...partition];
11710
12066
  let bestPenalty = currentPenalty;
11711
12067
  if (partition.length <= 8) {
@@ -11715,7 +12071,7 @@ function reduceCrossings(g, edgeList, nodeGroupMap) {
11715
12071
  for (let i = 0; i < perm.length; i++) {
11716
12072
  testPositions.set(perm[i], xSlots[i]);
11717
12073
  }
11718
- const penalty = computeEdgePenalty(edgeList, testPositions, degrees);
12074
+ const penalty = computeEdgePenalty(edgeList, testPositions, degrees, nodeGeometry);
11719
12075
  if (penalty < bestPenalty) {
11720
12076
  bestPenalty = penalty;
11721
12077
  bestPerm = [...perm];
@@ -11733,13 +12089,13 @@ function reduceCrossings(g, edgeList, nodeGroupMap) {
11733
12089
  for (let k = 0; k < workingOrder.length; k++) {
11734
12090
  testPositions.set(workingOrder[k], xSlots[k]);
11735
12091
  }
11736
- const before = computeEdgePenalty(edgeList, testPositions, degrees);
12092
+ const before = computeEdgePenalty(edgeList, testPositions, degrees, nodeGeometry);
11737
12093
  [workingOrder[i], workingOrder[i + 1]] = [workingOrder[i + 1], workingOrder[i]];
11738
12094
  const testPositions2 = new Map(basePositions);
11739
12095
  for (let k = 0; k < workingOrder.length; k++) {
11740
12096
  testPositions2.set(workingOrder[k], xSlots[k]);
11741
12097
  }
11742
- const after = computeEdgePenalty(edgeList, testPositions2, degrees);
12098
+ const after = computeEdgePenalty(edgeList, testPositions2, degrees, nodeGeometry);
11743
12099
  if (after < before) {
11744
12100
  improved = true;
11745
12101
  if (after < bestPenalty) {
@@ -11978,7 +12334,7 @@ function computeLegendGroups3(tagGroups) {
11978
12334
  const nameW = group.name.length * LEGEND_PILL_FONT_W4 + LEGEND_PILL_PAD4 * 2;
11979
12335
  let capsuleW = LEGEND_CAPSULE_PAD4;
11980
12336
  for (const e of entries) {
11981
- capsuleW += LEGEND_DOT_R4 * 2 + LEGEND_ENTRY_DOT_GAP4 + e.value.length * LEGEND_ENTRY_FONT_W4 + LEGEND_ENTRY_TRAIL4;
12337
+ capsuleW += LEGEND_DOT_R4 * 2 + LEGEND_ENTRY_DOT_GAP4 + e.value.length * LEGEND_ENTRY_FONT_W5 + LEGEND_ENTRY_TRAIL4;
11982
12338
  }
11983
12339
  capsuleW += LEGEND_CAPSULE_PAD4;
11984
12340
  result.push({
@@ -13059,7 +13415,7 @@ function layoutC4Deployment(parsed, activeTagGroup) {
13059
13415
  }
13060
13416
  return { nodes, edges, legend: legendGroups, groupBoundaries, width: totalWidth, height: totalHeight };
13061
13417
  }
13062
- var import_dagre5, 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;
13418
+ var import_dagre5, 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;
13063
13419
  var init_layout6 = __esm({
13064
13420
  "src/c4/layout.ts"() {
13065
13421
  "use strict";
@@ -13085,10 +13441,11 @@ var init_layout6 = __esm({
13085
13441
  LEGEND_PILL_PAD4 = 16;
13086
13442
  LEGEND_DOT_R4 = 4;
13087
13443
  LEGEND_ENTRY_FONT_SIZE2 = 10;
13088
- LEGEND_ENTRY_FONT_W4 = LEGEND_ENTRY_FONT_SIZE2 * 0.6;
13444
+ LEGEND_ENTRY_FONT_W5 = LEGEND_ENTRY_FONT_SIZE2 * 0.6;
13089
13445
  LEGEND_ENTRY_DOT_GAP4 = 4;
13090
13446
  LEGEND_ENTRY_TRAIL4 = 8;
13091
13447
  LEGEND_CAPSULE_PAD4 = 4;
13448
+ EDGE_NODE_COLLISION_WEIGHT = 5e3;
13092
13449
  META_EXCLUDE_KEYS = /* @__PURE__ */ new Set(["description", "tech", "technology", "is a"]);
13093
13450
  }
13094
13451
  });
@@ -15273,6 +15630,7 @@ var init_compute = __esm({
15273
15630
  // src/infra/layout.ts
15274
15631
  var layout_exports8 = {};
15275
15632
  __export(layout_exports8, {
15633
+ fixEdgeWaypoints: () => fixEdgeWaypoints,
15276
15634
  layoutInfra: () => layoutInfra,
15277
15635
  separateGroups: () => separateGroups
15278
15636
  });
@@ -15456,6 +15814,8 @@ function formatUptime(fraction) {
15456
15814
  return `${pct.toFixed(1)}%`;
15457
15815
  }
15458
15816
  function separateGroups(groups, nodes, isLR, maxIterations = 20) {
15817
+ const groupDeltas = /* @__PURE__ */ new Map();
15818
+ let converged = false;
15459
15819
  for (let iter = 0; iter < maxIterations; iter++) {
15460
15820
  let anyOverlap = false;
15461
15821
  for (let i = 0; i < groups.length; i++) {
@@ -15473,6 +15833,9 @@ function separateGroups(groups, nodes, isLR, maxIterations = 20) {
15473
15833
  const groupToShift = aCenter <= bCenter ? gb : ga;
15474
15834
  if (isLR) groupToShift.y += shift;
15475
15835
  else groupToShift.x += shift;
15836
+ const prev = groupDeltas.get(groupToShift.id) ?? { dx: 0, dy: 0 };
15837
+ if (isLR) groupDeltas.set(groupToShift.id, { dx: prev.dx, dy: prev.dy + shift });
15838
+ else groupDeltas.set(groupToShift.id, { dx: prev.dx + shift, dy: prev.dy });
15476
15839
  for (const node of nodes) {
15477
15840
  if (node.groupId === groupToShift.id) {
15478
15841
  if (isLR) node.y += shift;
@@ -15481,19 +15844,48 @@ function separateGroups(groups, nodes, isLR, maxIterations = 20) {
15481
15844
  }
15482
15845
  }
15483
15846
  }
15484
- if (!anyOverlap) break;
15847
+ if (!anyOverlap) {
15848
+ converged = true;
15849
+ break;
15850
+ }
15851
+ }
15852
+ if (!converged && maxIterations > 0) {
15853
+ console.warn(`separateGroups: hit maxIterations (${maxIterations}) without fully resolving all group overlaps`);
15854
+ }
15855
+ return groupDeltas;
15856
+ }
15857
+ function fixEdgeWaypoints(edges, nodes, groupDeltas) {
15858
+ if (groupDeltas.size === 0) return;
15859
+ const nodeToGroup = /* @__PURE__ */ new Map();
15860
+ for (const node of nodes) nodeToGroup.set(node.id, node.groupId);
15861
+ for (const edge of edges) {
15862
+ const srcGroup = nodeToGroup.get(edge.sourceId) ?? null;
15863
+ const tgtGroup = nodeToGroup.get(edge.targetId) ?? null;
15864
+ const srcDelta = srcGroup ? groupDeltas.get(srcGroup) : void 0;
15865
+ const tgtDelta = tgtGroup ? groupDeltas.get(tgtGroup) : void 0;
15866
+ if (!srcDelta && !tgtDelta) continue;
15867
+ if (srcDelta && tgtDelta && srcGroup !== tgtGroup) {
15868
+ edge.points = [];
15869
+ continue;
15870
+ }
15871
+ const delta = srcDelta ?? tgtDelta;
15872
+ for (const pt of edge.points) {
15873
+ pt.x += delta.dx;
15874
+ pt.y += delta.dy;
15875
+ }
15485
15876
  }
15486
15877
  }
15487
15878
  function layoutInfra(computed, expandedNodeIds, collapsedNodes) {
15488
15879
  if (computed.nodes.length === 0) {
15489
- return { nodes: [], edges: [], groups: [], options: {}, width: 0, height: 0 };
15880
+ return { nodes: [], edges: [], groups: [], options: {}, direction: computed.direction, width: 0, height: 0 };
15490
15881
  }
15882
+ const isLR = computed.direction !== "TB";
15491
15883
  const g = new import_dagre7.default.graphlib.Graph();
15492
15884
  g.setGraph({
15493
15885
  rankdir: computed.direction === "TB" ? "TB" : "LR",
15494
- nodesep: 50,
15495
- ranksep: 100,
15496
- edgesep: 20
15886
+ nodesep: isLR ? 70 : 60,
15887
+ ranksep: isLR ? 150 : 120,
15888
+ edgesep: 30
15497
15889
  });
15498
15890
  g.setDefaultEdgeLabel(() => ({}));
15499
15891
  const groupedNodeIds = /* @__PURE__ */ new Set();
@@ -15501,7 +15893,6 @@ function layoutInfra(computed, expandedNodeIds, collapsedNodes) {
15501
15893
  if (node.groupId) groupedNodeIds.add(node.id);
15502
15894
  }
15503
15895
  const GROUP_INFLATE = GROUP_PADDING3 * 2 + GROUP_HEADER_HEIGHT;
15504
- const isLR = computed.direction !== "TB";
15505
15896
  const widthMap = /* @__PURE__ */ new Map();
15506
15897
  const heightMap = /* @__PURE__ */ new Map();
15507
15898
  for (const node of computed.nodes) {
@@ -15636,7 +16027,8 @@ function layoutInfra(computed, expandedNodeIds, collapsedNodes) {
15636
16027
  lineNumber: group.lineNumber
15637
16028
  };
15638
16029
  });
15639
- separateGroups(layoutGroups, layoutNodes, isLR);
16030
+ const groupDeltas = separateGroups(layoutGroups, layoutNodes, isLR);
16031
+ fixEdgeWaypoints(layoutEdges, layoutNodes, groupDeltas);
15640
16032
  let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
15641
16033
  for (const node of layoutNodes) {
15642
16034
  const left = node.x - node.width / 2;
@@ -15694,6 +16086,7 @@ function layoutInfra(computed, expandedNodeIds, collapsedNodes) {
15694
16086
  edges: layoutEdges,
15695
16087
  groups: layoutGroups,
15696
16088
  options: computed.options,
16089
+ direction: computed.direction,
15697
16090
  width: totalWidth,
15698
16091
  height: totalHeight
15699
16092
  };
@@ -15736,23 +16129,23 @@ var init_layout8 = __esm({
15736
16129
  ]);
15737
16130
  DISPLAY_NAMES = {
15738
16131
  "cache-hit": "cache hit",
15739
- "firewall-block": "fw block",
16132
+ "firewall-block": "firewall block",
15740
16133
  "ratelimit-rps": "rate limit RPS",
15741
16134
  "latency-ms": "latency",
15742
16135
  "uptime": "uptime",
15743
16136
  "instances": "instances",
15744
16137
  "max-rps": "max RPS",
15745
- "cb-error-threshold": "CB error",
15746
- "cb-latency-threshold-ms": "CB latency",
16138
+ "cb-error-threshold": "CB error threshold",
16139
+ "cb-latency-threshold-ms": "CB latency threshold",
15747
16140
  "concurrency": "concurrency",
15748
16141
  "duration-ms": "duration",
15749
16142
  "cold-start-ms": "cold start",
15750
16143
  "buffer": "buffer",
15751
- "drain-rate": "drain",
16144
+ "drain-rate": "drain rate",
15752
16145
  "retention-hours": "retention",
15753
16146
  "partitions": "partitions"
15754
16147
  };
15755
- GROUP_GAP = 24;
16148
+ GROUP_GAP = GROUP_PADDING3 * 2 + GROUP_HEADER_HEIGHT;
15756
16149
  }
15757
16150
  });
15758
16151
 
@@ -15825,6 +16218,236 @@ function resolveNodeSlo(node, diagramOptions) {
15825
16218
  if (availThreshold == null && latencyP90 == null) return null;
15826
16219
  return { availThreshold, latencyP90, warningMargin };
15827
16220
  }
16221
+ function buildPathD(pts, direction) {
16222
+ const gen = d3Shape7.line().x((d) => d.x).y((d) => d.y);
16223
+ if (pts.length <= 2) {
16224
+ gen.curve(direction === "TB" ? d3Shape7.curveBumpY : d3Shape7.curveBumpX);
16225
+ } else {
16226
+ gen.curve(d3Shape7.curveCatmullRom.alpha(0.5));
16227
+ }
16228
+ return gen(pts) ?? "";
16229
+ }
16230
+ function computePortPts(edges, nodeMap, direction) {
16231
+ const srcPts = /* @__PURE__ */ new Map();
16232
+ const tgtPts = /* @__PURE__ */ new Map();
16233
+ const PAD = 0.1;
16234
+ const activeEdges = edges.filter((e) => e.points.length > 0);
16235
+ const bySource = /* @__PURE__ */ new Map();
16236
+ for (const e of activeEdges) {
16237
+ if (!bySource.has(e.sourceId)) bySource.set(e.sourceId, []);
16238
+ bySource.get(e.sourceId).push(e);
16239
+ }
16240
+ for (const [sourceId, es] of bySource) {
16241
+ if (es.length < 2) continue;
16242
+ const source = nodeMap.get(sourceId);
16243
+ if (!source) continue;
16244
+ 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);
16245
+ const n = sorted.length;
16246
+ for (let i = 0; i < n; i++) {
16247
+ const frac = n === 1 ? 0.5 : PAD + (1 - 2 * PAD) * i / (n - 1);
16248
+ const { e, t } = sorted[i];
16249
+ const isBackward = direction === "LR" ? t.x < source.x : t.y < source.y;
16250
+ if (direction === "LR") {
16251
+ srcPts.set(`${e.sourceId}:${e.targetId}`, {
16252
+ x: isBackward ? source.x - source.width / 2 : source.x + source.width / 2,
16253
+ y: source.y - source.height / 2 + frac * source.height
16254
+ });
16255
+ } else {
16256
+ srcPts.set(`${e.sourceId}:${e.targetId}`, {
16257
+ x: source.x - source.width / 2 + frac * source.width,
16258
+ y: isBackward ? source.y - source.height / 2 : source.y + source.height / 2
16259
+ });
16260
+ }
16261
+ }
16262
+ }
16263
+ const byTarget = /* @__PURE__ */ new Map();
16264
+ for (const e of activeEdges) {
16265
+ if (!byTarget.has(e.targetId)) byTarget.set(e.targetId, []);
16266
+ byTarget.get(e.targetId).push(e);
16267
+ }
16268
+ for (const [targetId, es] of byTarget) {
16269
+ if (es.length < 2) continue;
16270
+ const target = nodeMap.get(targetId);
16271
+ if (!target) continue;
16272
+ 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);
16273
+ const n = sorted.length;
16274
+ for (let i = 0; i < n; i++) {
16275
+ const frac = n === 1 ? 0.5 : PAD + (1 - 2 * PAD) * i / (n - 1);
16276
+ const { e, s } = sorted[i];
16277
+ const isBackward = direction === "LR" ? target.x < s.x : target.y < s.y;
16278
+ if (direction === "LR") {
16279
+ tgtPts.set(`${e.sourceId}:${e.targetId}`, {
16280
+ x: isBackward ? target.x + target.width / 2 : target.x - target.width / 2,
16281
+ y: target.y - target.height / 2 + frac * target.height
16282
+ });
16283
+ } else {
16284
+ tgtPts.set(`${e.sourceId}:${e.targetId}`, {
16285
+ x: target.x - target.width / 2 + frac * target.width,
16286
+ y: isBackward ? target.y + target.height / 2 : target.y - target.height / 2
16287
+ });
16288
+ }
16289
+ }
16290
+ }
16291
+ return { srcPts, tgtPts };
16292
+ }
16293
+ function findRoutingLane(blocking, targetY, margin) {
16294
+ const MERGE_SLOP = 4;
16295
+ const sorted = [...blocking].sort((a, b) => a.y + a.height / 2 - (b.y + b.height / 2));
16296
+ const merged = [];
16297
+ for (const r of sorted) {
16298
+ const lo = r.y - MERGE_SLOP;
16299
+ const hi = r.y + r.height + MERGE_SLOP;
16300
+ if (merged.length && lo <= merged[merged.length - 1][1]) {
16301
+ merged[merged.length - 1][1] = Math.max(merged[merged.length - 1][1], hi);
16302
+ } else {
16303
+ merged.push([lo, hi]);
16304
+ }
16305
+ }
16306
+ if (merged.length === 0) return targetY;
16307
+ const MIN_GAP = 10;
16308
+ const candidates = [
16309
+ merged[0][0] - margin,
16310
+ // above all blocking rects
16311
+ merged[merged.length - 1][1] + margin
16312
+ // below all blocking rects
16313
+ ];
16314
+ for (let i = 0; i < merged.length - 1; i++) {
16315
+ const gapLo = merged[i][1];
16316
+ const gapHi = merged[i + 1][0];
16317
+ if (gapHi - gapLo >= MIN_GAP) {
16318
+ candidates.push((gapLo + gapHi) / 2);
16319
+ }
16320
+ }
16321
+ return candidates.reduce(
16322
+ (best, c) => Math.abs(c - targetY) < Math.abs(best - targetY) ? c : best,
16323
+ candidates[0]
16324
+ );
16325
+ }
16326
+ function segmentIntersectsRect(p1, p2, rect) {
16327
+ const { x: rx, y: ry, width: rw, height: rh } = rect;
16328
+ const rr = rx + rw;
16329
+ const rb = ry + rh;
16330
+ const inRect = (p) => p.x >= rx && p.x <= rr && p.y >= ry && p.y <= rb;
16331
+ if (inRect(p1) || inRect(p2)) return true;
16332
+ if (Math.max(p1.x, p2.x) < rx || Math.min(p1.x, p2.x) > rr) return false;
16333
+ if (Math.max(p1.y, p2.y) < ry || Math.min(p1.y, p2.y) > rb) return false;
16334
+ const cross = (o, a, b) => (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
16335
+ const crosses = (a, b) => {
16336
+ const d1 = cross(a, b, p1);
16337
+ const d2 = cross(a, b, p2);
16338
+ const d3 = cross(p1, p2, a);
16339
+ const d4 = cross(p1, p2, b);
16340
+ return (d1 > 0 && d2 < 0 || d1 < 0 && d2 > 0) && (d3 > 0 && d4 < 0 || d3 < 0 && d4 > 0);
16341
+ };
16342
+ const tl = { x: rx, y: ry };
16343
+ const tr = { x: rr, y: ry };
16344
+ const br = { x: rr, y: rb };
16345
+ const bl = { x: rx, y: rb };
16346
+ return crosses(tl, tr) || crosses(tr, br) || crosses(br, bl) || crosses(bl, tl);
16347
+ }
16348
+ function curveIntersectsRect(sc, tc, rect, direction) {
16349
+ if (direction === "LR") {
16350
+ const midX = (sc.x + tc.x) / 2;
16351
+ const m1 = { x: midX, y: sc.y };
16352
+ const m2 = { x: midX, y: tc.y };
16353
+ return segmentIntersectsRect(sc, m1, rect) || segmentIntersectsRect(m1, m2, rect) || segmentIntersectsRect(m2, tc, rect);
16354
+ } else {
16355
+ const midY = (sc.y + tc.y) / 2;
16356
+ const m1 = { x: sc.x, y: midY };
16357
+ const m2 = { x: tc.x, y: midY };
16358
+ return segmentIntersectsRect(sc, m1, rect) || segmentIntersectsRect(m1, m2, rect) || segmentIntersectsRect(m2, tc, rect);
16359
+ }
16360
+ }
16361
+ function edgeWaypoints(source, target, groups, nodes, direction, margin = 30, srcExitPt, tgtEnterPt) {
16362
+ const sc = { x: source.x, y: source.y };
16363
+ const tc = { x: target.x, y: target.y };
16364
+ const isBackward = direction === "LR" ? tc.x < sc.x : tc.y < sc.y;
16365
+ if (isBackward) {
16366
+ if (direction === "LR") {
16367
+ const xBandObs = [];
16368
+ for (const g of groups) {
16369
+ if (g.x + g.width < tc.x - margin || g.x > sc.x + margin) continue;
16370
+ xBandObs.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 nLeft = n.x - n.width / 2;
16375
+ const nRight = n.x + n.width / 2;
16376
+ if (nRight < tc.x - margin || nLeft > sc.x + margin) continue;
16377
+ xBandObs.push({ x: nLeft, y: n.y - n.height / 2, width: n.width, height: n.height });
16378
+ }
16379
+ const midY = (sc.y + tc.y) / 2;
16380
+ const routeY2 = xBandObs.length > 0 ? findRoutingLane(xBandObs, midY, margin) : midY;
16381
+ const exitBorder = srcExitPt ?? nodeBorderPoint(source, { x: sc.x, y: routeY2 });
16382
+ const exitPt2 = { x: exitBorder.x, y: routeY2 };
16383
+ const enterPt2 = { x: tc.x, y: routeY2 };
16384
+ const tp2 = tgtEnterPt ?? nodeBorderPoint(target, enterPt2);
16385
+ return srcExitPt ? [srcExitPt, exitPt2, enterPt2, tp2] : [exitBorder, exitPt2, enterPt2, tp2];
16386
+ } else {
16387
+ const yBandObs = [];
16388
+ for (const g of groups) {
16389
+ if (g.y + g.height < tc.y - margin || g.y > sc.y + margin) continue;
16390
+ yBandObs.push({ x: g.x, y: g.y, width: g.width, height: g.height });
16391
+ }
16392
+ for (const n of nodes) {
16393
+ if (n.id === source.id || n.id === target.id) continue;
16394
+ const nTop = n.y - n.height / 2;
16395
+ const nBot = n.y + n.height / 2;
16396
+ if (nBot < tc.y - margin || nTop > sc.y + margin) continue;
16397
+ yBandObs.push({ x: n.x - n.width / 2, y: nTop, width: n.width, height: n.height });
16398
+ }
16399
+ const rotated = yBandObs.map((r) => ({ x: r.y, y: r.x, width: r.height, height: r.width }));
16400
+ const midX = (sc.x + tc.x) / 2;
16401
+ const routeX = rotated.length > 0 ? findRoutingLane(rotated, midX, margin) : midX;
16402
+ const exitPt2 = srcExitPt ?? { x: routeX, y: sc.y };
16403
+ const enterPt2 = { x: routeX, y: tc.y };
16404
+ return [
16405
+ srcExitPt ?? nodeBorderPoint(source, exitPt2),
16406
+ exitPt2,
16407
+ enterPt2,
16408
+ tgtEnterPt ?? nodeBorderPoint(target, enterPt2)
16409
+ ];
16410
+ }
16411
+ }
16412
+ const blocking = [];
16413
+ const blockingGroupIds = /* @__PURE__ */ new Set();
16414
+ const pathSrc = srcExitPt ?? sc;
16415
+ const pathTgt = tgtEnterPt ?? tc;
16416
+ for (const g of groups) {
16417
+ if (g.id === source.groupId || g.id === target.groupId) continue;
16418
+ const gRect = { x: g.x, y: g.y, width: g.width, height: g.height };
16419
+ if (curveIntersectsRect(pathSrc, pathTgt, gRect, direction)) {
16420
+ blocking.push(gRect);
16421
+ blockingGroupIds.add(g.id);
16422
+ }
16423
+ }
16424
+ for (const n of nodes) {
16425
+ if (n.id === source.id || n.id === target.id) continue;
16426
+ if (n.groupId && (n.groupId === source.groupId || n.groupId === target.groupId)) continue;
16427
+ if (n.groupId && blockingGroupIds.has(n.groupId)) continue;
16428
+ const nodeRect = { x: n.x - n.width / 2, y: n.y - n.height / 2, width: n.width, height: n.height };
16429
+ if (curveIntersectsRect(pathSrc, pathTgt, nodeRect, direction)) {
16430
+ blocking.push(nodeRect);
16431
+ }
16432
+ }
16433
+ if (blocking.length === 0) {
16434
+ const sp = srcExitPt ?? nodeBorderPoint(source, tc);
16435
+ const tp2 = tgtEnterPt ?? nodeBorderPoint(target, sp);
16436
+ return [sp, tp2];
16437
+ }
16438
+ const obsLeft = Math.min(...blocking.map((o) => o.x));
16439
+ const obsRight = Math.max(...blocking.map((o) => o.x + o.width));
16440
+ const routeY = findRoutingLane(blocking, tc.y, margin);
16441
+ const exitX = direction === "LR" ? Math.max(sc.x, obsLeft - margin) : obsLeft - margin;
16442
+ const enterX = direction === "LR" ? Math.min(tc.x, obsRight + margin) : obsRight + margin;
16443
+ const exitPt = { x: exitX, y: routeY };
16444
+ const enterPt = { x: enterX, y: routeY };
16445
+ const tp = tgtEnterPt ?? nodeBorderPoint(target, enterPt);
16446
+ if (srcExitPt) {
16447
+ return [srcExitPt, exitPt, enterPt, tp];
16448
+ }
16449
+ return [nodeBorderPoint(source, exitPt), exitPt, enterPt, tp];
16450
+ }
15828
16451
  function nodeBorderPoint(node, target) {
15829
16452
  const hw = node.width / 2;
15830
16453
  const hh = node.height / 2;
@@ -16108,33 +16731,29 @@ function renderGroups(svg, groups, palette, isDark) {
16108
16731
  }
16109
16732
  }
16110
16733
  }
16111
- function renderEdgePaths(svg, edges, nodes, palette, isDark, animate) {
16734
+ function renderEdgePaths(svg, edges, nodes, groups, palette, isDark, animate, direction) {
16112
16735
  const nodeMap = new Map(nodes.map((n) => [n.id, n]));
16113
16736
  const maxRps = Math.max(...edges.map((e) => e.computedRps), 1);
16737
+ const { srcPts, tgtPts } = computePortPts(edges, nodeMap, direction);
16114
16738
  for (const edge of edges) {
16115
16739
  if (edge.points.length === 0) continue;
16116
16740
  const targetNode = nodeMap.get(edge.targetId);
16117
16741
  const sourceNode = nodeMap.get(edge.sourceId);
16118
16742
  const color = edgeColor(edge, palette);
16119
16743
  const strokeW = edgeWidth();
16120
- let pts = edge.points;
16121
- if (sourceNode && targetNode && pts.length >= 2) {
16122
- const first = pts[0];
16123
- const distFirstToSource = (first.x - sourceNode.x) ** 2 + (first.y - sourceNode.y) ** 2;
16124
- const distFirstToTarget = (first.x - targetNode.x) ** 2 + (first.y - targetNode.y) ** 2;
16125
- if (distFirstToTarget < distFirstToSource) {
16126
- pts = [...pts].reverse();
16127
- }
16128
- }
16129
- if (sourceNode && pts.length > 0) {
16130
- const bp = nodeBorderPoint(sourceNode, pts[0]);
16131
- pts = [bp, ...pts];
16132
- }
16133
- if (targetNode && pts.length > 0) {
16134
- const bp = nodeBorderPoint(targetNode, pts[pts.length - 1]);
16135
- pts = [...pts, bp];
16136
- }
16137
- const pathD = lineGenerator7(pts) ?? "";
16744
+ if (!sourceNode || !targetNode) continue;
16745
+ const key = `${edge.sourceId}:${edge.targetId}`;
16746
+ const pts = edgeWaypoints(
16747
+ sourceNode,
16748
+ targetNode,
16749
+ groups,
16750
+ nodes,
16751
+ direction,
16752
+ 30,
16753
+ srcPts.get(key),
16754
+ tgtPts.get(key)
16755
+ );
16756
+ const pathD = buildPathD(pts, direction);
16138
16757
  const edgeG = svg.append("g").attr("class", "infra-edge").attr("data-line-number", edge.lineNumber);
16139
16758
  edgeG.append("path").attr("d", pathD).attr("fill", "none").attr("stroke", color).attr("stroke-width", strokeW);
16140
16759
  if (animate && edge.computedRps > 0) {
@@ -16149,19 +16768,34 @@ function renderEdgePaths(svg, edges, nodes, palette, isDark, animate) {
16149
16768
  }
16150
16769
  }
16151
16770
  }
16152
- function renderEdgeLabels(svg, edges, palette, isDark, animate) {
16771
+ function renderEdgeLabels(svg, edges, nodes, groups, palette, isDark, animate, direction) {
16772
+ const nodeMap = new Map(nodes.map((n) => [n.id, n]));
16773
+ const { srcPts, tgtPts } = computePortPts(edges, nodeMap, direction);
16153
16774
  for (const edge of edges) {
16154
16775
  if (edge.points.length === 0) continue;
16155
16776
  if (!edge.label) continue;
16156
- const midIdx = Math.floor(edge.points.length / 2);
16157
- const midPt = edge.points[midIdx];
16777
+ const sourceNode = nodeMap.get(edge.sourceId);
16778
+ const targetNode = nodeMap.get(edge.targetId);
16779
+ if (!sourceNode || !targetNode) continue;
16780
+ const key = `${edge.sourceId}:${edge.targetId}`;
16781
+ const wps = edgeWaypoints(
16782
+ sourceNode,
16783
+ targetNode,
16784
+ groups,
16785
+ nodes,
16786
+ direction,
16787
+ 30,
16788
+ srcPts.get(key),
16789
+ tgtPts.get(key)
16790
+ );
16791
+ const midPt = wps[Math.floor(wps.length / 2)];
16158
16792
  const labelText = edge.label;
16159
16793
  const g = svg.append("g").attr("class", animate ? "infra-edge-label" : "");
16160
16794
  const textWidth = labelText.length * 6.5 + 8;
16161
16795
  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);
16162
16796
  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);
16163
16797
  if (animate) {
16164
- const pathD = lineGenerator7(edge.points) ?? "";
16798
+ const pathD = buildPathD(wps, direction);
16165
16799
  g.insert("path", ":first-child").attr("d", pathD).attr("fill", "none").attr("stroke", "transparent").attr("stroke-width", 20);
16166
16800
  }
16167
16801
  }
@@ -16571,7 +17205,7 @@ function renderInfra(container, layout, palette, isDark, title, titleLineNumber,
16571
17205
  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);
16572
17206
  }
16573
17207
  renderGroups(svg, layout.groups, palette, isDark);
16574
- renderEdgePaths(svg, layout.edges, layout.nodes, palette, isDark, shouldAnimate);
17208
+ renderEdgePaths(svg, layout.edges, layout.nodes, layout.groups, palette, isDark, shouldAnimate, layout.direction);
16575
17209
  const fanoutSourceIds = collectFanoutSourceIds(layout.edges);
16576
17210
  const scaledGroupIds = new Set(
16577
17211
  layout.groups.filter((g) => {
@@ -16583,7 +17217,7 @@ function renderInfra(container, layout, palette, isDark, title, titleLineNumber,
16583
17217
  if (shouldAnimate) {
16584
17218
  renderRejectParticles(svg, layout.nodes);
16585
17219
  }
16586
- renderEdgeLabels(svg, layout.edges, palette, isDark, shouldAnimate);
17220
+ renderEdgeLabels(svg, layout.edges, layout.nodes, layout.groups, palette, isDark, shouldAnimate, layout.direction);
16587
17221
  if (hasLegend) {
16588
17222
  if (fixedLegend) {
16589
17223
  const containerWidth = container.clientWidth || totalWidth;
@@ -16601,7 +17235,7 @@ function parseAndLayoutInfra(content) {
16601
17235
  const layout = layoutInfra(computed);
16602
17236
  return { parsed, computed, layout };
16603
17237
  }
16604
- var d3Selection9, d3Shape7, 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;
17238
+ var d3Selection9, d3Shape7, 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;
16605
17239
  var init_renderer8 = __esm({
16606
17240
  "src/infra/renderer.ts"() {
16607
17241
  "use strict";
@@ -16647,7 +17281,6 @@ var init_renderer8 = __esm({
16647
17281
  REJECT_DURATION_MAX = 3;
16648
17282
  REJECT_COUNT_MIN = 1;
16649
17283
  REJECT_COUNT_MAX = 3;
16650
- lineGenerator7 = d3Shape7.line().x((d) => d.x).y((d) => d.y).curve(d3Shape7.curveBasis);
16651
17284
  PROP_DISPLAY = {
16652
17285
  "cache-hit": "cache hit",
16653
17286
  "firewall-block": "firewall block",
@@ -16830,7 +17463,7 @@ function renderState(container, graph, layout, palette, isDark, onClickItem, exp
16830
17463
  }
16831
17464
  }
16832
17465
  } else if (edge.points.length >= 2) {
16833
- const pathD = lineGenerator8(edge.points);
17466
+ const pathD = lineGenerator7(edge.points);
16834
17467
  if (pathD) {
16835
17468
  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");
16836
17469
  }
@@ -16894,7 +17527,7 @@ function renderStateForExport(content, theme, palette) {
16894
17527
  document.body.removeChild(container);
16895
17528
  }
16896
17529
  }
16897
- var d3Selection10, d3Shape8, 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;
17530
+ var d3Selection10, d3Shape8, 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;
16898
17531
  var init_state_renderer = __esm({
16899
17532
  "src/graph/state-renderer.ts"() {
16900
17533
  "use strict";
@@ -16916,7 +17549,7 @@ var init_state_renderer = __esm({
16916
17549
  PSEUDOSTATE_RADIUS = 10;
16917
17550
  STATE_CORNER_RADIUS = 10;
16918
17551
  GROUP_EXTRA_PADDING2 = 12;
16919
- lineGenerator8 = d3Shape8.line().x((d) => d.x).y((d) => d.y).curve(d3Shape8.curveBasis);
17552
+ lineGenerator7 = d3Shape8.line().x((d) => d.x).y((d) => d.y).curve(d3Shape8.curveBasis);
16920
17553
  }
16921
17554
  });
16922
17555