@cardenelabs/cdl 0.123.0 → 0.125.0

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
@@ -12332,6 +12332,8 @@ function diagram(id, options) {
12332
12332
  ...options.textMetrics !== void 0 ? { textMetrics: options.textMetrics } : {},
12333
12333
  // 種類の名前も同じ経路で渡し、空文字の扱いを `layout` の 1 箇所に揃える
12334
12334
  ...options.type !== void 0 ? { type: options.type } : {},
12335
+ // 空の並びは書かなかった図と同じ形にする。 凡例なしの JSON と配置を変えない
12336
+ ...options.legend && options.legend.length > 0 ? { legend: options.legend.map((item) => ({ ...item })) } : {},
12335
12337
  // inputs / formulas / scrollTriggers / eventBindings は空なら省略 (static diagram の JSON 形状維持)
12336
12338
  ...inputs.length > 0 ? { inputs: [...inputs] } : {},
12337
12339
  ...formulas.length > 0 ? { formulas: [...formulas] } : {},
@@ -12444,7 +12446,7 @@ function drawsTitleOutsideBox(kind) {
12444
12446
  }
12445
12447
  var TITLE_FONT_SIZE_DEFAULT = 18;
12446
12448
  var TITLE_FONT_SIZE = new Map([
12447
- ...["dyn-rect", "dyn-circle", "dyn-arc", "dyn-wave", "dyn-polygon", "station"].map(
12449
+ ...["dyn-rect", "dyn-circle", "dyn-arc", "dyn-wave", "dyn-polygon"].map(
12448
12450
  (k) => [k, 13]
12449
12451
  ),
12450
12452
  ...[
@@ -12489,8 +12491,10 @@ var TITLE_FONT_SIZE = new Map([
12489
12491
  "webhook"
12490
12492
  ].map((k) => [k, 20]),
12491
12493
  ...["card", "event", "function"].map((k) => [k, 24]),
12494
+ ["station", 25],
12492
12495
  ["actor", 32],
12493
- ["storage", 34]
12496
+ ["storage", 34],
12497
+ ["timeline-number", 18]
12494
12498
  ]);
12495
12499
  function titleFontSize(kind) {
12496
12500
  return TITLE_FONT_SIZE.get(kind ?? "") ?? TITLE_FONT_SIZE_DEFAULT;
@@ -12500,9 +12504,17 @@ function skipsNodeTitleWidthCheck(kind) {
12500
12504
  return KINDS_WITH_TITLE_WIDTH_OUTSIDE_NODE.has(kind ?? "");
12501
12505
  }
12502
12506
  var TITLE_BOX_PADDING = 52;
12507
+ var TIMELINE_NUMBER_STROKE = 4;
12508
+ function titleUsableWidth(kind, w, h) {
12509
+ if (kind === "timeline-number") {
12510
+ return Math.min(w, h) - 2 * TIMELINE_NUMBER_STROKE;
12511
+ }
12512
+ return w - (drawsTitleOutsideBox(kind) ? 0 : TITLE_BOX_PADDING);
12513
+ }
12503
12514
  var KINDS_SCALING_TITLE_WHEN_LOW = /* @__PURE__ */ new Set(["actor", "event", "function", "storage"]);
12504
12515
  var KINDS_SCALING_TITLE_ALWAYS = /* @__PURE__ */ new Set([
12505
12516
  "card",
12517
+ "timeline-number",
12506
12518
  "shape-file",
12507
12519
  "shape-folder",
12508
12520
  "shape-cloud",
@@ -12586,14 +12598,16 @@ var KINDS_DRAWING_ONLY_SHAPE = [
12586
12598
  ];
12587
12599
  var KINDS_WITHOUT_EYEBROW_TEXT = /* @__PURE__ */ new Set([
12588
12600
  ...KINDS_DRAWING_ONLY_SHAPE,
12589
- ...KINDS_WITHOUT_TITLE_TEXT
12601
+ ...KINDS_WITHOUT_TITLE_TEXT,
12602
+ "timeline-number"
12590
12603
  ]);
12591
12604
  function rendersEyebrow(kind) {
12592
12605
  return !KINDS_WITHOUT_EYEBROW_TEXT.has(kind ?? "");
12593
12606
  }
12594
12607
  var KINDS_WITHOUT_SUBTITLE_TEXT = /* @__PURE__ */ new Set([
12595
12608
  ...KINDS_DRAWING_OWN_PAYLOAD,
12596
- ...KINDS_WITHOUT_TITLE_TEXT
12609
+ ...KINDS_WITHOUT_TITLE_TEXT,
12610
+ "timeline-number"
12597
12611
  ]);
12598
12612
  function rendersSubtitle(kind) {
12599
12613
  return !KINDS_WITHOUT_SUBTITLE_TEXT.has(kind ?? "");
@@ -14700,6 +14714,14 @@ function curvePath(from, to, edge, labelMarginExtra = 0, textMetrics = "proporti
14700
14714
 
14701
14715
  // src/layout/metro-path.ts
14702
14716
  var METRO_CORNER_RADIUS = 24;
14717
+ var METRO_BRANCH_CORNER_RADIUS = 22;
14718
+ var METRO_BRANCH_STUB = 30;
14719
+ var METRO_DECISION_HEIGHT = 88;
14720
+ var METRO_DECISION_WIDTH = 116;
14721
+ var METRO_RETURN_END_OFFSET = 43;
14722
+ var METRO_RETURN_START_OFFSET = 24;
14723
+ var METRO_LINE_WIDTH = 10;
14724
+ var METRO_LINE_WIDTH_CSS = `var(--cdl-metro-line-width, ${METRO_LINE_WIDTH}px)`;
14703
14725
  var T = Math.SQRT2 - 1;
14704
14726
  function round2(v) {
14705
14727
  return Math.round(v * 100) / 100;
@@ -14717,7 +14739,63 @@ function polyline(points) {
14717
14739
  });
14718
14740
  return distinct.map((p, i) => `${i === 0 ? "M" : "L"} ${point(p)}`).join(" ");
14719
14741
  }
14720
- function metroPath(from, to, edge, labelMarginExtra = 0, textMetrics = "proportional") {
14742
+ function quadraticCornerPath(start, corner, end, radius) {
14743
+ const horizontalDir = Math.sign(corner.x - start.x);
14744
+ const verticalDir = Math.sign(end.y - corner.y);
14745
+ const r = Math.min(
14746
+ radius,
14747
+ Math.abs(corner.x - start.x),
14748
+ Math.abs(end.y - corner.y)
14749
+ );
14750
+ const before = { x: corner.x - horizontalDir * r, y: corner.y };
14751
+ const after = { x: corner.x, y: corner.y + verticalDir * r };
14752
+ return [
14753
+ `M ${point(start)}`,
14754
+ `L ${point(before)}`,
14755
+ `Q ${point(corner)} ${point(after)}`,
14756
+ `L ${point(end)}`
14757
+ ].join(" ");
14758
+ }
14759
+ function decisionBranchPath(start, end) {
14760
+ const horizontalDir = Math.sign(end.x - start.x);
14761
+ const verticalDir = Math.sign(end.y - start.y);
14762
+ const turnX = start.x + horizontalDir * (METRO_BRANCH_STUB + METRO_BRANCH_CORNER_RADIUS);
14763
+ const radius = Math.min(
14764
+ METRO_BRANCH_CORNER_RADIUS,
14765
+ Math.abs(end.y - start.y) / 2,
14766
+ Math.abs(end.x - turnX)
14767
+ );
14768
+ const firstCorner = { x: turnX, y: start.y };
14769
+ const secondCorner = { x: turnX, y: end.y };
14770
+ const beforeFirst = {
14771
+ x: turnX - horizontalDir * radius,
14772
+ y: start.y
14773
+ };
14774
+ const afterFirst = {
14775
+ x: turnX,
14776
+ y: start.y + verticalDir * radius
14777
+ };
14778
+ const beforeSecond = {
14779
+ x: turnX,
14780
+ y: end.y - verticalDir * radius
14781
+ };
14782
+ const afterSecond = {
14783
+ x: turnX + horizontalDir * radius,
14784
+ y: end.y
14785
+ };
14786
+ return {
14787
+ d: [
14788
+ `M ${point(start)}`,
14789
+ `L ${point(beforeFirst)}`,
14790
+ `Q ${point(firstCorner)} ${point(afterFirst)}`,
14791
+ `L ${point(beforeSecond)}`,
14792
+ `Q ${point(secondCorner)} ${point(afterSecond)}`,
14793
+ `L ${point(end)}`
14794
+ ].join(" "),
14795
+ turnX
14796
+ };
14797
+ }
14798
+ function metroPath(from, to, edge, _labelMarginExtra = 0, _textMetrics = "proportional") {
14721
14799
  const fromLeft = from.cx - from.w / 2;
14722
14800
  const fromRight = from.cx + from.w / 2;
14723
14801
  const toLeft = to.cx - to.w / 2;
@@ -14752,11 +14830,45 @@ function metroPath(from, to, edge, labelMarginExtra = 0, textMetrics = "proporti
14752
14830
  const sgn = Math.sign(dy);
14753
14831
  let d;
14754
14832
  let mid;
14755
- let tangent;
14756
- if (Math.abs(dy) < SAME_HEIGHT_THRESHOLD) {
14833
+ let metroReturn = false;
14834
+ if (from.kind === "decision" && Math.abs(dy) >= SAME_HEIGHT_THRESHOLD) {
14835
+ const verticalDir = Math.sign(dy);
14836
+ if (Math.abs(to.cx - from.cx) < SAME_HEIGHT_THRESHOLD) {
14837
+ const start = { x: from.cx, y: from.cy + verticalDir * from.h / 2 };
14838
+ const end = { x: to.cx, y: to.cy };
14839
+ d = polyline([start, end]);
14840
+ fromSide = verticalDir < 0 ? "top" : "bottom";
14841
+ toSide = verticalDir < 0 ? "bottom" : "top";
14842
+ mid = { x: start.x, y: (start.y + end.y) / 2 };
14843
+ } else {
14844
+ const horizontalDir = Math.sign(to.cx - from.cx);
14845
+ const start = {
14846
+ x: from.cx + horizontalDir * from.w / 2,
14847
+ y: from.cy
14848
+ };
14849
+ const end = {
14850
+ x: to.cx - horizontalDir * to.w / 2,
14851
+ y: to.cy
14852
+ };
14853
+ const branch = decisionBranchPath(start, end);
14854
+ d = branch.d;
14855
+ fromSide = horizontalDir > 0 ? "right" : "left";
14856
+ toSide = horizontalDir > 0 ? "left" : "right";
14857
+ mid = { x: branch.turnX, y: (start.y + end.y) / 2 };
14858
+ }
14859
+ } else if (edge?.style === "dashed" && from.kind === "station" && to.kind === "station" && to.cx < from.cx && Math.abs(dy) >= SAME_HEIGHT_THRESHOLD) {
14860
+ const verticalDir = Math.sign(to.cy - from.cy);
14861
+ const start = { x: from.cx - METRO_RETURN_START_OFFSET, y: from.cy };
14862
+ const end = { x: to.cx, y: to.cy - verticalDir * METRO_RETURN_END_OFFSET };
14863
+ const corner = { x: end.x, y: start.y };
14864
+ fromSide = "left";
14865
+ toSide = verticalDir < 0 ? "bottom" : "top";
14866
+ d = quadraticCornerPath(start, corner, end, METRO_BRANCH_CORNER_RADIUS);
14867
+ mid = { x: (start.x + corner.x) / 2, y: start.y };
14868
+ metroReturn = true;
14869
+ } else if (Math.abs(dy) < SAME_HEIGHT_THRESHOLD) {
14757
14870
  d = `M ${point({ x: x1, y: y1 })} L ${point({ x: x2, y: y2 })}`;
14758
14871
  mid = { x: (x1 + x2) / 2, y: (y1 + y2) / 2 };
14759
- tangent = { x: x2 - x1, y: y2 - y1 };
14760
14872
  } else if (fromSide !== toSide && Math.abs(x2 - x1) >= Math.abs(dy)) {
14761
14873
  const horizontal = Math.abs(x2 - x1);
14762
14874
  const s = (horizontal - Math.abs(dy)) / 2;
@@ -14786,7 +14898,6 @@ function metroPath(from, to, edge, labelMarginExtra = 0, textMetrics = "proporti
14786
14898
  d = polyline([p0, p1, p2, p3]);
14787
14899
  }
14788
14900
  mid = { x: (x1 + x2) / 2, y: (y1 + y2) / 2 };
14789
- tangent = w;
14790
14901
  } else {
14791
14902
  fromSide = "right";
14792
14903
  toSide = "right";
@@ -14816,33 +14927,32 @@ function metroPath(from, to, edge, labelMarginExtra = 0, textMetrics = "proporti
14816
14927
  `L ${point(p4)}`
14817
14928
  ].join(" ");
14818
14929
  mid = { x: p2.x - T * r, y: p2.y };
14819
- tangent = { x: 0, y: sgn };
14820
- }
14821
- if (edge?.overlay) {
14822
- return { d, fromSide, toSide, labelX: mid.x, labelY: mid.y, labelAnchor: "middle" };
14823
14930
  }
14824
- const tangentLength = Math.hypot(tangent.x, tangent.y);
14825
- let normalX = tangent.y / tangentLength;
14826
- let normalY = -tangent.x / tangentLength;
14827
- if (normalY > Number.EPSILON || Math.abs(normalY) <= Number.EPSILON && normalX < 0) {
14828
- normalX *= -1;
14829
- normalY *= -1;
14830
- }
14831
- const labelW = computeLabelBoxW(edge?.label, edge?.sub, textMetrics);
14832
- const labelH = labelPillH(edge?.sub);
14833
- const distance = labelPathClearanceHorizontal(edge) + (Math.abs(normalX) * labelW + Math.abs(normalY) * labelH) / 2 + labelMarginExtra;
14834
14931
  return {
14835
14932
  d,
14836
14933
  fromSide,
14837
14934
  toSide,
14838
- labelX: mid.x + normalX * distance,
14839
- labelY: mid.y + normalY * distance,
14840
- labelAnchor: "middle"
14935
+ labelX: mid.x,
14936
+ labelY: mid.y,
14937
+ labelAnchor: "middle",
14938
+ ...metroReturn ? { metroReturn: true } : {}
14841
14939
  };
14842
14940
  }
14843
14941
 
14844
14942
  // src/types.ts
14845
14943
  var TONES = ["accent", "teal", "success", "error", "warning", "info"];
14944
+ var LEGEND_MARKS = [
14945
+ "diamond",
14946
+ "filled-circle",
14947
+ "double-circle",
14948
+ "dotted-line",
14949
+ "solid-line",
14950
+ "curved-line",
14951
+ "rounded-label",
14952
+ "numbered-circle",
14953
+ "station",
14954
+ "arrow"
14955
+ ];
14846
14956
  var NODE_KINDS = [
14847
14957
  // dynamic shape (CAR: shape primitive) ... signal 値で内部 fill / radius / angle 等が
14848
14958
  // リアルタイム変化する汎用 container、 shape spec は node.shape に格納
@@ -14964,12 +15074,14 @@ var NODE_KINDS = [
14964
15074
  "shape-lawyer",
14965
15075
  "shape-trader",
14966
15076
  "shape-customer-service",
14967
- // 流れの始まりと終わりの印 2 種 (#560) ... 塗った丸と輪で囲んだ丸。
14968
- // 状態遷移図が使う。 印そのものが意味を持つので文字は描かない
15077
+ // 流れの始まりと終わりの印 2 種 (#560) ... 始点と終点は線の端から推測せず、著者が
15078
+ // `kind: "mark-start"` / `kind: "mark-end"` の node を明示する。印は文字を描かない
14969
15079
  "mark-start",
14970
15080
  "mark-end",
14971
15081
  // 路線図の駅 (#996) ... 線路上の点を小さな丸で示し、名前と説明は丸の外に置く
14972
15082
  "station",
15083
+ // 時間軸の番号 (#1017) ... 実線の軸上に番号を入れた丸を置く。路線図の駅とは別の意味を持つ。
15084
+ "timeline-number",
14973
15085
  // 時系列のやり取り (#578) ... 1 つの箱が図を丸ごと描く。 縦が時間で、参加者は箱を持たない
14974
15086
  "sequence-board"
14975
15087
  ];
@@ -17703,7 +17815,7 @@ function runAxes(laid, diag, violations, counts, push, profile, skippedAxes, \u5
17703
17815
  const LIFELINE_W_THRESHOLD = 8;
17704
17816
  for (const n of laid.nodes) {
17705
17817
  if (n.w < LIFELINE_W_THRESHOLD) continue;
17706
- if (n.kind === "station") continue;
17818
+ if (n.kind === "station" || n.kind === "timeline-number") continue;
17707
17819
  if (n.w < MIN_NODE_W || n.h < MIN_NODE_H) {
17708
17820
  push(
17709
17821
  "node-visibility",
@@ -17729,8 +17841,7 @@ function runAxes(laid, diag, violations, counts, push, profile, skippedAxes, \u5
17729
17841
  if (!isRenderedNode(n)) continue;
17730
17842
  const titleTrimmed = n.title?.trim() ?? "";
17731
17843
  if (!titleTrimmed) continue;
17732
- const \u4F59\u767D = drawsTitleOutsideBox(n.kind) ? 0 : TITLE_BOX_PADDING;
17733
- const \u4F7F\u3048\u308B\u5E452 = n.w - \u4F59\u767D;
17844
+ const \u4F7F\u3048\u308B\u5E452 = titleUsableWidth(n.kind, n.w, n.h);
17734
17845
  const \u5B9F\u5E45 = measureTextWidth(titleTrimmed, {
17735
17846
  fontSize: titleFontSize(n.kind),
17736
17847
  bound: "drawn",
@@ -17749,7 +17860,7 @@ function runAxes(laid, diag, violations, counts, push, profile, skippedAxes, \u5
17749
17860
  push(
17750
17861
  "text-readability",
17751
17862
  // 字ごとの実幅は小数になるので、読む人に出す時だけ丸める (判定は丸めない)
17752
- `node "${n.id}" title "${n.title}" \u304C node \u5E45 ${n.w}px \u3092\u8D85\u904E\u3059\u308B\u53EF\u80FD\u6027 (\u671F\u5F85 ${Math.round(\u5B9F\u5E45 + \u4F59\u767D)}px)`,
17863
+ `node "${n.id}" title "${n.title}" \u304C node \u5E45 ${n.w}px \u3092\u8D85\u904E\u3059\u308B\u53EF\u80FD\u6027 (\u671F\u5F85 ${Math.round(\u5B9F\u5E45 + (n.w - \u4F7F\u3048\u308B\u5E452))}px)`,
17753
17864
  "warn"
17754
17865
  );
17755
17866
  }
@@ -19563,6 +19674,8 @@ var TOKENS = {
19563
19674
  lane_label_h: 40,
19564
19675
  lane_top_pad: 100,
19565
19676
  contain_pad: 28,
19677
+ stage_column_gap: 48,
19678
+ stage_card_text_inset: 18,
19566
19679
  // node sizes (kind ごと、 unified row h は engine が max() で算出)
19567
19680
  node: {
19568
19681
  actor: { w: 340, h: 165 },
@@ -19689,7 +19802,10 @@ var TOKENS = {
19689
19802
  "mark-start": { w: 96, h: 96 },
19690
19803
  "mark-end": { w: 96, h: 96 },
19691
19804
  // 路線図の駅 (#996) ... 正方形にして小さな丸が縦横どちらにも歪まないようにする。
19692
- station: { w: 24, h: 24 },
19805
+ // 半径 16 と縁 6 の外径を、線路との接点を含めて収める。
19806
+ station: { w: 38, h: 38 },
19807
+ // 時間軸の番号 (#1017) ... 半径 24 の丸と太さ 4 の縁を収める。
19808
+ "timeline-number": { w: 52, h: 52 },
19693
19809
  // 時系列のやり取り (#578)。 実寸は中身から決まる (`sequenceBoardMetrics`) ので、
19694
19810
  // ここは書かなかった時の受け皿
19695
19811
  "sequence-board": { w: 960, h: 640 }
@@ -19704,6 +19820,8 @@ var LANE_TOP_PAD = TOKENS.lane_top_pad;
19704
19820
  var STACK_GAP = TOKENS.row_gap;
19705
19821
  var NODE_SIZE = TOKENS.node;
19706
19822
  var CONTAIN_PAD = TOKENS.contain_pad;
19823
+ var STAGE_COLUMN_GAP = TOKENS.stage_column_gap;
19824
+ var STAGE_CARD_TEXT_INSET = TOKENS.stage_card_text_inset;
19707
19825
  var VIEW_PAD = TOKENS.view_pad;
19708
19826
  var HEADER_RESERVE = TOKENS.header_reserve;
19709
19827
  var DEFAULT_LANE_GAP = 80;
@@ -19804,12 +19922,24 @@ function layoutEdges(diag, nodes, lanes = [], textMetrics = "proportional") {
19804
19922
  if (!from) throw new Error(`cdl layout: edge "${e.id}" \u306E from "${e.from}" \u304C node \u306B\u7121\u3044`);
19805
19923
  if (!to) throw new Error(`cdl layout: edge "${e.id}" \u306E to "${e.to}" \u304C node \u306B\u7121\u3044`);
19806
19924
  const c = curvePath(from, to, e, labelMarginExtra, textMetrics);
19925
+ const fromLane = lmap.get(from.lane);
19926
+ const toLane = lmap.get(to.lane);
19927
+ let labelX = c.labelX;
19928
+ if (fromLane?.stage !== void 0 && toLane?.stage !== void 0 && fromLane.id !== toLane.id && (e.label || e.sub)) {
19929
+ const [leftLane, rightLane] = fromLane.x <= toLane.x ? [fromLane, toLane] : [toLane, fromLane];
19930
+ const gapLeft = leftLane.x + leftLane.width - STAGE_COLUMN_GAP / 2;
19931
+ const gapRight = rightLane.x + STAGE_COLUMN_GAP / 2;
19932
+ const labelHalfWidth = computeLabelBoxW(e.label, e.sub, textMetrics) / 2;
19933
+ const minX = gapLeft + labelHalfWidth;
19934
+ const maxX = gapRight - labelHalfWidth;
19935
+ if (minX <= maxX) labelX = Math.min(maxX, Math.max(minX, labelX));
19936
+ }
19807
19937
  positionedEdges.push({
19808
19938
  ...e,
19809
19939
  d: c.d,
19810
19940
  fromSide: c.fromSide,
19811
19941
  toSide: c.toSide,
19812
- labelX: c.labelX + (e.labelOffsetX ?? 0),
19942
+ labelX: labelX + (e.labelOffsetX ?? 0),
19813
19943
  labelY: c.labelY + (e.labelOffsetY ?? 0),
19814
19944
  labelAnchor: c.labelAnchor
19815
19945
  });
@@ -19826,7 +19956,8 @@ function layoutEdges(diag, nodes, lanes = [], textMetrics = "proportional") {
19826
19956
  toSide: m.toSide,
19827
19957
  labelX: m.labelX + (e.labelOffsetX ?? 0),
19828
19958
  labelY: m.labelY + (e.labelOffsetY ?? 0),
19829
- labelAnchor: m.labelAnchor
19959
+ labelAnchor: m.labelAnchor,
19960
+ ...m.metroReturn ? { metroReturn: true } : {}
19830
19961
  });
19831
19962
  } else {
19832
19963
  autoEdges.push(e);
@@ -20514,6 +20645,57 @@ function bowSharedStraightPaths(edges, nodes, skipIds, textMetrics = "proportion
20514
20645
  function nodeRect(n) {
20515
20646
  return { x: n.cx - n.w / 2, y: n.cy - n.h / 2, w: n.w, h: n.h };
20516
20647
  }
20648
+ function countPathObstacleCrossings(d, obstacles) {
20649
+ const segments = flattenPathSegments(extractPathSegments(d));
20650
+ return obstacles.filter((obstacle) => segmentsCrossRect(segments, nodeRect(obstacle))).length;
20651
+ }
20652
+ function makeBackDetourCandidate(A, B, from, to, obstacles, detourY, side, columnXOverride) {
20653
+ const lowIsA = A.y >= B.y;
20654
+ const low = lowIsA ? A : B;
20655
+ const lowBendY = low.y - VERTICAL_DETOUR_EXIT;
20656
+ const bandTop = Math.min(detourY, lowBendY);
20657
+ const bandBottom = Math.max(detourY, lowBendY);
20658
+ const bandObstacles = obstacles.map(nodeRect).filter((rect) => rect.y + rect.h >= bandTop && rect.y <= bandBottom);
20659
+ let columnX = columnXOverride ?? detourColumnFor([from, to], side);
20660
+ if (columnXOverride === void 0) {
20661
+ while (true) {
20662
+ const blockers = bandObstacles.filter(
20663
+ (rect) => rect.x - VERTICAL_DETOUR_GAP < columnX && columnX < rect.x + rect.w + VERTICAL_DETOUR_GAP
20664
+ );
20665
+ if (blockers.length === 0) break;
20666
+ columnX = side === "left" ? Math.min(...blockers.map((rect) => rect.x - VERTICAL_DETOUR_GAP)) : Math.max(...blockers.map((rect) => rect.x + rect.w + VERTICAL_DETOUR_GAP));
20667
+ }
20668
+ }
20669
+ const bendAY = lowIsA ? lowBendY : detourY;
20670
+ const bendBY = lowIsA ? detourY : lowBendY;
20671
+ const vertices = [
20672
+ A,
20673
+ { x: A.x, y: bendAY },
20674
+ { x: columnX, y: bendAY },
20675
+ { x: columnX, y: bendBY },
20676
+ { x: B.x, y: bendBY },
20677
+ B
20678
+ ];
20679
+ const d = verticesToPath(vertices);
20680
+ const high = lowIsA ? B : A;
20681
+ return {
20682
+ d,
20683
+ labelX: (high.x + columnX) / 2,
20684
+ crossings: countPathObstacleCrossings(d, obstacles),
20685
+ length: vertices.slice(1).reduce((sum, vertex, index) => {
20686
+ const previous = vertices[index];
20687
+ return sum + Math.hypot(vertex.x - previous.x, vertex.y - previous.y);
20688
+ }, 0)
20689
+ };
20690
+ }
20691
+ function chooseBackDetourCandidate(A, B, from, to, obstacles, detourY) {
20692
+ const left = makeBackDetourCandidate(A, B, from, to, obstacles, detourY, "left");
20693
+ const right = makeBackDetourCandidate(A, B, from, to, obstacles, detourY, "right");
20694
+ if (left.crossings !== right.crossings) return left.crossings < right.crossings ? left : right;
20695
+ if (Math.abs(left.length - right.length) >= 0.5) return left.length < right.length ? left : right;
20696
+ const preferred = Math.abs(A.x - B.x) < 0.5 ? "right" : A.x < B.x ? "left" : "right";
20697
+ return preferred === "left" ? left : right;
20698
+ }
20517
20699
  function rectGap(a, b) {
20518
20700
  const dx = Math.max(0, a.x - (b.x + b.w), b.x - (a.x + a.w));
20519
20701
  const dy = Math.max(0, a.y - (b.y + b.h), b.y - (a.y + a.h));
@@ -20591,7 +20773,7 @@ function sidePoint(n, side) {
20591
20773
  return { x: n.cx + n.w / 2, y: n.cy };
20592
20774
  }
20593
20775
  }
20594
- function routePath(from, to, fromSide, toSide, obstacles = [], detourSide, _fromLane, _toLane, edge, labelObstacles = obstacles, fromOffset = { x: 0, y: 0 }, toOffset = { x: 0, y: 0 }, avoidRects = [], textMetrics = "proportional") {
20776
+ function routePath(from, to, fromSide, toSide, obstacles = [], detourSide, fromLane, toLane, edge, labelObstacles = obstacles, fromOffset = { x: 0, y: 0 }, toOffset = { x: 0, y: 0 }, avoidRects = [], textMetrics = "proportional") {
20595
20777
  const labelBoxW = edge ? computeLabelBoxW(edge.label, edge.sub, textMetrics) : 0;
20596
20778
  const labelHalfW = labelBoxW / 2;
20597
20779
  const labelInit = labelInitClearance(edge);
@@ -20961,23 +21143,52 @@ function routePath(from, to, fromSide, toSide, obstacles = [], detourSide, _from
20961
21143
  };
20962
21144
  }
20963
21145
  if (topTopPair) {
20964
- const detourY = Math.min(A.y, B.y) - 100;
21146
+ const stagePoint = (point2, node, lane) => lane?.stage === void 0 ? point2 : {
21147
+ x: Math.min(node.cx + node.w / 2 - 24, lane.x + lane.width - 24),
21148
+ y: point2.y
21149
+ };
21150
+ const backA = stagePoint(A, from, fromLane);
21151
+ const backB = stagePoint(B, to, toLane);
21152
+ const stageTops = [fromLane, toLane].filter((lane) => lane?.stage !== void 0).map((lane) => lane.y);
21153
+ const detourY = Math.min(
21154
+ Math.min(backA.y, backB.y) - 100,
21155
+ ...stageTops.map((top) => top - VERTICAL_DETOUR_GAP)
21156
+ );
20965
21157
  const corner = 14;
20966
- const xDir = B.x > A.x ? 1 : -1;
20967
- return {
20968
- d: `M ${A.x} ${A.y}
20969
- L ${A.x} ${detourY + corner}
20970
- Q ${A.x} ${detourY}, ${A.x + xDir * corner} ${detourY}
20971
- L ${B.x - xDir * corner} ${detourY}
20972
- Q ${B.x} ${detourY}, ${B.x} ${detourY + corner}
20973
- L ${B.x} ${B.y}`,
20974
- labelX: (A.x + B.x) / 2,
21158
+ const xDir = backB.x > backA.x ? 1 : -1;
21159
+ const existing = {
21160
+ d: `M ${backA.x} ${backA.y}
21161
+ L ${backA.x} ${detourY + corner}
21162
+ Q ${backA.x} ${detourY}, ${backA.x + xDir * corner} ${detourY}
21163
+ L ${backB.x - xDir * corner} ${detourY}
21164
+ Q ${backB.x} ${detourY}, ${backB.x} ${detourY + corner}
21165
+ L ${backB.x} ${backB.y}`,
21166
+ labelX: (backA.x + backB.x) / 2,
20975
21167
  // CAR-468 SSOT ... 旧 detourY - 30 magic number を LABEL_INIT_CLEARANCE (26、 CAR-570 で 34 → 26 縮小) に置換。
20976
21168
  // CAR-518 SSOT ... dotted-flow は 62 world (marker glow radius 分拡張) を labelInit で参照。
20977
21169
  // top-top detour の水平 segment 上方 labelInit world = spec 準拠距離。
20978
21170
  labelY: detourY - labelInit,
20979
21171
  labelAnchor: "middle"
20980
21172
  };
21173
+ const existingCrossings = countPathObstacleCrossings(existing.d, obstacles);
21174
+ if (existingCrossings === 0) return existing;
21175
+ const candidate = from.lane === to.lane && fromLane?.stage !== void 0 ? makeBackDetourCandidate(
21176
+ backA,
21177
+ backB,
21178
+ from,
21179
+ to,
21180
+ obstacles,
21181
+ detourY,
21182
+ "right",
21183
+ fromLane.x + fromLane.width
21184
+ ) : chooseBackDetourCandidate(backA, backB, from, to, obstacles, detourY);
21185
+ if (candidate.crossings >= existingCrossings) return existing;
21186
+ return {
21187
+ d: candidate.d,
21188
+ labelX: candidate.labelX,
21189
+ labelY: detourY - labelInit,
21190
+ labelAnchor: "middle"
21191
+ };
20981
21192
  }
20982
21193
  const midX = fromSide === "left" || fromSide === "right" ? B.x : A.x;
20983
21194
  const midY = fromSide === "top" || fromSide === "bottom" ? B.y : A.y;
@@ -21904,12 +22115,56 @@ function \u76F4\u524D\u306E\u6298\u308C\u3054\u3068\u305A\u3089\u3059(segs, \u6A
21904
22115
  return parts.join(" ");
21905
22116
  }
21906
22117
 
22118
+ // src/layout/legend.ts
22119
+ var LEGEND_FONT_SIZE = 17;
22120
+ var LEGEND_MARK_SCALE = LEGEND_FONT_SIZE / 21;
22121
+ var LEGEND_MARK_REFERENCE_WIDTH = 46;
22122
+ var LEGEND_MARK_WIDTH = LEGEND_MARK_REFERENCE_WIDTH * LEGEND_MARK_SCALE;
22123
+ var LEGEND_MARK_TEXT_GAP = 12 * LEGEND_MARK_SCALE;
22124
+ var LEGEND_ITEM_GAP_RATIO = 0.07;
22125
+ var LEGEND_BASELINE_GAP = 32;
22126
+ var LEGEND_BOTTOM_EXTENT = 10;
22127
+ function layoutLegend(items, viewBox, textMetrics, nodes) {
22128
+ const renderedNodes = nodes.filter(isRenderedNode);
22129
+ const startX = renderedNodes.length > 0 ? Math.min(...renderedNodes.map((node) => node.cx - node.w / 2)) : viewBox.x + VIEW_PAD;
22130
+ const diagramRight = renderedNodes.length > 0 ? Math.max(...renderedNodes.map((node) => node.cx + node.w / 2)) : viewBox.x + viewBox.w - VIEW_PAD;
22131
+ const itemGap = (diagramRight - startX) * LEGEND_ITEM_GAP_RATIO;
22132
+ const y = viewBox.y + viewBox.h - VIEW_PAD + LEGEND_BASELINE_GAP;
22133
+ let x = startX;
22134
+ const legend = items.map((item) => {
22135
+ const laid = { ...item, x, y };
22136
+ x += LEGEND_MARK_WIDTH + LEGEND_MARK_TEXT_GAP + textWidth(item.text, LEGEND_FONT_SIZE, 0, textMetrics) + itemGap;
22137
+ return laid;
22138
+ });
22139
+ const right = x - itemGap;
22140
+ const bottom = y + LEGEND_BOTTOM_EXTENT + VIEW_PAD;
22141
+ return {
22142
+ legend,
22143
+ viewBox: {
22144
+ ...viewBox,
22145
+ w: Math.max(viewBox.w, right + VIEW_PAD - viewBox.x),
22146
+ h: Math.max(viewBox.h, bottom - viewBox.y)
22147
+ }
22148
+ };
22149
+ }
22150
+
21907
22151
  // src/layout/lanes.ts
21908
22152
  var LANE_POSITION_EPSILON = 1e-9;
22153
+ function equalizeStageColumns(lanes, nodes = []) {
22154
+ const stages = lanes.filter((lane) => lane.stage !== void 0);
22155
+ if (stages.length === 0) return lanes;
22156
+ const top = Math.min(...stages.map((lane) => lane.y));
22157
+ const stageIds = new Set(stages.map((lane) => lane.id));
22158
+ const nodeBottoms = nodes.filter((node) => stageIds.has(node.lane)).map((node) => node.cy + node.h / 2 + CONTAIN_PAD);
22159
+ const bottom = Math.max(...stages.map((lane) => lane.y + lane.height), ...nodeBottoms);
22160
+ return lanes.map(
22161
+ (lane) => lane.stage === void 0 ? lane : { ...lane, y: top, height: bottom - top }
22162
+ );
22163
+ }
21909
22164
  function layoutLanes(diag) {
21910
22165
  const laneGap = diag.viewport?.laneGap ?? diag.viewport?.gap ?? DEFAULT_LANE_GAP;
21911
22166
  let cursorX = 0;
21912
- return diag.lanes.map((lane) => {
22167
+ const lanes = diag.lanes.map((lane, index) => {
21913
22168
  if (lane.posX !== void 0 && lane.posY !== void 0) {
21914
22169
  return {
21915
22170
  ...lane,
@@ -21920,7 +22175,9 @@ function layoutLanes(diag) {
21920
22175
  };
21921
22176
  }
21922
22177
  const resolvedX = lane.x ?? cursorX;
21923
- cursorX = resolvedX + lane.width + laneGap;
22178
+ const nextLane = diag.lanes[index + 1];
22179
+ const gapAfter = lane.stage !== void 0 && nextLane?.stage !== void 0 ? 0 : laneGap;
22180
+ cursorX = resolvedX + lane.width + gapAfter;
21924
22181
  return {
21925
22182
  ...lane,
21926
22183
  x: resolvedX,
@@ -21928,6 +22185,7 @@ function layoutLanes(diag) {
21928
22185
  height: LANE_LABEL_H
21929
22186
  };
21930
22187
  });
22188
+ return equalizeStageColumns(lanes);
21931
22189
  }
21932
22190
  function expandLanesForNodes(lanes, nodes) {
21933
22191
  const targetNodeGap = targetNodeNodeClearance();
@@ -21956,7 +22214,8 @@ function expandLanesForNodes(lanes, nodes) {
21956
22214
  if (lane.posX !== void 0 && lane.posY !== void 0) continue;
21957
22215
  const inside = nodes.filter((n) => n.lane === lane.id);
21958
22216
  const maxNodeW = inside.length > 0 ? Math.max(...inside.map((n) => n.w)) : 0;
21959
- const newWidth = Math.max(lane.width, maxNodeW + padFor(lane) * 2);
22217
+ const stageMinWidth = lane.stage === void 0 ? 0 : maxNodeW + CONTAIN_PAD + STAGE_COLUMN_GAP;
22218
+ const newWidth = Math.max(lane.width, maxNodeW + padFor(lane) * 2, stageMinWidth);
21960
22219
  widthOf.set(lane.id, newWidth);
21961
22220
  deltaOf.set(lane.id, newWidth - lane.width);
21962
22221
  }
@@ -21981,6 +22240,34 @@ function expandLaneGapsForEdgeLabels(lanes, nodes, edges, textMetrics = "proport
21981
22240
  if (lanes.length < 2) return lanes;
21982
22241
  const layoutedLanes = lanes.filter((l) => !(l.posX !== void 0 && l.posY !== void 0));
21983
22242
  if (layoutedLanes.length < 2) return lanes;
22243
+ if (layoutedLanes.length === lanes.length && layoutedLanes.every((lane) => lane.stage !== void 0)) {
22244
+ const sortedStages = [...layoutedLanes].sort((a, b) => a.x - b.x);
22245
+ const stageIndexById = new Map(sortedStages.map((lane, index) => [lane.id, index]));
22246
+ const nodeLaneById = new Map(nodes.map((node) => [node.id, node.lane]));
22247
+ const requiredSurfaceGaps = new Array(sortedStages.length - 1).fill(STAGE_COLUMN_GAP);
22248
+ for (const edge of edges) {
22249
+ if (edge.routing !== "curve" || !edge.label && !edge.sub) continue;
22250
+ const fromIndex = stageIndexById.get(nodeLaneById.get(edge.from) ?? "");
22251
+ const toIndex = stageIndexById.get(nodeLaneById.get(edge.to) ?? "");
22252
+ if (fromIndex === void 0 || toIndex === void 0 || fromIndex === toIndex) continue;
22253
+ const lo = Math.min(fromIndex, toIndex);
22254
+ const hi = Math.max(fromIndex, toIndex);
22255
+ const labelWidth = computeLabelBoxW(edge.label, edge.sub, textMetrics);
22256
+ for (let pair = lo; pair < hi; pair++) {
22257
+ requiredSurfaceGaps[pair] = Math.max(requiredSurfaceGaps[pair], labelWidth);
22258
+ }
22259
+ }
22260
+ const shiftedXById2 = /* @__PURE__ */ new Map([[sortedStages[0].id, sortedStages[0].x]]);
22261
+ for (let index = 1; index < sortedStages.length; index++) {
22262
+ const previous = sortedStages[index - 1];
22263
+ const lane = sortedStages[index];
22264
+ const previousX = shiftedXById2.get(previous.id);
22265
+ const authoredGap = Math.max(0, lane.x - (previous.x + previous.width));
22266
+ const laneGap = Math.max(authoredGap, requiredSurfaceGaps[index - 1] - STAGE_COLUMN_GAP);
22267
+ shiftedXById2.set(lane.id, previousX + previous.width + laneGap);
22268
+ }
22269
+ return lanes.map((lane) => ({ ...lane, x: shiftedXById2.get(lane.id) ?? lane.x }));
22270
+ }
21984
22271
  const nodeById = new Map(nodes.map((n) => [n.id, n]));
21985
22272
  const sortedByX = [...layoutedLanes].sort((a, b) => a.x - b.x);
21986
22273
  const laneIndexById = new Map(sortedByX.map((lane, i) => [lane.id, i]));
@@ -22052,7 +22339,7 @@ function recomputeContainedLanes(lanes, nodes) {
22052
22339
  lifelineBottom = Math.max(lifelineBottom, bottom);
22053
22340
  }
22054
22341
  }
22055
- return lanes.map((lane) => {
22342
+ const contained = lanes.map((lane) => {
22056
22343
  if (lane.lifeline) {
22057
22344
  return {
22058
22345
  ...lane,
@@ -22070,6 +22357,7 @@ function recomputeContainedLanes(lanes, nodes) {
22070
22357
  height: bottom - top + CONTAIN_PAD * 2 + LANE_LABEL_H
22071
22358
  };
22072
22359
  });
22360
+ return equalizeStageColumns(contained, nodes);
22073
22361
  }
22074
22362
 
22075
22363
  // src/layout/head-label.ts
@@ -22219,11 +22507,15 @@ function requiredStackGapForEdgeLabels(diag, allStacks) {
22219
22507
  }
22220
22508
  function layoutNodes(diag, lanes, textMetrics) {
22221
22509
  const laneMap = new Map(lanes.map((l) => [l.id, l]));
22510
+ const metroDecisionIds = new Set(
22511
+ diag.edges.filter((edge) => edge.routing === "metro").flatMap((edge) => [edge.from, edge.to]).filter((id) => diag.nodes.some((node) => node.id === id && node.kind === "decision"))
22512
+ );
22513
+ const nodeSize = (node) => metroDecisionIds.has(node.id) ? { w: METRO_DECISION_WIDTH, h: METRO_DECISION_HEIGHT } : NODE_SIZE[node.kind];
22222
22514
  const allStacks = Array.from(new Set(diag.nodes.map((n) => n.stack))).sort((a, b) => a - b);
22223
22515
  const rowH = /* @__PURE__ */ new Map();
22224
22516
  for (const stack of allStacks) {
22225
22517
  const nodesAtStack = diag.nodes.filter((n) => n.stack === stack);
22226
- const h = Math.max(...nodesAtStack.map((n) => \u7BB1\u306E\u9AD8\u30552(n, NODE_SIZE[n.kind].h)));
22518
+ const h = Math.max(...nodesAtStack.map((n) => \u7BB1\u306E\u9AD8\u30552(n, nodeSize(n).h)));
22227
22519
  rowH.set(stack, h);
22228
22520
  }
22229
22521
  const rowCy = /* @__PURE__ */ new Map();
@@ -22259,7 +22551,7 @@ function layoutNodes(diag, lanes, textMetrics) {
22259
22551
  if (!lane) {
22260
22552
  throw new Error(`cdl layout: node "${n.id}" \u306E lane "${n.lane}" \u304C\u5B9A\u7FA9\u3055\u308C\u3066\u3044\u306A\u3044`);
22261
22553
  }
22262
- const size = NODE_SIZE[n.kind];
22554
+ const size = nodeSize(n);
22263
22555
  if (n.posX !== void 0 && n.posY !== void 0) {
22264
22556
  const w2 = n.posW ?? n.w ?? autoRowsWidth(n, size.w, textMetrics);
22265
22557
  const h2 = n.posH ?? n.h ?? rowH.get(n.stack) ?? size.h;
@@ -22382,6 +22674,9 @@ function dropUnsafeLaneDx(dxByLane, nodes) {
22382
22674
  function applyRowLevelNodeShifts(nodes, lanes, nodeShifts) {
22383
22675
  const dyByStack = aggregateRowDy(nodes, nodeShifts);
22384
22676
  const dxByLane = dropUnsafeLaneDx(aggregateLaneDx(nodes, nodeShifts), nodes);
22677
+ for (const lane of lanes) {
22678
+ if (lane.stage !== void 0) dxByLane.delete(lane.id);
22679
+ }
22385
22680
  let moved = false;
22386
22681
  const outNodes = nodes.map((n) => {
22387
22682
  const dx = dxByLane.get(n.lane) ?? 0;
@@ -22501,7 +22796,15 @@ function layout(diag) {
22501
22796
  );
22502
22797
  }
22503
22798
  }
22504
- const viewBox = computeViewBox(containedLanesFinal, nodesFinal, edges, bboxes, diag.viewport);
22799
+ const contentViewBox = computeViewBox(
22800
+ containedLanesFinal,
22801
+ nodesFinal,
22802
+ edges,
22803
+ bboxes,
22804
+ diag.viewport
22805
+ );
22806
+ const legendLayout = diag.legend?.length ? layoutLegend(diag.legend, contentViewBox, textMetrics, nodesFinal) : void 0;
22807
+ const viewBox = legendLayout?.viewBox ?? contentViewBox;
22505
22808
  return {
22506
22809
  id: diag.id,
22507
22810
  topic: diag.topic,
@@ -22528,6 +22831,7 @@ function layout(diag) {
22528
22831
  ...textMetrics === "monospace" ? { textMetrics: "monospace" } : {},
22529
22832
  // 種類も配色と同じく空の目印を出さない。 遮断をここに揃えると手書きの図にも効く
22530
22833
  ...diag.type ? { type: diag.type } : {},
22834
+ ...legendLayout ? { legend: legendLayout.legend } : {},
22531
22835
  bboxes,
22532
22836
  collisions,
22533
22837
  nearCollisions,
@@ -22716,6 +23020,11 @@ function validate(diag) {
22716
23020
  \u2192 \u4FEE\u6B63\u4F8B: .lane("${n.lane}", { x: 0, width: 400 }) \u3092 node \u5BA3\u8A00\u524D\u306B\u8FFD\u52A0`
22717
23021
  );
22718
23022
  }
23023
+ if (n.leaderTo !== void 0 && !nodeIds.has(n.leaderTo)) {
23024
+ errors.push(
23025
+ `[cdl] unknown-ref: node "${n.id}" \u306E leaderTo "${n.leaderTo}" \u304C node \u306B\u5B58\u5728\u3057\u307E\u305B\u3093${suggest(n.leaderTo, nodeIds, "node")}`
23026
+ );
23027
+ }
22719
23028
  }
22720
23029
  for (const e of diag.edges) {
22721
23030
  if (!nodeIds.has(e.from)) {
@@ -24551,6 +24860,10 @@ function uniformLifelineEndY(lanes, nodes, starts = lifelineStartYs(nodes)) {
24551
24860
 
24552
24861
  // src/render/edge-head.ts
24553
24862
  var EDGE_HEAD_MUTED_MARKER_ID = "cdl-arrow-muted";
24863
+ var METRO_RETURN_ARROW_D = "M 21 0 L 4 9.5 L 4 -9.5 Z";
24864
+ function metroReturnArrowMarkerId(tone) {
24865
+ return `cdl-metro-return-arrow-${tone}`;
24866
+ }
24554
24867
  var EDGE_HEAD_SHAPE = {
24555
24868
  // 何も描かない端。 marker 自体を出さないので `d` は読まれない
24556
24869
  none: { d: "", w: 10, h: 10, refX: 0, filled: false },
@@ -24605,6 +24918,7 @@ function \u7B49\u5E45\u306E\u5B57(text, fontSize) {
24605
24918
  });
24606
24919
  return out;
24607
24920
  }
24921
+ var METRO_PENDING_ATTRIBUTE = "data-cdl-pending";
24608
24922
  function defaultEdgeWidth(active) {
24609
24923
  return active ? 2.5 : 1.25;
24610
24924
  }
@@ -24623,12 +24937,14 @@ function CdlEdgeView({
24623
24937
  drawing,
24624
24938
  progress,
24625
24939
  stateValues,
24626
- flowing = false
24940
+ flowing = false,
24941
+ pending = false
24627
24942
  }) {
24628
24943
  const color = edgeColor(edge);
24629
- const marker = edgeHeadMarkerName(edge.tone, edge.head, !active, edge.headFill);
24630
- const tailMarker = edge.tailHead === void 0 ? void 0 : edgeHeadMarkerName(edge.tone, edge.tailHead, !active, edge.tailHeadFill);
24631
24944
  const style = edge.style ?? "solid";
24945
+ const isMetroReturn = edge.metroReturn === true;
24946
+ const marker = isMetroReturn ? metroReturnArrowMarkerId(edge.tone) : edgeHeadMarkerName(edge.tone, edge.head, !active, edge.headFill);
24947
+ const tailMarker = edge.tailHead === void 0 ? void 0 : edgeHeadMarkerName(edge.tone, edge.tailHead, !active, edge.tailHeadFill);
24632
24948
  const isDotted = style === "dotted-flow";
24633
24949
  const isPassThrough = isDotted && Boolean(edge.dThrough);
24634
24950
  const lineDotsD = isDotted ? shrinkPathEnd(edge.d, 12) : edge.d;
@@ -24638,6 +24954,7 @@ function CdlEdgeView({
24638
24954
  return /* @__PURE__ */ jsxRuntime.jsxs(
24639
24955
  "g",
24640
24956
  {
24957
+ ...pending ? { [METRO_PENDING_ATTRIBUTE]: "true" } : {},
24641
24958
  "data-cdl-edge": edge.id,
24642
24959
  "data-cdl-routing": edge.routing === "metro" ? "metro" : void 0,
24643
24960
  "data-cdl-from": edge.from,
@@ -24663,11 +24980,12 @@ function CdlEdgeView({
24663
24980
  fill: "none",
24664
24981
  stroke: color,
24665
24982
  strokeOpacity: active ? 0.95 : 0.9,
24666
- strokeWidth: active ? 5 : 4,
24983
+ strokeWidth: edge.routing === "metro" ? METRO_LINE_WIDTH : active ? 5 : 4,
24667
24984
  strokeDasharray: "0 18",
24668
24985
  strokeLinecap: "round",
24669
24986
  style: {
24670
- transition: "stroke 280ms ease, stroke-opacity 280ms ease, stroke-width 280ms ease"
24987
+ transition: "stroke 280ms ease, stroke-opacity 280ms ease, stroke-width 280ms ease",
24988
+ strokeWidth: edge.routing === "metro" ? METRO_LINE_WIDTH_CSS : void 0
24671
24989
  }
24672
24990
  }
24673
24991
  ),
@@ -24722,7 +25040,9 @@ function SolidEdgePath({
24722
25040
  const boundWidth = edge.widthBind && stateValues ? resolveNumericAttr(edge.widthBind, stateValues, defaultWidth) : defaultWidth;
24723
25041
  const boundStroke = edge.strokeBind && stateValues ? interpolate(edge.strokeBind, stateValues) : color;
24724
25042
  const boundDashOffset = edge.dashOffsetBind && stateValues ? resolveNumericAttr(edge.dashOffsetBind, stateValues, 0) : void 0;
24725
- const dashArray = dashed ? "10 8" : active ? boundDashOffset !== void 0 ? "8 6" : void 0 : "4 5";
25043
+ const strokeWidth = edge.routing === "metro" && edge.widthBind === void 0 ? edge.metroReturn ? 6 : METRO_LINE_WIDTH : boundWidth;
25044
+ const strokeWidthCss = edge.routing === "metro" && !edge.metroReturn ? edge.widthBind === void 0 ? METRO_LINE_WIDTH_CSS : `var(--cdl-metro-line-width, ${strokeWidth}px)` : void 0;
25045
+ const dashArray = dashed ? edge.metroReturn ? "0 9.6" : "10 8" : active ? boundDashOffset !== void 0 ? "8 6" : void 0 : "4 5";
24726
25046
  return /* @__PURE__ */ jsxRuntime.jsx(
24727
25047
  "path",
24728
25048
  {
@@ -24731,14 +25051,15 @@ function SolidEdgePath({
24731
25051
  fill: "none",
24732
25052
  stroke: boundStroke,
24733
25053
  strokeOpacity: active ? 0.95 : 0.9,
24734
- strokeWidth: boundWidth,
25054
+ strokeWidth,
24735
25055
  strokeDasharray: dashArray,
24736
25056
  strokeDashoffset: boundDashOffset,
24737
25057
  strokeLinecap: "round",
24738
25058
  markerEnd: marker === void 0 ? void 0 : `url(#${marker})`,
24739
25059
  markerStart: tailMarker === void 0 ? void 0 : `url(#${tailMarker})`,
24740
25060
  style: {
24741
- transition: "stroke 280ms ease, stroke-opacity 280ms ease, stroke-width 280ms ease"
25061
+ transition: "stroke 280ms ease, stroke-opacity 280ms ease, stroke-width 280ms ease",
25062
+ strokeWidth: strokeWidthCss
24742
25063
  }
24743
25064
  }
24744
25065
  );
@@ -25035,7 +25356,11 @@ function \u5272\u308B\u5019\u88DC(text) {
25035
25356
  function \u5165\u3063\u3066\u3044\u308B([\u5DE6, \u53F3]) {
25036
25357
  return \u5DE6 !== "" && \u53F3 !== "";
25037
25358
  }
25038
- function CardNode({ node, active }) {
25359
+ function CardNode({
25360
+ node,
25361
+ active,
25362
+ compactTitleInset
25363
+ }) {
25039
25364
  const accent = nodeAccent(node, "var(--cdl-tone-accent, #2d6a8f)");
25040
25365
  const x = node.cx - node.w / 2;
25041
25366
  const y = node.cy - node.h / 2;
@@ -25053,12 +25378,12 @@ function CardNode({ node, active }) {
25053
25378
  });
25054
25379
  const \u305A\u3089\u3057 = \u7E70\u308A\u4E0A\u3052 - \u4F59\u308A;
25055
25380
  const titleY = compact ? \u5C0F\u578B\u306B\u547C\u3073\u540D ? node.h / 2 - 2 : node.h / 2 + 8 : 86 - \u305A\u3089\u3057;
25056
- const titleX = compact ? node.w / 2 : 26;
25057
- const paddingX = compact ? 16 : 52;
25381
+ const titleX = compact ? compactTitleInset ?? node.w / 2 : 26;
25382
+ const paddingX = compact ? (compactTitleInset ?? 8) * 2 : 52;
25058
25383
  const availableW = node.w - paddingX;
25059
25384
  const initialTitleSize = compact ? 20 : 24;
25060
25385
  const titleSize = fitTextSize(node.title, availableW, initialTitleSize, 11);
25061
- const textAnchor = compact ? "middle" : "start";
25386
+ const textAnchor = compact && compactTitleInset === void 0 ? "middle" : "start";
25062
25387
  const subtitleY = 124 - \u305A\u3089\u3057;
25063
25388
  const \u6700\u5927\u306E\u884C\u9001\u308A = SUBTITLE_SIZE * \u884C\u9001\u308A\u306E\u500D\u7387;
25064
25389
  const \u4E8C\u884C\u304C\u5165\u308B = !compact && subtitleY - \u6700\u5927\u306E\u884C\u9001\u308A / 2 - SUBTITLE_SIZE * ASCENT > titleY + titleSize * DESCENT && subtitleY + \u6700\u5927\u306E\u884C\u9001\u308A / 2 + SUBTITLE_SIZE * DESCENT <= node.h - 4;
@@ -25821,7 +26146,9 @@ function GenericNode({
25821
26146
  value,
25822
26147
  stateValues,
25823
26148
  \u5024\u306E\u540D\u524D,
25824
- textMetrics = "proportional"
26149
+ textMetrics = "proportional",
26150
+ showKindIcon = true,
26151
+ metroDecision = false
25825
26152
  }) {
25826
26153
  const base = STYLES[node.kind] ?? FALLBACK;
25827
26154
  const toned = toneColor3(node.tone);
@@ -25852,14 +26179,14 @@ function GenericNode({
25852
26179
  if (shape === "diamond") {
25853
26180
  const cx = w / 2;
25854
26181
  const cy = h / 2;
25855
- const pad = 8;
26182
+ const pad = metroDecision ? 0 : 8;
25856
26183
  return /* @__PURE__ */ jsxRuntime.jsx(
25857
26184
  "path",
25858
26185
  {
25859
26186
  d: `M ${cx} ${pad} L ${w - pad} ${cy} L ${cx} ${h - pad} L ${pad} ${cy} Z`,
25860
26187
  fill: "var(--cdl-node-fill, #ffffff)",
25861
26188
  stroke: active ? style.accent : "var(--cdl-divider, #d4d4d8)",
25862
- strokeWidth: active ? 3 : 1.25
26189
+ strokeWidth: metroDecision ? 1.5 : active ? 3 : 1.25
25863
26190
  }
25864
26191
  );
25865
26192
  }
@@ -25923,11 +26250,11 @@ function GenericNode({
25923
26250
  const \u305A\u3089\u3057 = \u7E70\u308A\u4E0A\u3052 - \u4F59\u308A;
25924
26251
  const \u7D75\u306E\u82AF = centered && STYLES[node.kind]?.shape === "cloud" ? h * CLOUD_ART_CENTER_RISE : 0;
25925
26252
  const eyebrowY = centered ? h / 2 - 26 - \u7D75\u306E\u82AF : 32 + cylinderShift + \u4F59\u308A;
25926
- const titleY = genericTitleBaselineY(node.kind, h, hasRows) - \u305A\u3089\u3057 - \u7D75\u306E\u82AF;
26253
+ const titleY = metroDecision ? h / 2 + 9 : genericTitleBaselineY(node.kind, h, hasRows) - \u305A\u3089\u3057 - \u7D75\u306E\u82AF;
25927
26254
  const subtitleY = centered ? h / 2 + 28 - \u7D75\u306E\u82AF : 96 + cylinderShift - \u305A\u3089\u3057;
25928
26255
  const iconX = centered ? w / 2 + 40 : w - 48;
25929
26256
  const iconY = centered ? h / 2 - 44 - \u7D75\u306E\u82AF : 18 + cylinderShift;
25930
- const dominantBaseline = centered ? "middle" : void 0;
26257
+ const dominantBaseline = centered && !metroDecision ? "middle" : void 0;
25931
26258
  return /* @__PURE__ */ jsxRuntime.jsxs(
25932
26259
  "g",
25933
26260
  {
@@ -25936,12 +26263,12 @@ function GenericNode({
25936
26263
  "data-cdl-look": style.look,
25937
26264
  children: [
25938
26265
  shapeEl,
25939
- /* @__PURE__ */ jsxRuntime.jsxs("g", { transform: `translate(${iconX} ${iconY}) scale(1)`, "data-cdl-role": "node-kind-icon", children: [
26266
+ showKindIcon && /* @__PURE__ */ jsxRuntime.jsxs("g", { transform: `translate(${iconX} ${iconY}) scale(1)`, "data-cdl-role": "node-kind-icon", children: [
25940
26267
  /* @__PURE__ */ jsxRuntime.jsx("rect", { width: 24, height: 24, fill: "transparent" }),
25941
26268
  /* @__PURE__ */ jsxRuntime.jsx("path", { d: style.icon, fill: style.eyebrowColor, opacity: 0.7, transform: "scale(1)" })
25942
26269
  ] }),
25943
26270
  node.eyebrow && /* @__PURE__ */ jsxRuntime.jsx("text", { x: textX, y: eyebrowY, fontSize: 17, fontWeight: 700, letterSpacing: 1.5, fill: paint.onSolid ? "var(--cdl-node-fill, #ffffff)" : style.eyebrowColor, textAnchor, dominantBaseline, children: node.eyebrow }),
25944
- /* @__PURE__ */ jsxRuntime.jsx("text", { "data-cdl-role": "node-label", x: textX, y: titleY, fontSize: 20, fontWeight: 700, fill: paint.onSolid ? "var(--cdl-node-fill, #ffffff)" : "var(--cdl-text, #1a1f2a)", textAnchor, dominantBaseline, style: node.kind === "function" || node.kind === "api" ? { fontFamily: "'JetBrains Mono', monospace" } : void 0, children: node.kind === "function" || node.kind === "api" ? \u7B49\u5E45\u306E\u5B57(node.title, 20) : node.title }),
26271
+ /* @__PURE__ */ jsxRuntime.jsx("text", { "data-cdl-role": "node-label", x: textX, y: titleY, fontSize: metroDecision ? 26 : 20, fontWeight: 700, fill: paint.onSolid ? "var(--cdl-node-fill, #ffffff)" : "var(--cdl-text, #1a1f2a)", textAnchor, dominantBaseline, style: node.kind === "function" || node.kind === "api" ? { fontFamily: "'JetBrains Mono', monospace" } : void 0, children: node.kind === "function" || node.kind === "api" ? \u7B49\u5E45\u306E\u5B57(node.title, 20) : node.title }),
25945
26272
  node.subtitle && /* @__PURE__ */ jsxRuntime.jsx("text", { x: textX, y: subtitleY, fontSize: 17, fill: paint.onSolid ? "var(--cdl-node-fill, #ffffff)" : "var(--cdl-text-dim, #5a6270)", textAnchor, dominantBaseline, style: { fontFamily: "'JetBrains Mono', monospace" }, children: \u7B49\u5E45\u306E\u5B57(node.subtitle, 17) }),
25946
26273
  resolvedValue && /* @__PURE__ */ jsxRuntime.jsx(
25947
26274
  "text",
@@ -25990,50 +26317,176 @@ function GenericNode({
25990
26317
  var MARK_INNER_RATIO = 0.56;
25991
26318
  var RING_WIDTH_ACTIVE = 6;
25992
26319
  var RING_WIDTH_IDLE = 4;
26320
+ var METRO_START_RADIUS = 17;
26321
+ var METRO_END_RADIUS = 19;
26322
+ var METRO_END_RING_WIDTH = 3.5;
26323
+ var METRO_END_INNER_RADIUS = 10;
25993
26324
  var MARK_DEFAULT = "var(--cdl-tone-accent, #2d6a8f)";
25994
26325
  function markRadius(node) {
25995
26326
  return Math.min(node.w, node.h) / 2;
25996
26327
  }
25997
- function MarkStartNode({ node }) {
26328
+ function MarkStartNode({
26329
+ node,
26330
+ metro = false
26331
+ }) {
25998
26332
  const \u8272 = nodeAccent(node, MARK_DEFAULT);
25999
- return /* @__PURE__ */ jsxRuntime.jsx("g", { transform: `translate(${node.cx - node.w / 2} ${node.cy - node.h / 2})`, children: /* @__PURE__ */ jsxRuntime.jsx(
26000
- "circle",
26333
+ return /* @__PURE__ */ jsxRuntime.jsx(
26334
+ "g",
26001
26335
  {
26002
- "data-cdl-role": "node-body",
26003
- "data-cdl-mark": "start",
26004
- cx: node.w / 2,
26005
- cy: node.h / 2,
26006
- r: markRadius(node),
26007
- fill: \u8272
26336
+ "data-cdl-role": metro ? "mark-start" : void 0,
26337
+ transform: `translate(${node.cx - node.w / 2} ${node.cy - node.h / 2})`,
26338
+ children: /* @__PURE__ */ jsxRuntime.jsx(
26339
+ "circle",
26340
+ {
26341
+ "data-cdl-role": "node-body",
26342
+ "data-cdl-mark": "start",
26343
+ cx: node.w / 2,
26344
+ cy: node.h / 2,
26345
+ r: metro ? METRO_START_RADIUS : markRadius(node),
26346
+ fill: \u8272
26347
+ }
26348
+ )
26008
26349
  }
26009
- ) });
26350
+ );
26010
26351
  }
26011
- function MarkEndNode({ node, active }) {
26352
+ function MarkEndNode({
26353
+ node,
26354
+ active,
26355
+ metro = false
26356
+ }) {
26012
26357
  const \u8272 = nodeAccent(node, MARK_DEFAULT);
26013
- const r = markRadius(node);
26014
- const \u8F2A\u306E\u592A\u3055 = active ? RING_WIDTH_ACTIVE : RING_WIDTH_IDLE;
26015
- return /* @__PURE__ */ jsxRuntime.jsxs("g", { transform: `translate(${node.cx - node.w / 2} ${node.cy - node.h / 2})`, children: [
26358
+ const r = metro ? METRO_END_RADIUS : markRadius(node);
26359
+ const \u8F2A\u306E\u592A\u3055 = metro ? METRO_END_RING_WIDTH : active ? RING_WIDTH_ACTIVE : RING_WIDTH_IDLE;
26360
+ return /* @__PURE__ */ jsxRuntime.jsxs(
26361
+ "g",
26362
+ {
26363
+ "data-cdl-role": metro ? "mark-end" : void 0,
26364
+ transform: `translate(${node.cx - node.w / 2} ${node.cy - node.h / 2})`,
26365
+ children: [
26366
+ /* @__PURE__ */ jsxRuntime.jsx(
26367
+ "circle",
26368
+ {
26369
+ "data-cdl-role": "node-body",
26370
+ "data-cdl-mark": "end",
26371
+ cx: node.w / 2,
26372
+ cy: node.h / 2,
26373
+ r: metro ? r : Math.max(0, r - \u8F2A\u306E\u592A\u3055 / 2),
26374
+ fill: "var(--cdl-node-fill, #ffffff)",
26375
+ stroke: \u8272,
26376
+ strokeWidth: \u8F2A\u306E\u592A\u3055
26377
+ }
26378
+ ),
26379
+ /* @__PURE__ */ jsxRuntime.jsx(
26380
+ "circle",
26381
+ {
26382
+ "data-cdl-role": "node-inner",
26383
+ cx: node.w / 2,
26384
+ cy: node.h / 2,
26385
+ r: metro ? METRO_END_INNER_RADIUS : Math.max(0, r * MARK_INNER_RATIO),
26386
+ fill: \u8272
26387
+ }
26388
+ )
26389
+ ]
26390
+ }
26391
+ );
26392
+ }
26393
+ var METRO_STATION_RADIUS = 16;
26394
+ var METRO_STATION_STROKE_WIDTH = 6;
26395
+ var METRO_STATION_RADIUS_CSS = `var(--cdl-metro-station-radius, ${METRO_STATION_RADIUS}px)`;
26396
+ var METRO_STATION_STROKE_WIDTH_CSS = `var(--cdl-metro-station-stroke-width, ${METRO_STATION_STROKE_WIDTH}px)`;
26397
+ var STATION_DEFAULT = "var(--cdl-tone-accent, #2d6a8f)";
26398
+ var STATION_NAME_FONT_SIZE = 25;
26399
+ var STATION_NAME_GAP = 18;
26400
+ var STATION_NAME_RIGHT_GAP = 32;
26401
+ var STATION_NAME_RIGHT_BASELINE = 9;
26402
+ var STATION_SUBTITLE_FONT_SIZE = 18;
26403
+ var STATION_SUBTITLE_GAP = 22;
26404
+ function StationNode({
26405
+ node,
26406
+ nameRight = false
26407
+ }) {
26408
+ const name = node.title.trim();
26409
+ const subtitle = node.subtitle?.trim() ?? "";
26410
+ return /* @__PURE__ */ jsxRuntime.jsxs("g", { children: [
26016
26411
  /* @__PURE__ */ jsxRuntime.jsx(
26017
26412
  "circle",
26018
26413
  {
26019
26414
  "data-cdl-role": "node-body",
26020
- "data-cdl-mark": "end",
26021
- cx: node.w / 2,
26022
- cy: node.h / 2,
26023
- r: Math.max(0, r - \u8F2A\u306E\u592A\u3055 / 2),
26415
+ "data-cdl-mark": "station",
26416
+ cx: node.cx,
26417
+ cy: node.cy,
26418
+ r: METRO_STATION_RADIUS,
26024
26419
  fill: "var(--cdl-node-fill, #ffffff)",
26025
- stroke: \u8272,
26026
- strokeWidth: \u8F2A\u306E\u592A\u3055
26420
+ stroke: nodeAccent(node, STATION_DEFAULT),
26421
+ strokeWidth: METRO_STATION_STROKE_WIDTH,
26422
+ style: { r: METRO_STATION_RADIUS_CSS, strokeWidth: METRO_STATION_STROKE_WIDTH_CSS }
26027
26423
  }
26028
26424
  ),
26425
+ name !== "" && /* @__PURE__ */ jsxRuntime.jsx(
26426
+ "text",
26427
+ {
26428
+ "data-cdl-role": "shape-name",
26429
+ x: nameRight ? node.cx + STATION_NAME_RIGHT_GAP : node.cx,
26430
+ y: nameRight ? node.cy + STATION_NAME_RIGHT_BASELINE : node.cy - METRO_STATION_RADIUS - STATION_NAME_GAP,
26431
+ textAnchor: nameRight ? "start" : "middle",
26432
+ fontSize: STATION_NAME_FONT_SIZE,
26433
+ fontWeight: 700,
26434
+ fill: "var(--cdl-text, #23201b)",
26435
+ stroke: "var(--cdl-node-fill, #ffffff)",
26436
+ strokeWidth: 8,
26437
+ strokeLinejoin: "round",
26438
+ paintOrder: "stroke",
26439
+ children: name
26440
+ }
26441
+ ),
26442
+ subtitle !== "" && /* @__PURE__ */ jsxRuntime.jsx(
26443
+ "text",
26444
+ {
26445
+ "data-cdl-role": "shape-value",
26446
+ x: node.cx,
26447
+ y: node.cy + METRO_STATION_RADIUS + STATION_SUBTITLE_GAP,
26448
+ textAnchor: "middle",
26449
+ fontSize: STATION_SUBTITLE_FONT_SIZE,
26450
+ fill: "var(--cdl-text-dim, #5a6270)",
26451
+ children: subtitle
26452
+ }
26453
+ )
26454
+ ] });
26455
+ }
26456
+ function TimelineNumberNode({ node }) {
26457
+ const radius = Math.max(0, Math.min(node.w, node.h) / 2 - TIMELINE_NUMBER_STROKE / 2);
26458
+ const fontSize = fitTextSize(
26459
+ node.title,
26460
+ titleUsableWidth(node.kind, node.w, node.h),
26461
+ 18,
26462
+ MIN_TEXT_SIZE
26463
+ );
26464
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
26029
26465
  /* @__PURE__ */ jsxRuntime.jsx(
26030
26466
  "circle",
26031
26467
  {
26032
- "data-cdl-role": "node-inner",
26033
- cx: node.w / 2,
26034
- cy: node.h / 2,
26035
- r: Math.max(0, r * MARK_INNER_RATIO),
26036
- fill: \u8272
26468
+ "data-cdl-role": "node-body",
26469
+ "data-cdl-mark": "timeline-number",
26470
+ cx: node.cx,
26471
+ cy: node.cy,
26472
+ r: radius,
26473
+ fill: "var(--cdl-node-fill, #ffffff)",
26474
+ stroke: nodeAccent(node, STATION_DEFAULT),
26475
+ strokeWidth: TIMELINE_NUMBER_STROKE
26476
+ }
26477
+ ),
26478
+ /* @__PURE__ */ jsxRuntime.jsx(
26479
+ "text",
26480
+ {
26481
+ "data-cdl-role": "node-label",
26482
+ x: node.cx,
26483
+ y: node.cy,
26484
+ textAnchor: "middle",
26485
+ dominantBaseline: "central",
26486
+ fontSize,
26487
+ fontWeight: 700,
26488
+ fill: "var(--cdl-text, #1a1f2a)",
26489
+ children: node.title
26037
26490
  }
26038
26491
  )
26039
26492
  ] });
@@ -26365,27 +26818,6 @@ function interpolateColor(template, stateValues, fallback) {
26365
26818
  if (!template) return fallback;
26366
26819
  return interpolate(template, stateValues);
26367
26820
  }
26368
- var STATION_STROKE_ACTIVE = 4;
26369
- var STATION_STROKE_IDLE = 3;
26370
- var STATION_DEFAULT = "var(--cdl-tone-accent, #2d6a8f)";
26371
- function StationNode({ node, active }) {
26372
- const strokeWidth = active ? STATION_STROKE_ACTIVE : STATION_STROKE_IDLE;
26373
- const radius = Math.max(0, Math.min(node.w, node.h) / 2 - strokeWidth / 2);
26374
- const \u5F62 = /* @__PURE__ */ jsxRuntime.jsx(
26375
- "circle",
26376
- {
26377
- "data-cdl-role": "node-body",
26378
- "data-cdl-mark": "station",
26379
- cx: node.cx,
26380
- cy: node.cy,
26381
- r: radius,
26382
- fill: "var(--cdl-node-fill, #ffffff)",
26383
- stroke: nodeAccent(node, STATION_DEFAULT),
26384
- strokeWidth
26385
- }
26386
- );
26387
- return renderShapeWithOutsideLabels(\u5F62, node, "middle");
26388
- }
26389
26821
 
26390
26822
  // src/kinds/label-stack.ts
26391
26823
  function \u540D\u672D\u306E\u6BB5\u3092\u307B\u3069\u304F(\u771F\u306Ey, \u9593\u9694, \u4E0A\u9650, \u4E0B\u9650) {
@@ -33547,10 +33979,14 @@ function CdlNodeView({
33547
33979
  stateValues,
33548
33980
  currentPhaseId,
33549
33981
  chartLineScale,
33982
+ compactTitleInset,
33550
33983
  \u95A2\u4FC2\u306E\u53D7\u3051\u53E3,
33551
33984
  lang,
33552
33985
  textMetrics,
33553
- \u5024\u306E\u540D\u524D
33986
+ \u5024\u306E\u540D\u524D,
33987
+ metroDiagram = false,
33988
+ metroDecision = false,
33989
+ stationNameRight = false
33554
33990
  }) {
33555
33991
  const value = node.value ? interpolate(node.value, stateValues, \u5024\u306E\u540D\u524D) : void 0;
33556
33992
  const rows = (node.rows ?? []).map((r) => interpolate(r, stateValues, \u5024\u306E\u540D\u524D));
@@ -33591,7 +34027,7 @@ function CdlNodeView({
33591
34027
  case "event":
33592
34028
  return /* @__PURE__ */ jsxRuntime.jsx(EventNode, { node: resolvedNode, active, progress });
33593
34029
  case "card":
33594
- return /* @__PURE__ */ jsxRuntime.jsx(CardNode, { node: resolvedNode, active });
34030
+ return /* @__PURE__ */ jsxRuntime.jsx(CardNode, { node: resolvedNode, active, compactTitleInset });
33595
34031
  // 時系列のやり取り (#578)。 1 つの箱が図を丸ごと描く = 縦が時間で、参加者は箱を持たない
33596
34032
  case "sequence-board":
33597
34033
  return /* @__PURE__ */ jsxRuntime.jsx(
@@ -33878,11 +34314,13 @@ function CdlNodeView({
33878
34314
  case "shape-customer-service":
33879
34315
  return /* @__PURE__ */ jsxRuntime.jsx(ShapeCustomerServiceNode, { node: resolvedNode, active });
33880
34316
  case "mark-start":
33881
- return /* @__PURE__ */ jsxRuntime.jsx(MarkStartNode, { node: resolvedNode, active });
34317
+ return /* @__PURE__ */ jsxRuntime.jsx(MarkStartNode, { node: resolvedNode, active, metro: metroDiagram });
33882
34318
  case "mark-end":
33883
- return /* @__PURE__ */ jsxRuntime.jsx(MarkEndNode, { node: resolvedNode, active });
34319
+ return /* @__PURE__ */ jsxRuntime.jsx(MarkEndNode, { node: resolvedNode, active, metro: metroDiagram });
33884
34320
  case "station":
33885
- return /* @__PURE__ */ jsxRuntime.jsx(StationNode, { node: resolvedNode, active });
34321
+ return /* @__PURE__ */ jsxRuntime.jsx(StationNode, { node: resolvedNode, active, nameRight: stationNameRight });
34322
+ case "timeline-number":
34323
+ return /* @__PURE__ */ jsxRuntime.jsx(TimelineNumberNode, { node: resolvedNode });
33886
34324
  default:
33887
34325
  return /* @__PURE__ */ jsxRuntime.jsx(
33888
34326
  GenericNode,
@@ -33892,7 +34330,9 @@ function CdlNodeView({
33892
34330
  value: node.value,
33893
34331
  stateValues,
33894
34332
  \u5024\u306E\u540D\u524D,
33895
- textMetrics
34333
+ textMetrics,
34334
+ showKindIcon: !(metroDiagram && node.kind === "decision"),
34335
+ metroDecision
33896
34336
  }
33897
34337
  );
33898
34338
  }
@@ -34006,6 +34446,112 @@ function \u7BB1\u306E\u4E0A\u306E\u8A9E(node, textMetrics = "proportional") {
34006
34446
  )
34007
34447
  ] });
34008
34448
  }
34449
+ var ink = "var(--cdl-text-accent, #143a52)";
34450
+ var paper = "var(--cdl-stage-bg, #f8fafc)";
34451
+ var LEGEND_MARK_RENDERERS = {
34452
+ diamond: () => /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M 23 -26 L 46 -8 L 23 10 L 0 -8 Z", fill: "none", stroke: ink, strokeWidth: 2 }),
34453
+ "filled-circle": () => /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: 23, cy: -8, r: 11, fill: ink }),
34454
+ "double-circle": () => /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
34455
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: 23, cy: -8, r: 12, fill: "none", stroke: ink, strokeWidth: 2.5 }),
34456
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: 23, cy: -8, r: 6, fill: ink })
34457
+ ] }),
34458
+ "dotted-line": () => /* @__PURE__ */ jsxRuntime.jsx(
34459
+ "path",
34460
+ {
34461
+ d: `M 0 -8 H ${LEGEND_MARK_REFERENCE_WIDTH}`,
34462
+ fill: "none",
34463
+ stroke: ink,
34464
+ strokeWidth: 6,
34465
+ strokeLinecap: "round",
34466
+ strokeDasharray: "0 9.6"
34467
+ }
34468
+ ),
34469
+ "solid-line": () => /* @__PURE__ */ jsxRuntime.jsx(
34470
+ "path",
34471
+ {
34472
+ d: `M 0 -8 H ${LEGEND_MARK_REFERENCE_WIDTH}`,
34473
+ fill: "none",
34474
+ stroke: ink,
34475
+ strokeWidth: 4,
34476
+ strokeLinecap: "round"
34477
+ }
34478
+ ),
34479
+ "curved-line": () => /* @__PURE__ */ jsxRuntime.jsx(
34480
+ "path",
34481
+ {
34482
+ d: `M 0 6 C 12 -22, 34 -22, ${LEGEND_MARK_REFERENCE_WIDTH} 6`,
34483
+ fill: "none",
34484
+ stroke: ink,
34485
+ strokeWidth: 4,
34486
+ strokeLinecap: "round"
34487
+ }
34488
+ ),
34489
+ "rounded-label": () => /* @__PURE__ */ jsxRuntime.jsx(
34490
+ "rect",
34491
+ {
34492
+ x: 0,
34493
+ y: -23,
34494
+ width: LEGEND_MARK_REFERENCE_WIDTH,
34495
+ height: 30,
34496
+ rx: 9,
34497
+ fill: paper,
34498
+ stroke: ink,
34499
+ strokeWidth: 2
34500
+ }
34501
+ ),
34502
+ "numbered-circle": () => /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
34503
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: 23, cy: -8, r: 13, fill: paper, stroke: ink, strokeWidth: 2.5 }),
34504
+ /* @__PURE__ */ jsxRuntime.jsx("text", { x: 23, y: -2, textAnchor: "middle", fontSize: 17, fontWeight: 700, fill: ink, children: "1" })
34505
+ ] }),
34506
+ station: () => /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: 23, cy: -8, r: 11, fill: paper, stroke: ink, strokeWidth: 4 }),
34507
+ arrow: () => /* @__PURE__ */ jsxRuntime.jsx(
34508
+ "path",
34509
+ {
34510
+ d: `M 0 -8 H 42 M 32 -18 L ${LEGEND_MARK_REFERENCE_WIDTH} -8 L 32 2`,
34511
+ fill: "none",
34512
+ stroke: ink,
34513
+ strokeWidth: 3,
34514
+ strokeLinecap: "round",
34515
+ strokeLinejoin: "round"
34516
+ }
34517
+ )
34518
+ };
34519
+ function CdlLegend({ items }) {
34520
+ return /* @__PURE__ */ jsxRuntime.jsx("g", { "data-cdl-role": "legend", children: items.map((item, index) => {
34521
+ const Mark = LEGEND_MARK_RENDERERS[item.mark];
34522
+ return /* @__PURE__ */ jsxRuntime.jsxs(
34523
+ "g",
34524
+ {
34525
+ "data-cdl-role": "legend-item",
34526
+ "data-cdl-y": item.y,
34527
+ transform: `translate(${item.x} ${item.y})`,
34528
+ children: [
34529
+ /* @__PURE__ */ jsxRuntime.jsx(
34530
+ "g",
34531
+ {
34532
+ "data-cdl-role": "legend-mark",
34533
+ "data-cdl-legend-mark": item.mark,
34534
+ transform: `scale(${LEGEND_MARK_SCALE})`,
34535
+ children: /* @__PURE__ */ jsxRuntime.jsx(Mark, {})
34536
+ }
34537
+ ),
34538
+ /* @__PURE__ */ jsxRuntime.jsx(
34539
+ "text",
34540
+ {
34541
+ "data-cdl-role": "legend-text",
34542
+ x: LEGEND_MARK_WIDTH + LEGEND_MARK_TEXT_GAP,
34543
+ y: 0,
34544
+ fontSize: LEGEND_FONT_SIZE,
34545
+ fill: "var(--cdl-text-mute, #4d7187)",
34546
+ children: item.text
34547
+ }
34548
+ )
34549
+ ]
34550
+ },
34551
+ `${item.mark}-${index}`
34552
+ );
34553
+ }) });
34554
+ }
34009
34555
 
34010
34556
  // src/render/relation-focus.ts
34011
34557
  function relatedFocusSet(edges, nodeId) {
@@ -34018,10 +34564,17 @@ function relatedFocusSet(edges, nodeId) {
34018
34564
  }
34019
34565
  return \u96C6\u5408;
34020
34566
  }
34567
+ var METRO_LANE_BADGE_WIDTH = 200;
34568
+ var METRO_LANE_BADGE_HEIGHT = 88;
34569
+ var METRO_LANE_BADGE_INSET = 24;
34570
+ var METRO_LANE_GUIDE_GAP = 24;
34021
34571
  function \u63CF\u304F\u3082\u306E\u304C\u3042\u308B(laid) {
34572
+ if (laid.legend && laid.legend.length > 0) return true;
34022
34573
  if (laid.edges.length > 0) return true;
34023
34574
  if (laid.nodes.some((n) => isRenderedNode(n))) return true;
34024
- return laid.lanes.some((l) => l.contain === true || l.lifeline === true || Boolean(l.label));
34575
+ return laid.lanes.some(
34576
+ (l) => l.contain === true || l.lifeline === true || Boolean(l.label) || l.stage !== void 0 || l.metro !== void 0
34577
+ );
34025
34578
  }
34026
34579
  function CdlStage({
34027
34580
  laid,
@@ -34062,6 +34615,7 @@ function CdlStage({
34062
34615
  [\u95A2\u4FC2\u3092\u5149\u3089\u305B\u308B, \u7126\u70B9, laid.edges]
34063
34616
  );
34064
34617
  const activeSet = \u7126\u70B9\u306E\u96C6\u5408 ?? new Set(currentPhase?.activate ?? []);
34618
+ const \u8DEF\u7DDA\u56F3 = laid.edges.some((edge) => edge.routing === "metro") && laid.nodes.some((node) => node.kind === "station");
34065
34619
  const \u7BB1\u306E\u53D7\u3051\u53E3 = \u95A2\u4FC2\u3092\u5149\u3089\u305B\u308B ? { \u89E6\u308C\u305F, \u96E2\u308C\u305F, \u62BC\u3057\u305F, \u7126\u70B9: \u7126\u70B9?.id ?? null } : void 0;
34066
34620
  const \u77E2\u5370\u306E\u51FA\u756A = react.useMemo(() => {
34067
34621
  const \u521D\u51FA = /* @__PURE__ */ new Map();
@@ -34090,9 +34644,15 @@ function CdlStage({
34090
34644
  const first = \u521D\u51FA.get(edgeId);
34091
34645
  if (first === void 0) return false;
34092
34646
  return \u4ECA === first;
34647
+ },
34648
+ /** 初出の段より前にある区間だけを、通過前として選べるようにする。 */
34649
+ \u307E\u3060\u304B: (edgeId) => {
34650
+ const first = \u521D\u51FA.get(edgeId);
34651
+ return first !== void 0 && \u4ECA >= 0 && \u4ECA < first;
34093
34652
  }
34094
34653
  };
34095
34654
  }, [laid.phases, currentPhase, laid.edgeReveal]);
34655
+ const \u7DDA\u3092\u63CF\u304F = (edge) => edge.routing === "metro" || \u77E2\u5370\u306E\u51FA\u756A.\u51FA\u308B\u304B(edge.id);
34096
34656
  const drawSet = new Set(currentPhase?.draw ?? []);
34097
34657
  const chartLineScales = react.useMemo(() => {
34098
34658
  const nodes = laid.nodes.filter((node) => node.kind === "chart-line");
@@ -34180,7 +34740,53 @@ function CdlStage({
34180
34740
  )
34181
34741
  );
34182
34742
  }, [laid]);
34743
+ const metroReturnArrowMarkers = react.useMemo(() => {
34744
+ const tones = new Set(
34745
+ laid.edges.filter((edge) => edge.metroReturn === true).map((edge) => edge.tone)
34746
+ );
34747
+ return [...tones].map((tone) => /* @__PURE__ */ jsxRuntime.jsx(
34748
+ "marker",
34749
+ {
34750
+ id: metroReturnArrowMarkerId(tone),
34751
+ viewBox: "0 -9.5 21 19",
34752
+ refX: "0",
34753
+ refY: "0",
34754
+ markerWidth: "21",
34755
+ markerHeight: "19",
34756
+ markerUnits: "userSpaceOnUse",
34757
+ orient: "auto",
34758
+ children: /* @__PURE__ */ jsxRuntime.jsx(
34759
+ "path",
34760
+ {
34761
+ "data-cdl-role": "edge-arrowhead",
34762
+ "data-cdl-mark": "metro-return-arrow",
34763
+ d: METRO_RETURN_ARROW_D,
34764
+ fill: TONE[tone]
34765
+ }
34766
+ )
34767
+ },
34768
+ tone
34769
+ ));
34770
+ }, [laid.edges]);
34183
34771
  const diagramScale = laid.diagramScale ?? 1;
34772
+ const stageLaneIds = new Set(
34773
+ laid.lanes.filter((lane) => lane.stage !== void 0).map((lane) => lane.id)
34774
+ );
34775
+ const stageNoteNodes = laid.nodes.filter(
34776
+ (node) => node.kind === "card" && (stageLaneIds.has(node.lane) || node.leaderTo !== void 0) && isRenderedNode(node) && Boolean(node.subtitle)
34777
+ );
34778
+ const stageNoteNodeIds = new Set(stageNoteNodes.map((node) => node.id));
34779
+ const nodesById = new Map(laid.nodes.map((node) => [node.id, node]));
34780
+ const metroDecisionIds = /* @__PURE__ */ new Set();
34781
+ const stationNamesRight = /* @__PURE__ */ new Set();
34782
+ for (const edge of laid.edges) {
34783
+ if (edge.routing !== "metro") continue;
34784
+ if (nodesById.get(edge.from)?.kind === "decision") metroDecisionIds.add(edge.from);
34785
+ if (nodesById.get(edge.to)?.kind === "decision") metroDecisionIds.add(edge.to);
34786
+ if (edge.toSide === "top" && nodesById.get(edge.to)?.kind === "station") {
34787
+ stationNamesRight.add(edge.to);
34788
+ }
34789
+ }
34184
34790
  if (!\u63CF\u304F\u3082\u306E\u304C\u3042\u308B(laid)) return null;
34185
34791
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: compact ? "px-3 py-3" : "px-6 py-6", style: compact ? void 0 : { minHeight: 460 }, children: [
34186
34792
  /* @__PURE__ */ jsxRuntime.jsxs(
@@ -34200,6 +34806,7 @@ function CdlStage({
34200
34806
  children: [
34201
34807
  /* @__PURE__ */ jsxRuntime.jsxs("defs", { children: [
34202
34808
  edgeHeadMarkers,
34809
+ metroReturnArrowMarkers,
34203
34810
  /* @__PURE__ */ jsxRuntime.jsx(
34204
34811
  "marker",
34205
34812
  {
@@ -34236,7 +34843,61 @@ function CdlStage({
34236
34843
  pointerEvents: "none"
34237
34844
  }
34238
34845
  ),
34239
- lane.contain && /* @__PURE__ */ jsxRuntime.jsx(
34846
+ lane.stage && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
34847
+ /* @__PURE__ */ jsxRuntime.jsx(
34848
+ "rect",
34849
+ {
34850
+ "data-cdl-role": "stage-column",
34851
+ x: lane.x + STAGE_COLUMN_GAP / 2,
34852
+ y: lane.y,
34853
+ width: lane.width - STAGE_COLUMN_GAP,
34854
+ height: lane.height,
34855
+ rx: 16,
34856
+ fill: "var(--cdl-stage-bg, #fafafa)",
34857
+ fillOpacity: 0.5,
34858
+ stroke: "var(--cdl-text-mute, #8a8678)",
34859
+ strokeWidth: 1.5
34860
+ }
34861
+ ),
34862
+ /* @__PURE__ */ jsxRuntime.jsx(
34863
+ "text",
34864
+ {
34865
+ "data-cdl-role": "stage-number",
34866
+ x: lane.x + STAGE_COLUMN_GAP / 2 + 20,
34867
+ y: lane.y + 24,
34868
+ fontSize: 14,
34869
+ fontWeight: 500,
34870
+ letterSpacing: 1.5,
34871
+ fill: "var(--cdl-text-mute, #8a8678)",
34872
+ children: `\u6BB5\u968E ${lane.stage.number}`
34873
+ }
34874
+ ),
34875
+ /* @__PURE__ */ jsxRuntime.jsx(
34876
+ "text",
34877
+ {
34878
+ "data-cdl-role": "stage-name",
34879
+ x: lane.x + STAGE_COLUMN_GAP / 2 + 20,
34880
+ y: lane.y + 55,
34881
+ fontSize: 22,
34882
+ fontWeight: 700,
34883
+ fill: "var(--cdl-text, #1a1f2a)",
34884
+ children: lane.stage.name
34885
+ }
34886
+ ),
34887
+ /* @__PURE__ */ jsxRuntime.jsx(
34888
+ "line",
34889
+ {
34890
+ "data-cdl-role": "stage-divider",
34891
+ x1: lane.x + STAGE_COLUMN_GAP / 2,
34892
+ y1: lane.y + 72,
34893
+ x2: lane.x + lane.width - STAGE_COLUMN_GAP / 2,
34894
+ y2: lane.y + 72,
34895
+ stroke: "var(--cdl-divider, #d4d4d8)",
34896
+ strokeWidth: 1.5
34897
+ }
34898
+ )
34899
+ ] }),
34900
+ lane.contain && lane.stage === void 0 && /* @__PURE__ */ jsxRuntime.jsx(
34240
34901
  "rect",
34241
34902
  {
34242
34903
  "data-cdl-role": "lane-container",
@@ -34272,7 +34933,85 @@ function CdlStage({
34272
34933
  }
34273
34934
  );
34274
34935
  })(),
34275
- lane.label && /* @__PURE__ */ jsxRuntime.jsx(
34936
+ lane.timelineAxis && (() => {
34937
+ const ys = laid.nodes.filter((node) => node.lane === lane.id && isRenderedNode(node)).map((node) => node.cy);
34938
+ if (ys.length < 2) return null;
34939
+ const y1 = Math.min(...ys);
34940
+ const y2 = Math.max(...ys);
34941
+ if (y2 <= y1) return null;
34942
+ return /* @__PURE__ */ jsxRuntime.jsx(
34943
+ "line",
34944
+ {
34945
+ "data-cdl-role": "timeline-axis",
34946
+ x1: lane.x + lane.width / 2,
34947
+ y1,
34948
+ x2: lane.x + lane.width / 2,
34949
+ y2,
34950
+ stroke: "var(--cdl-tone-accent, #2d6a8f)",
34951
+ strokeWidth: TIMELINE_NUMBER_STROKE
34952
+ }
34953
+ );
34954
+ })(),
34955
+ lane.metro !== void 0 && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
34956
+ /* @__PURE__ */ jsxRuntime.jsx(
34957
+ "line",
34958
+ {
34959
+ "data-cdl-role": "metro-lane-guide",
34960
+ x1: lane.x + METRO_LANE_BADGE_INSET + METRO_LANE_BADGE_WIDTH + METRO_LANE_GUIDE_GAP,
34961
+ y1: lane.y + lane.height / 2,
34962
+ x2: lane.x + lane.width - METRO_LANE_BADGE_INSET,
34963
+ y2: lane.y + lane.height / 2,
34964
+ stroke: "var(--cdl-line-mute, #a89d8f)",
34965
+ strokeWidth: 2,
34966
+ strokeDasharray: "22 7 5 7",
34967
+ opacity: 0.45
34968
+ }
34969
+ ),
34970
+ /* @__PURE__ */ jsxRuntime.jsxs(
34971
+ "g",
34972
+ {
34973
+ "data-cdl-role": "metro-lane-badge",
34974
+ transform: `translate(${lane.x + METRO_LANE_BADGE_INSET} ${lane.y + lane.height / 2 - METRO_LANE_BADGE_HEIGHT / 2})`,
34975
+ children: [
34976
+ /* @__PURE__ */ jsxRuntime.jsx(
34977
+ "rect",
34978
+ {
34979
+ width: METRO_LANE_BADGE_WIDTH,
34980
+ height: METRO_LANE_BADGE_HEIGHT,
34981
+ rx: 12,
34982
+ fill: "var(--cdl-node-fill, #ffffff)",
34983
+ stroke: "var(--cdl-tone-accent, #2d6a8f)",
34984
+ strokeWidth: 1.25
34985
+ }
34986
+ ),
34987
+ /* @__PURE__ */ jsxRuntime.jsx(
34988
+ "text",
34989
+ {
34990
+ x: METRO_LANE_BADGE_WIDTH / 2,
34991
+ y: lane.metro.subtitle ? 37 : 53,
34992
+ fontSize: 26,
34993
+ fontWeight: 700,
34994
+ fill: "var(--cdl-text, #1a1f2a)",
34995
+ textAnchor: "middle",
34996
+ children: lane.label
34997
+ }
34998
+ ),
34999
+ lane.metro.subtitle && /* @__PURE__ */ jsxRuntime.jsx(
35000
+ "text",
35001
+ {
35002
+ x: METRO_LANE_BADGE_WIDTH / 2,
35003
+ y: 67,
35004
+ fontSize: 18,
35005
+ fill: "var(--cdl-text-dim, #5a6270)",
35006
+ textAnchor: "middle",
35007
+ children: lane.metro.subtitle
35008
+ }
35009
+ )
35010
+ ]
35011
+ }
35012
+ )
35013
+ ] }),
35014
+ lane.label && lane.metro === void 0 && /* @__PURE__ */ jsxRuntime.jsx(
34276
35015
  "text",
34277
35016
  {
34278
35017
  "data-cdl-role": "lane-label",
@@ -34289,7 +35028,34 @@ function CdlStage({
34289
35028
  },
34290
35029
  lane.id
34291
35030
  )),
34292
- laid.edges.filter((edge) => \u77E2\u5370\u306E\u51FA\u756A.\u51FA\u308B\u304B(edge.id)).map((edge) => /* @__PURE__ */ jsxRuntime.jsx(
35031
+ laid.nodes.map((node) => {
35032
+ if (node.leaderTo === void 0) return null;
35033
+ const target = nodesById.get(node.leaderTo);
35034
+ if (target === void 0) return null;
35035
+ if (!isRenderedNode(node) || !isRenderedNode(target)) return null;
35036
+ const targetIsRight = target.cx >= node.cx;
35037
+ const x1 = node.cx + (targetIsRight ? node.w / 2 : -node.w / 2);
35038
+ const x2 = target.cx + (targetIsRight ? -target.w / 2 : target.w / 2);
35039
+ if (targetIsRight ? x2 <= x1 : x1 <= x2) return null;
35040
+ return /* @__PURE__ */ jsxRuntime.jsx(
35041
+ "line",
35042
+ {
35043
+ "data-cdl-role": "timeline-leader",
35044
+ "data-cdl-leader-from": node.id,
35045
+ "data-cdl-leader-to": node.leaderTo,
35046
+ x1,
35047
+ y1: node.cy,
35048
+ x2,
35049
+ y2: target.cy,
35050
+ stroke: "var(--cdl-line-mute, #a89d8f)",
35051
+ strokeWidth: 1.5,
35052
+ strokeDasharray: "2 4",
35053
+ strokeLinecap: "round"
35054
+ },
35055
+ `timeline-leader-${node.id}`
35056
+ );
35057
+ }),
35058
+ laid.edges.filter(\u7DDA\u3092\u63CF\u304F).map((edge) => /* @__PURE__ */ jsxRuntime.jsx(
34293
35059
  CdlEdgeView,
34294
35060
  {
34295
35061
  edge,
@@ -34297,16 +35063,19 @@ function CdlStage({
34297
35063
  flowing: \u7126\u70B9\u306E\u96C6\u5408?.has(edge.id) === true && !\u52D5\u304D\u3092\u63A7\u3048\u308B,
34298
35064
  drawing: \u77E2\u5370\u306E\u51FA\u756A.\u4F38\u3070\u3059\u304B(edge.id),
34299
35065
  progress: \u77E2\u5370\u306E\u51FA\u756A.\u4F38\u3070\u3059\u304B(edge.id) ? \u63CF\u304F\u9032\u307F : progress,
34300
- stateValues
35066
+ stateValues,
35067
+ pending: edge.routing === "metro" && \u77E2\u5370\u306E\u51FA\u756A.\u307E\u3060\u304B(edge.id)
34301
35068
  },
34302
35069
  edge.id
34303
35070
  )),
34304
35071
  laid.nodes.map((node) => {
34305
35072
  const \u63CF\u304F = drawSet.has(node.id);
35073
+ const \u6BB5\u968E\u306E\u88DC\u8DB3 = stageNoteNodeIds.has(node.id);
34306
35074
  const view = /* @__PURE__ */ jsxRuntime.jsx(
34307
35075
  CdlNodeView,
34308
35076
  {
34309
- node,
35077
+ node: \u6BB5\u968E\u306E\u88DC\u8DB3 ? { ...node, subtitle: void 0 } : node,
35078
+ compactTitleInset: \u6BB5\u968E\u306E\u88DC\u8DB3 ? STAGE_CARD_TEXT_INSET : void 0,
34310
35079
  active: activeSet.has(node.id),
34311
35080
  lang: laid.lang,
34312
35081
  textMetrics: laid.textMetrics,
@@ -34316,13 +35085,32 @@ function CdlStage({
34316
35085
  stateValues,
34317
35086
  \u5024\u306E\u540D\u524D,
34318
35087
  currentPhaseId: currentPhase?.id,
34319
- chartLineScale: chartLineScales.get(node.id)
35088
+ chartLineScale: chartLineScales.get(node.id),
35089
+ metroDiagram: \u8DEF\u7DDA\u56F3,
35090
+ metroDecision: metroDecisionIds.has(node.id),
35091
+ stationNameRight: stationNamesRight.has(node.id)
34320
35092
  }
34321
35093
  );
34322
35094
  return /* @__PURE__ */ jsxRuntime.jsx("g", { children: view }, node.id);
34323
35095
  }),
34324
- laid.edges.filter((edge) => Boolean(edge.dThrough) && \u77E2\u5370\u306E\u51FA\u756A.\u51FA\u308B\u304B(edge.id)).map((edge) => /* @__PURE__ */ jsxRuntime.jsx(CdlEdgeGlowView, { edge, active: activeSet.has(edge.id) }, `glow-${edge.id}`)),
34325
- laid.edges.filter((edge) => \u77E2\u5370\u306E\u51FA\u756A.\u51FA\u308B\u304B(edge.id)).map((edge) => /* @__PURE__ */ jsxRuntime.jsx(
35096
+ stageNoteNodes.map((node) => /* @__PURE__ */ jsxRuntime.jsx(
35097
+ "text",
35098
+ {
35099
+ "data-cdl-role": "stage-note",
35100
+ "data-cdl-stage-node": node.id,
35101
+ x: node.cx + node.w / 2 - STAGE_CARD_TEXT_INSET,
35102
+ y: node.cy + 1,
35103
+ fontSize: 14,
35104
+ fontWeight: 500,
35105
+ textAnchor: "end",
35106
+ dominantBaseline: "middle",
35107
+ fill: "var(--cdl-text-mute, #8a8678)",
35108
+ children: interpolate(node.subtitle, stateValues, \u5024\u306E\u540D\u524D)
35109
+ },
35110
+ `stage-note-${node.id}`
35111
+ )),
35112
+ laid.edges.filter((edge) => Boolean(edge.dThrough) && \u7DDA\u3092\u63CF\u304F(edge)).map((edge) => /* @__PURE__ */ jsxRuntime.jsx(CdlEdgeGlowView, { edge, active: activeSet.has(edge.id) }, `glow-${edge.id}`)),
35113
+ laid.edges.filter(\u7DDA\u3092\u63CF\u304F).map((edge) => /* @__PURE__ */ jsxRuntime.jsx(
34326
35114
  CdlEdgeLabelView,
34327
35115
  {
34328
35116
  edge,
@@ -34331,7 +35119,7 @@ function CdlStage({
34331
35119
  },
34332
35120
  `label-${edge.id}`
34333
35121
  )),
34334
- laid.edges.filter((edge) => (edge.headLabel || edge.tailLabel) && \u77E2\u5370\u306E\u51FA\u756A.\u51FA\u308B\u304B(edge.id)).flatMap(
35122
+ laid.edges.filter((edge) => (edge.headLabel || edge.tailLabel) && \u7DDA\u3092\u63CF\u304F(edge)).flatMap(
34335
35123
  (edge) => ["head", "tail"].map((\u7AEF) => /* @__PURE__ */ jsxRuntime.jsx(
34336
35124
  CdlEdgeEndLabelView,
34337
35125
  {
@@ -34343,6 +35131,7 @@ function CdlStage({
34343
35131
  `${\u7AEF}-label-${edge.id}`
34344
35132
  ))
34345
35133
  ),
35134
+ laid.legend && /* @__PURE__ */ jsxRuntime.jsx(CdlLegend, { items: laid.legend }),
34346
35135
  debug && /* @__PURE__ */ jsxRuntime.jsxs("g", { pointerEvents: "none", children: [
34347
35136
  laid.bboxes.map((bbox, idx) => {
34348
35137
  const color = bbox.kind === "node" ? "#3b82f6" : bbox.kind === "lane-label" ? "#a855f7" : bbox.kind === "edge-label" ? "#10b981" : "#f59e0b";
@@ -36007,6 +36796,7 @@ exports.LABEL_INIT_CLEARANCE_DOTTED_FLOW = LABEL_INIT_CLEARANCE_DOTTED_FLOW;
36007
36796
  exports.LABEL_SIZE_TOLERANCE_WORLD = LABEL_SIZE_TOLERANCE_WORLD;
36008
36797
  exports.LABEL_TO_PATH_CLEARANCE = LABEL_TO_PATH_CLEARANCE;
36009
36798
  exports.LANE_BORDER_CLEARANCE_TOL = LANE_BORDER_CLEARANCE_TOL;
36799
+ exports.LEGEND_MARKS = LEGEND_MARKS;
36010
36800
  exports.MARKER_GLOW_RADIUS = MARKER_GLOW_RADIUS;
36011
36801
  exports.MIN_COLUMN_ALIGNMENT_TOLERANCE = MIN_COLUMN_ALIGNMENT_TOLERANCE;
36012
36802
  exports.MIN_ROW_ALIGNMENT_TOLERANCE = MIN_ROW_ALIGNMENT_TOLERANCE;