@cardenelabs/cdl 0.124.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/react.cjs CHANGED
@@ -1006,7 +1006,7 @@ function drawsTitleOutsideBox(kind) {
1006
1006
  }
1007
1007
  var TITLE_FONT_SIZE_DEFAULT = 18;
1008
1008
  var TITLE_FONT_SIZE = new Map([
1009
- ...["dyn-rect", "dyn-circle", "dyn-arc", "dyn-wave", "dyn-polygon", "station"].map(
1009
+ ...["dyn-rect", "dyn-circle", "dyn-arc", "dyn-wave", "dyn-polygon"].map(
1010
1010
  (k) => [k, 13]
1011
1011
  ),
1012
1012
  ...[
@@ -1051,8 +1051,10 @@ var TITLE_FONT_SIZE = new Map([
1051
1051
  "webhook"
1052
1052
  ].map((k) => [k, 20]),
1053
1053
  ...["card", "event", "function"].map((k) => [k, 24]),
1054
+ ["station", 25],
1054
1055
  ["actor", 32],
1055
- ["storage", 34]
1056
+ ["storage", 34],
1057
+ ["timeline-number", 18]
1056
1058
  ]);
1057
1059
  function titleFontSize(kind) {
1058
1060
  return TITLE_FONT_SIZE.get(kind ?? "") ?? TITLE_FONT_SIZE_DEFAULT;
@@ -1062,9 +1064,17 @@ function skipsNodeTitleWidthCheck(kind) {
1062
1064
  return KINDS_WITH_TITLE_WIDTH_OUTSIDE_NODE.has(kind ?? "");
1063
1065
  }
1064
1066
  var TITLE_BOX_PADDING = 52;
1067
+ var TIMELINE_NUMBER_STROKE = 4;
1068
+ function titleUsableWidth(kind, w, h) {
1069
+ if (kind === "timeline-number") {
1070
+ return Math.min(w, h) - 2 * TIMELINE_NUMBER_STROKE;
1071
+ }
1072
+ return w - (drawsTitleOutsideBox(kind) ? 0 : TITLE_BOX_PADDING);
1073
+ }
1065
1074
  var KINDS_SCALING_TITLE_WHEN_LOW = /* @__PURE__ */ new Set(["actor", "event", "function", "storage"]);
1066
1075
  var KINDS_SCALING_TITLE_ALWAYS = /* @__PURE__ */ new Set([
1067
1076
  "card",
1077
+ "timeline-number",
1068
1078
  "shape-file",
1069
1079
  "shape-folder",
1070
1080
  "shape-cloud",
@@ -1148,14 +1158,16 @@ var KINDS_DRAWING_ONLY_SHAPE = [
1148
1158
  ];
1149
1159
  var KINDS_WITHOUT_EYEBROW_TEXT = /* @__PURE__ */ new Set([
1150
1160
  ...KINDS_DRAWING_ONLY_SHAPE,
1151
- ...KINDS_WITHOUT_TITLE_TEXT
1161
+ ...KINDS_WITHOUT_TITLE_TEXT,
1162
+ "timeline-number"
1152
1163
  ]);
1153
1164
  function rendersEyebrow(kind) {
1154
1165
  return !KINDS_WITHOUT_EYEBROW_TEXT.has(kind ?? "");
1155
1166
  }
1156
1167
  var KINDS_WITHOUT_SUBTITLE_TEXT = /* @__PURE__ */ new Set([
1157
1168
  ...KINDS_DRAWING_OWN_PAYLOAD,
1158
- ...KINDS_WITHOUT_TITLE_TEXT
1169
+ ...KINDS_WITHOUT_TITLE_TEXT,
1170
+ "timeline-number"
1159
1171
  ]);
1160
1172
  function rendersSubtitle(kind) {
1161
1173
  return !KINDS_WITHOUT_SUBTITLE_TEXT.has(kind ?? "");
@@ -3089,6 +3101,14 @@ function curvePath(from, to, edge, labelMarginExtra = 0, textMetrics = "proporti
3089
3101
 
3090
3102
  // src/layout/metro-path.ts
3091
3103
  var METRO_CORNER_RADIUS = 24;
3104
+ var METRO_BRANCH_CORNER_RADIUS = 22;
3105
+ var METRO_BRANCH_STUB = 30;
3106
+ var METRO_DECISION_HEIGHT = 88;
3107
+ var METRO_DECISION_WIDTH = 116;
3108
+ var METRO_RETURN_END_OFFSET = 43;
3109
+ var METRO_RETURN_START_OFFSET = 24;
3110
+ var METRO_LINE_WIDTH = 10;
3111
+ var METRO_LINE_WIDTH_CSS = `var(--cdl-metro-line-width, ${METRO_LINE_WIDTH}px)`;
3092
3112
  var T = Math.SQRT2 - 1;
3093
3113
  function round2(v) {
3094
3114
  return Math.round(v * 100) / 100;
@@ -3106,7 +3126,63 @@ function polyline(points) {
3106
3126
  });
3107
3127
  return distinct.map((p, i) => `${i === 0 ? "M" : "L"} ${point(p)}`).join(" ");
3108
3128
  }
3109
- function metroPath(from, to, edge, labelMarginExtra = 0, textMetrics = "proportional") {
3129
+ function quadraticCornerPath(start, corner, end, radius) {
3130
+ const horizontalDir = Math.sign(corner.x - start.x);
3131
+ const verticalDir = Math.sign(end.y - corner.y);
3132
+ const r = Math.min(
3133
+ radius,
3134
+ Math.abs(corner.x - start.x),
3135
+ Math.abs(end.y - corner.y)
3136
+ );
3137
+ const before = { x: corner.x - horizontalDir * r, y: corner.y };
3138
+ const after = { x: corner.x, y: corner.y + verticalDir * r };
3139
+ return [
3140
+ `M ${point(start)}`,
3141
+ `L ${point(before)}`,
3142
+ `Q ${point(corner)} ${point(after)}`,
3143
+ `L ${point(end)}`
3144
+ ].join(" ");
3145
+ }
3146
+ function decisionBranchPath(start, end) {
3147
+ const horizontalDir = Math.sign(end.x - start.x);
3148
+ const verticalDir = Math.sign(end.y - start.y);
3149
+ const turnX = start.x + horizontalDir * (METRO_BRANCH_STUB + METRO_BRANCH_CORNER_RADIUS);
3150
+ const radius = Math.min(
3151
+ METRO_BRANCH_CORNER_RADIUS,
3152
+ Math.abs(end.y - start.y) / 2,
3153
+ Math.abs(end.x - turnX)
3154
+ );
3155
+ const firstCorner = { x: turnX, y: start.y };
3156
+ const secondCorner = { x: turnX, y: end.y };
3157
+ const beforeFirst = {
3158
+ x: turnX - horizontalDir * radius,
3159
+ y: start.y
3160
+ };
3161
+ const afterFirst = {
3162
+ x: turnX,
3163
+ y: start.y + verticalDir * radius
3164
+ };
3165
+ const beforeSecond = {
3166
+ x: turnX,
3167
+ y: end.y - verticalDir * radius
3168
+ };
3169
+ const afterSecond = {
3170
+ x: turnX + horizontalDir * radius,
3171
+ y: end.y
3172
+ };
3173
+ return {
3174
+ d: [
3175
+ `M ${point(start)}`,
3176
+ `L ${point(beforeFirst)}`,
3177
+ `Q ${point(firstCorner)} ${point(afterFirst)}`,
3178
+ `L ${point(beforeSecond)}`,
3179
+ `Q ${point(secondCorner)} ${point(afterSecond)}`,
3180
+ `L ${point(end)}`
3181
+ ].join(" "),
3182
+ turnX
3183
+ };
3184
+ }
3185
+ function metroPath(from, to, edge, _labelMarginExtra = 0, _textMetrics = "proportional") {
3110
3186
  const fromLeft = from.cx - from.w / 2;
3111
3187
  const fromRight = from.cx + from.w / 2;
3112
3188
  const toLeft = to.cx - to.w / 2;
@@ -3141,11 +3217,45 @@ function metroPath(from, to, edge, labelMarginExtra = 0, textMetrics = "proporti
3141
3217
  const sgn = Math.sign(dy);
3142
3218
  let d;
3143
3219
  let mid;
3144
- let tangent;
3145
- if (Math.abs(dy) < SAME_HEIGHT_THRESHOLD) {
3220
+ let metroReturn = false;
3221
+ if (from.kind === "decision" && Math.abs(dy) >= SAME_HEIGHT_THRESHOLD) {
3222
+ const verticalDir = Math.sign(dy);
3223
+ if (Math.abs(to.cx - from.cx) < SAME_HEIGHT_THRESHOLD) {
3224
+ const start = { x: from.cx, y: from.cy + verticalDir * from.h / 2 };
3225
+ const end = { x: to.cx, y: to.cy };
3226
+ d = polyline([start, end]);
3227
+ fromSide = verticalDir < 0 ? "top" : "bottom";
3228
+ toSide = verticalDir < 0 ? "bottom" : "top";
3229
+ mid = { x: start.x, y: (start.y + end.y) / 2 };
3230
+ } else {
3231
+ const horizontalDir = Math.sign(to.cx - from.cx);
3232
+ const start = {
3233
+ x: from.cx + horizontalDir * from.w / 2,
3234
+ y: from.cy
3235
+ };
3236
+ const end = {
3237
+ x: to.cx - horizontalDir * to.w / 2,
3238
+ y: to.cy
3239
+ };
3240
+ const branch = decisionBranchPath(start, end);
3241
+ d = branch.d;
3242
+ fromSide = horizontalDir > 0 ? "right" : "left";
3243
+ toSide = horizontalDir > 0 ? "left" : "right";
3244
+ mid = { x: branch.turnX, y: (start.y + end.y) / 2 };
3245
+ }
3246
+ } else if (edge?.style === "dashed" && from.kind === "station" && to.kind === "station" && to.cx < from.cx && Math.abs(dy) >= SAME_HEIGHT_THRESHOLD) {
3247
+ const verticalDir = Math.sign(to.cy - from.cy);
3248
+ const start = { x: from.cx - METRO_RETURN_START_OFFSET, y: from.cy };
3249
+ const end = { x: to.cx, y: to.cy - verticalDir * METRO_RETURN_END_OFFSET };
3250
+ const corner = { x: end.x, y: start.y };
3251
+ fromSide = "left";
3252
+ toSide = verticalDir < 0 ? "bottom" : "top";
3253
+ d = quadraticCornerPath(start, corner, end, METRO_BRANCH_CORNER_RADIUS);
3254
+ mid = { x: (start.x + corner.x) / 2, y: start.y };
3255
+ metroReturn = true;
3256
+ } else if (Math.abs(dy) < SAME_HEIGHT_THRESHOLD) {
3146
3257
  d = `M ${point({ x: x1, y: y1 })} L ${point({ x: x2, y: y2 })}`;
3147
3258
  mid = { x: (x1 + x2) / 2, y: (y1 + y2) / 2 };
3148
- tangent = { x: x2 - x1, y: y2 - y1 };
3149
3259
  } else if (fromSide !== toSide && Math.abs(x2 - x1) >= Math.abs(dy)) {
3150
3260
  const horizontal = Math.abs(x2 - x1);
3151
3261
  const s = (horizontal - Math.abs(dy)) / 2;
@@ -3175,7 +3285,6 @@ function metroPath(from, to, edge, labelMarginExtra = 0, textMetrics = "proporti
3175
3285
  d = polyline([p0, p1, p2, p3]);
3176
3286
  }
3177
3287
  mid = { x: (x1 + x2) / 2, y: (y1 + y2) / 2 };
3178
- tangent = w;
3179
3288
  } else {
3180
3289
  fromSide = "right";
3181
3290
  toSide = "right";
@@ -3205,28 +3314,15 @@ function metroPath(from, to, edge, labelMarginExtra = 0, textMetrics = "proporti
3205
3314
  `L ${point(p4)}`
3206
3315
  ].join(" ");
3207
3316
  mid = { x: p2.x - T * r, y: p2.y };
3208
- tangent = { x: 0, y: sgn };
3209
- }
3210
- if (edge?.overlay) {
3211
- return { d, fromSide, toSide, labelX: mid.x, labelY: mid.y, labelAnchor: "middle" };
3212
- }
3213
- const tangentLength = Math.hypot(tangent.x, tangent.y);
3214
- let normalX = tangent.y / tangentLength;
3215
- let normalY = -tangent.x / tangentLength;
3216
- if (normalY > Number.EPSILON || Math.abs(normalY) <= Number.EPSILON && normalX < 0) {
3217
- normalX *= -1;
3218
- normalY *= -1;
3219
3317
  }
3220
- const labelW = computeLabelBoxW(edge?.label, edge?.sub, textMetrics);
3221
- const labelH = labelPillH(edge?.sub);
3222
- const distance = labelPathClearanceHorizontal(edge) + (Math.abs(normalX) * labelW + Math.abs(normalY) * labelH) / 2 + labelMarginExtra;
3223
3318
  return {
3224
3319
  d,
3225
3320
  fromSide,
3226
3321
  toSide,
3227
- labelX: mid.x + normalX * distance,
3228
- labelY: mid.y + normalY * distance,
3229
- labelAnchor: "middle"
3322
+ labelX: mid.x,
3323
+ labelY: mid.y,
3324
+ labelAnchor: "middle",
3325
+ ...metroReturn ? { metroReturn: true } : {}
3230
3326
  };
3231
3327
  }
3232
3328
 
@@ -3353,12 +3449,14 @@ var NODE_KINDS = [
3353
3449
  "shape-lawyer",
3354
3450
  "shape-trader",
3355
3451
  "shape-customer-service",
3356
- // 流れの始まりと終わりの印 2 種 (#560) ... 塗った丸と輪で囲んだ丸。
3357
- // 状態遷移図が使う。 印そのものが意味を持つので文字は描かない
3452
+ // 流れの始まりと終わりの印 2 種 (#560) ... 始点と終点は線の端から推測せず、著者が
3453
+ // `kind: "mark-start"` / `kind: "mark-end"` の node を明示する。印は文字を描かない
3358
3454
  "mark-start",
3359
3455
  "mark-end",
3360
3456
  // 路線図の駅 (#996) ... 線路上の点を小さな丸で示し、名前と説明は丸の外に置く
3361
3457
  "station",
3458
+ // 時間軸の番号 (#1017) ... 実線の軸上に番号を入れた丸を置く。路線図の駅とは別の意味を持つ。
3459
+ "timeline-number",
3362
3460
  // 時系列のやり取り (#578) ... 1 つの箱が図を丸ごと描く。 縦が時間で、参加者は箱を持たない
3363
3461
  "sequence-board"
3364
3462
  ];
@@ -6550,7 +6648,7 @@ function runAxes(laid, diag, violations, counts, push, profile, skippedAxes, \u5
6550
6648
  const LIFELINE_W_THRESHOLD = 8;
6551
6649
  for (const n of laid.nodes) {
6552
6650
  if (n.w < LIFELINE_W_THRESHOLD) continue;
6553
- if (n.kind === "station") continue;
6651
+ if (n.kind === "station" || n.kind === "timeline-number") continue;
6554
6652
  if (n.w < MIN_NODE_W || n.h < MIN_NODE_H) {
6555
6653
  push(
6556
6654
  "node-visibility",
@@ -6576,8 +6674,7 @@ function runAxes(laid, diag, violations, counts, push, profile, skippedAxes, \u5
6576
6674
  if (!isRenderedNode(n)) continue;
6577
6675
  const titleTrimmed = n.title?.trim() ?? "";
6578
6676
  if (!titleTrimmed) continue;
6579
- const \u4F59\u767D = drawsTitleOutsideBox(n.kind) ? 0 : TITLE_BOX_PADDING;
6580
- const \u4F7F\u3048\u308B\u5E452 = n.w - \u4F59\u767D;
6677
+ const \u4F7F\u3048\u308B\u5E452 = titleUsableWidth(n.kind, n.w, n.h);
6581
6678
  const \u5B9F\u5E45 = measureTextWidth(titleTrimmed, {
6582
6679
  fontSize: titleFontSize(n.kind),
6583
6680
  bound: "drawn",
@@ -6596,7 +6693,7 @@ function runAxes(laid, diag, violations, counts, push, profile, skippedAxes, \u5
6596
6693
  push(
6597
6694
  "text-readability",
6598
6695
  // 字ごとの実幅は小数になるので、読む人に出す時だけ丸める (判定は丸めない)
6599
- `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)`,
6696
+ `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)`,
6600
6697
  "warn"
6601
6698
  );
6602
6699
  }
@@ -8156,6 +8253,8 @@ var TOKENS = {
8156
8253
  lane_label_h: 40,
8157
8254
  lane_top_pad: 100,
8158
8255
  contain_pad: 28,
8256
+ stage_column_gap: 48,
8257
+ stage_card_text_inset: 18,
8159
8258
  // node sizes (kind ごと、 unified row h は engine が max() で算出)
8160
8259
  node: {
8161
8260
  actor: { w: 340, h: 165 },
@@ -8282,7 +8381,10 @@ var TOKENS = {
8282
8381
  "mark-start": { w: 96, h: 96 },
8283
8382
  "mark-end": { w: 96, h: 96 },
8284
8383
  // 路線図の駅 (#996) ... 正方形にして小さな丸が縦横どちらにも歪まないようにする。
8285
- station: { w: 24, h: 24 },
8384
+ // 半径 16 と縁 6 の外径を、線路との接点を含めて収める。
8385
+ station: { w: 38, h: 38 },
8386
+ // 時間軸の番号 (#1017) ... 半径 24 の丸と太さ 4 の縁を収める。
8387
+ "timeline-number": { w: 52, h: 52 },
8286
8388
  // 時系列のやり取り (#578)。 実寸は中身から決まる (`sequenceBoardMetrics`) ので、
8287
8389
  // ここは書かなかった時の受け皿
8288
8390
  "sequence-board": { w: 960, h: 640 }
@@ -8297,6 +8399,8 @@ var LANE_TOP_PAD = TOKENS.lane_top_pad;
8297
8399
  var STACK_GAP = TOKENS.row_gap;
8298
8400
  var NODE_SIZE = TOKENS.node;
8299
8401
  var CONTAIN_PAD = TOKENS.contain_pad;
8402
+ var STAGE_COLUMN_GAP = TOKENS.stage_column_gap;
8403
+ var STAGE_CARD_TEXT_INSET = TOKENS.stage_card_text_inset;
8300
8404
  var VIEW_PAD = TOKENS.view_pad;
8301
8405
  var HEADER_RESERVE = TOKENS.header_reserve;
8302
8406
  var DEFAULT_LANE_GAP = 80;
@@ -8397,12 +8501,24 @@ function layoutEdges(diag, nodes, lanes = [], textMetrics = "proportional") {
8397
8501
  if (!from) throw new Error(`cdl layout: edge "${e.id}" \u306E from "${e.from}" \u304C node \u306B\u7121\u3044`);
8398
8502
  if (!to) throw new Error(`cdl layout: edge "${e.id}" \u306E to "${e.to}" \u304C node \u306B\u7121\u3044`);
8399
8503
  const c = curvePath(from, to, e, labelMarginExtra, textMetrics);
8504
+ const fromLane = lmap.get(from.lane);
8505
+ const toLane = lmap.get(to.lane);
8506
+ let labelX = c.labelX;
8507
+ if (fromLane?.stage !== void 0 && toLane?.stage !== void 0 && fromLane.id !== toLane.id && (e.label || e.sub)) {
8508
+ const [leftLane, rightLane] = fromLane.x <= toLane.x ? [fromLane, toLane] : [toLane, fromLane];
8509
+ const gapLeft = leftLane.x + leftLane.width - STAGE_COLUMN_GAP / 2;
8510
+ const gapRight = rightLane.x + STAGE_COLUMN_GAP / 2;
8511
+ const labelHalfWidth = computeLabelBoxW(e.label, e.sub, textMetrics) / 2;
8512
+ const minX = gapLeft + labelHalfWidth;
8513
+ const maxX = gapRight - labelHalfWidth;
8514
+ if (minX <= maxX) labelX = Math.min(maxX, Math.max(minX, labelX));
8515
+ }
8400
8516
  positionedEdges.push({
8401
8517
  ...e,
8402
8518
  d: c.d,
8403
8519
  fromSide: c.fromSide,
8404
8520
  toSide: c.toSide,
8405
- labelX: c.labelX + (e.labelOffsetX ?? 0),
8521
+ labelX: labelX + (e.labelOffsetX ?? 0),
8406
8522
  labelY: c.labelY + (e.labelOffsetY ?? 0),
8407
8523
  labelAnchor: c.labelAnchor
8408
8524
  });
@@ -8419,7 +8535,8 @@ function layoutEdges(diag, nodes, lanes = [], textMetrics = "proportional") {
8419
8535
  toSide: m.toSide,
8420
8536
  labelX: m.labelX + (e.labelOffsetX ?? 0),
8421
8537
  labelY: m.labelY + (e.labelOffsetY ?? 0),
8422
- labelAnchor: m.labelAnchor
8538
+ labelAnchor: m.labelAnchor,
8539
+ ...m.metroReturn ? { metroReturn: true } : {}
8423
8540
  });
8424
8541
  } else {
8425
8542
  autoEdges.push(e);
@@ -9111,20 +9228,22 @@ function countPathObstacleCrossings(d, obstacles) {
9111
9228
  const segments = flattenPathSegments(extractPathSegments(d));
9112
9229
  return obstacles.filter((obstacle) => segmentsCrossRect(segments, nodeRect(obstacle))).length;
9113
9230
  }
9114
- function makeBackDetourCandidate(A, B, from, to, obstacles, detourY, side) {
9231
+ function makeBackDetourCandidate(A, B, from, to, obstacles, detourY, side, columnXOverride) {
9115
9232
  const lowIsA = A.y >= B.y;
9116
9233
  const low = lowIsA ? A : B;
9117
9234
  const lowBendY = low.y - VERTICAL_DETOUR_EXIT;
9118
9235
  const bandTop = Math.min(detourY, lowBendY);
9119
9236
  const bandBottom = Math.max(detourY, lowBendY);
9120
9237
  const bandObstacles = obstacles.map(nodeRect).filter((rect) => rect.y + rect.h >= bandTop && rect.y <= bandBottom);
9121
- let columnX = detourColumnFor([from, to], side);
9122
- while (true) {
9123
- const blockers = bandObstacles.filter(
9124
- (rect) => rect.x - VERTICAL_DETOUR_GAP < columnX && columnX < rect.x + rect.w + VERTICAL_DETOUR_GAP
9125
- );
9126
- if (blockers.length === 0) break;
9127
- 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));
9238
+ let columnX = columnXOverride ?? detourColumnFor([from, to], side);
9239
+ if (columnXOverride === void 0) {
9240
+ while (true) {
9241
+ const blockers = bandObstacles.filter(
9242
+ (rect) => rect.x - VERTICAL_DETOUR_GAP < columnX && columnX < rect.x + rect.w + VERTICAL_DETOUR_GAP
9243
+ );
9244
+ if (blockers.length === 0) break;
9245
+ 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));
9246
+ }
9128
9247
  }
9129
9248
  const bendAY = lowIsA ? lowBendY : detourY;
9130
9249
  const bendBY = lowIsA ? detourY : lowBendY;
@@ -9233,7 +9352,7 @@ function sidePoint(n, side) {
9233
9352
  return { x: n.cx + n.w / 2, y: n.cy };
9234
9353
  }
9235
9354
  }
9236
- 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") {
9355
+ 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") {
9237
9356
  const labelBoxW = edge ? computeLabelBoxW(edge.label, edge.sub, textMetrics) : 0;
9238
9357
  const labelHalfW = labelBoxW / 2;
9239
9358
  const labelInit = labelInitClearance(edge);
@@ -9603,17 +9722,27 @@ function routePath(from, to, fromSide, toSide, obstacles = [], detourSide, _from
9603
9722
  };
9604
9723
  }
9605
9724
  if (topTopPair) {
9606
- const detourY = Math.min(A.y, B.y) - 100;
9725
+ const stagePoint = (point2, node, lane) => lane?.stage === void 0 ? point2 : {
9726
+ x: Math.min(node.cx + node.w / 2 - 24, lane.x + lane.width - 24),
9727
+ y: point2.y
9728
+ };
9729
+ const backA = stagePoint(A, from, fromLane);
9730
+ const backB = stagePoint(B, to, toLane);
9731
+ const stageTops = [fromLane, toLane].filter((lane) => lane?.stage !== void 0).map((lane) => lane.y);
9732
+ const detourY = Math.min(
9733
+ Math.min(backA.y, backB.y) - 100,
9734
+ ...stageTops.map((top) => top - VERTICAL_DETOUR_GAP)
9735
+ );
9607
9736
  const corner = 14;
9608
- const xDir = B.x > A.x ? 1 : -1;
9737
+ const xDir = backB.x > backA.x ? 1 : -1;
9609
9738
  const existing = {
9610
- d: `M ${A.x} ${A.y}
9611
- L ${A.x} ${detourY + corner}
9612
- Q ${A.x} ${detourY}, ${A.x + xDir * corner} ${detourY}
9613
- L ${B.x - xDir * corner} ${detourY}
9614
- Q ${B.x} ${detourY}, ${B.x} ${detourY + corner}
9615
- L ${B.x} ${B.y}`,
9616
- labelX: (A.x + B.x) / 2,
9739
+ d: `M ${backA.x} ${backA.y}
9740
+ L ${backA.x} ${detourY + corner}
9741
+ Q ${backA.x} ${detourY}, ${backA.x + xDir * corner} ${detourY}
9742
+ L ${backB.x - xDir * corner} ${detourY}
9743
+ Q ${backB.x} ${detourY}, ${backB.x} ${detourY + corner}
9744
+ L ${backB.x} ${backB.y}`,
9745
+ labelX: (backA.x + backB.x) / 2,
9617
9746
  // CAR-468 SSOT ... 旧 detourY - 30 magic number を LABEL_INIT_CLEARANCE (26、 CAR-570 で 34 → 26 縮小) に置換。
9618
9747
  // CAR-518 SSOT ... dotted-flow は 62 world (marker glow radius 分拡張) を labelInit で参照。
9619
9748
  // top-top detour の水平 segment 上方 labelInit world = spec 準拠距離。
@@ -9622,7 +9751,16 @@ function routePath(from, to, fromSide, toSide, obstacles = [], detourSide, _from
9622
9751
  };
9623
9752
  const existingCrossings = countPathObstacleCrossings(existing.d, obstacles);
9624
9753
  if (existingCrossings === 0) return existing;
9625
- const candidate = chooseBackDetourCandidate(A, B, from, to, obstacles, detourY);
9754
+ const candidate = from.lane === to.lane && fromLane?.stage !== void 0 ? makeBackDetourCandidate(
9755
+ backA,
9756
+ backB,
9757
+ from,
9758
+ to,
9759
+ obstacles,
9760
+ detourY,
9761
+ "right",
9762
+ fromLane.x + fromLane.width
9763
+ ) : chooseBackDetourCandidate(backA, backB, from, to, obstacles, detourY);
9626
9764
  if (candidate.crossings >= existingCrossings) return existing;
9627
9765
  return {
9628
9766
  d: candidate.d,
@@ -10556,12 +10694,56 @@ function \u76F4\u524D\u306E\u6298\u308C\u3054\u3068\u305A\u3089\u3059(segs, \u6A
10556
10694
  return parts.join(" ");
10557
10695
  }
10558
10696
 
10697
+ // src/layout/legend.ts
10698
+ var LEGEND_FONT_SIZE = 17;
10699
+ var LEGEND_MARK_SCALE = LEGEND_FONT_SIZE / 21;
10700
+ var LEGEND_MARK_REFERENCE_WIDTH = 46;
10701
+ var LEGEND_MARK_WIDTH = LEGEND_MARK_REFERENCE_WIDTH * LEGEND_MARK_SCALE;
10702
+ var LEGEND_MARK_TEXT_GAP = 12 * LEGEND_MARK_SCALE;
10703
+ var LEGEND_ITEM_GAP_RATIO = 0.07;
10704
+ var LEGEND_BASELINE_GAP = 32;
10705
+ var LEGEND_BOTTOM_EXTENT = 10;
10706
+ function layoutLegend(items, viewBox, textMetrics, nodes) {
10707
+ const renderedNodes = nodes.filter(isRenderedNode);
10708
+ const startX = renderedNodes.length > 0 ? Math.min(...renderedNodes.map((node) => node.cx - node.w / 2)) : viewBox.x + VIEW_PAD;
10709
+ const diagramRight = renderedNodes.length > 0 ? Math.max(...renderedNodes.map((node) => node.cx + node.w / 2)) : viewBox.x + viewBox.w - VIEW_PAD;
10710
+ const itemGap = (diagramRight - startX) * LEGEND_ITEM_GAP_RATIO;
10711
+ const y = viewBox.y + viewBox.h - VIEW_PAD + LEGEND_BASELINE_GAP;
10712
+ let x = startX;
10713
+ const legend = items.map((item) => {
10714
+ const laid = { ...item, x, y };
10715
+ x += LEGEND_MARK_WIDTH + LEGEND_MARK_TEXT_GAP + textWidth(item.text, LEGEND_FONT_SIZE, 0, textMetrics) + itemGap;
10716
+ return laid;
10717
+ });
10718
+ const right = x - itemGap;
10719
+ const bottom = y + LEGEND_BOTTOM_EXTENT + VIEW_PAD;
10720
+ return {
10721
+ legend,
10722
+ viewBox: {
10723
+ ...viewBox,
10724
+ w: Math.max(viewBox.w, right + VIEW_PAD - viewBox.x),
10725
+ h: Math.max(viewBox.h, bottom - viewBox.y)
10726
+ }
10727
+ };
10728
+ }
10729
+
10559
10730
  // src/layout/lanes.ts
10560
10731
  var LANE_POSITION_EPSILON = 1e-9;
10732
+ function equalizeStageColumns(lanes, nodes = []) {
10733
+ const stages = lanes.filter((lane) => lane.stage !== void 0);
10734
+ if (stages.length === 0) return lanes;
10735
+ const top = Math.min(...stages.map((lane) => lane.y));
10736
+ const stageIds = new Set(stages.map((lane) => lane.id));
10737
+ const nodeBottoms = nodes.filter((node) => stageIds.has(node.lane)).map((node) => node.cy + node.h / 2 + CONTAIN_PAD);
10738
+ const bottom = Math.max(...stages.map((lane) => lane.y + lane.height), ...nodeBottoms);
10739
+ return lanes.map(
10740
+ (lane) => lane.stage === void 0 ? lane : { ...lane, y: top, height: bottom - top }
10741
+ );
10742
+ }
10561
10743
  function layoutLanes(diag) {
10562
10744
  const laneGap = diag.viewport?.laneGap ?? diag.viewport?.gap ?? DEFAULT_LANE_GAP;
10563
10745
  let cursorX = 0;
10564
- return diag.lanes.map((lane) => {
10746
+ const lanes = diag.lanes.map((lane, index) => {
10565
10747
  if (lane.posX !== void 0 && lane.posY !== void 0) {
10566
10748
  return {
10567
10749
  ...lane,
@@ -10572,7 +10754,9 @@ function layoutLanes(diag) {
10572
10754
  };
10573
10755
  }
10574
10756
  const resolvedX = lane.x ?? cursorX;
10575
- cursorX = resolvedX + lane.width + laneGap;
10757
+ const nextLane = diag.lanes[index + 1];
10758
+ const gapAfter = lane.stage !== void 0 && nextLane?.stage !== void 0 ? 0 : laneGap;
10759
+ cursorX = resolvedX + lane.width + gapAfter;
10576
10760
  return {
10577
10761
  ...lane,
10578
10762
  x: resolvedX,
@@ -10580,6 +10764,7 @@ function layoutLanes(diag) {
10580
10764
  height: LANE_LABEL_H
10581
10765
  };
10582
10766
  });
10767
+ return equalizeStageColumns(lanes);
10583
10768
  }
10584
10769
  function expandLanesForNodes(lanes, nodes) {
10585
10770
  const targetNodeGap = targetNodeNodeClearance();
@@ -10608,7 +10793,8 @@ function expandLanesForNodes(lanes, nodes) {
10608
10793
  if (lane.posX !== void 0 && lane.posY !== void 0) continue;
10609
10794
  const inside = nodes.filter((n) => n.lane === lane.id);
10610
10795
  const maxNodeW = inside.length > 0 ? Math.max(...inside.map((n) => n.w)) : 0;
10611
- const newWidth = Math.max(lane.width, maxNodeW + padFor(lane) * 2);
10796
+ const stageMinWidth = lane.stage === void 0 ? 0 : maxNodeW + CONTAIN_PAD + STAGE_COLUMN_GAP;
10797
+ const newWidth = Math.max(lane.width, maxNodeW + padFor(lane) * 2, stageMinWidth);
10612
10798
  widthOf.set(lane.id, newWidth);
10613
10799
  deltaOf.set(lane.id, newWidth - lane.width);
10614
10800
  }
@@ -10633,6 +10819,34 @@ function expandLaneGapsForEdgeLabels(lanes, nodes, edges, textMetrics = "proport
10633
10819
  if (lanes.length < 2) return lanes;
10634
10820
  const layoutedLanes = lanes.filter((l) => !(l.posX !== void 0 && l.posY !== void 0));
10635
10821
  if (layoutedLanes.length < 2) return lanes;
10822
+ if (layoutedLanes.length === lanes.length && layoutedLanes.every((lane) => lane.stage !== void 0)) {
10823
+ const sortedStages = [...layoutedLanes].sort((a, b) => a.x - b.x);
10824
+ const stageIndexById = new Map(sortedStages.map((lane, index) => [lane.id, index]));
10825
+ const nodeLaneById = new Map(nodes.map((node) => [node.id, node.lane]));
10826
+ const requiredSurfaceGaps = new Array(sortedStages.length - 1).fill(STAGE_COLUMN_GAP);
10827
+ for (const edge of edges) {
10828
+ if (edge.routing !== "curve" || !edge.label && !edge.sub) continue;
10829
+ const fromIndex = stageIndexById.get(nodeLaneById.get(edge.from) ?? "");
10830
+ const toIndex = stageIndexById.get(nodeLaneById.get(edge.to) ?? "");
10831
+ if (fromIndex === void 0 || toIndex === void 0 || fromIndex === toIndex) continue;
10832
+ const lo = Math.min(fromIndex, toIndex);
10833
+ const hi = Math.max(fromIndex, toIndex);
10834
+ const labelWidth = computeLabelBoxW(edge.label, edge.sub, textMetrics);
10835
+ for (let pair = lo; pair < hi; pair++) {
10836
+ requiredSurfaceGaps[pair] = Math.max(requiredSurfaceGaps[pair], labelWidth);
10837
+ }
10838
+ }
10839
+ const shiftedXById2 = /* @__PURE__ */ new Map([[sortedStages[0].id, sortedStages[0].x]]);
10840
+ for (let index = 1; index < sortedStages.length; index++) {
10841
+ const previous = sortedStages[index - 1];
10842
+ const lane = sortedStages[index];
10843
+ const previousX = shiftedXById2.get(previous.id);
10844
+ const authoredGap = Math.max(0, lane.x - (previous.x + previous.width));
10845
+ const laneGap = Math.max(authoredGap, requiredSurfaceGaps[index - 1] - STAGE_COLUMN_GAP);
10846
+ shiftedXById2.set(lane.id, previousX + previous.width + laneGap);
10847
+ }
10848
+ return lanes.map((lane) => ({ ...lane, x: shiftedXById2.get(lane.id) ?? lane.x }));
10849
+ }
10636
10850
  const nodeById = new Map(nodes.map((n) => [n.id, n]));
10637
10851
  const sortedByX = [...layoutedLanes].sort((a, b) => a.x - b.x);
10638
10852
  const laneIndexById = new Map(sortedByX.map((lane, i) => [lane.id, i]));
@@ -10704,7 +10918,7 @@ function recomputeContainedLanes(lanes, nodes) {
10704
10918
  lifelineBottom = Math.max(lifelineBottom, bottom);
10705
10919
  }
10706
10920
  }
10707
- return lanes.map((lane) => {
10921
+ const contained = lanes.map((lane) => {
10708
10922
  if (lane.lifeline) {
10709
10923
  return {
10710
10924
  ...lane,
@@ -10722,6 +10936,7 @@ function recomputeContainedLanes(lanes, nodes) {
10722
10936
  height: bottom - top + CONTAIN_PAD * 2 + LANE_LABEL_H
10723
10937
  };
10724
10938
  });
10939
+ return equalizeStageColumns(contained, nodes);
10725
10940
  }
10726
10941
 
10727
10942
  // src/layout/head-label.ts
@@ -10871,11 +11086,15 @@ function requiredStackGapForEdgeLabels(diag, allStacks) {
10871
11086
  }
10872
11087
  function layoutNodes(diag, lanes, textMetrics) {
10873
11088
  const laneMap = new Map(lanes.map((l) => [l.id, l]));
11089
+ const metroDecisionIds = new Set(
11090
+ 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"))
11091
+ );
11092
+ const nodeSize = (node) => metroDecisionIds.has(node.id) ? { w: METRO_DECISION_WIDTH, h: METRO_DECISION_HEIGHT } : NODE_SIZE[node.kind];
10874
11093
  const allStacks = Array.from(new Set(diag.nodes.map((n) => n.stack))).sort((a, b) => a - b);
10875
11094
  const rowH = /* @__PURE__ */ new Map();
10876
11095
  for (const stack of allStacks) {
10877
11096
  const nodesAtStack = diag.nodes.filter((n) => n.stack === stack);
10878
- const h = Math.max(...nodesAtStack.map((n) => \u7BB1\u306E\u9AD8\u30552(n, NODE_SIZE[n.kind].h)));
11097
+ const h = Math.max(...nodesAtStack.map((n) => \u7BB1\u306E\u9AD8\u30552(n, nodeSize(n).h)));
10879
11098
  rowH.set(stack, h);
10880
11099
  }
10881
11100
  const rowCy = /* @__PURE__ */ new Map();
@@ -10911,7 +11130,7 @@ function layoutNodes(diag, lanes, textMetrics) {
10911
11130
  if (!lane) {
10912
11131
  throw new Error(`cdl layout: node "${n.id}" \u306E lane "${n.lane}" \u304C\u5B9A\u7FA9\u3055\u308C\u3066\u3044\u306A\u3044`);
10913
11132
  }
10914
- const size = NODE_SIZE[n.kind];
11133
+ const size = nodeSize(n);
10915
11134
  if (n.posX !== void 0 && n.posY !== void 0) {
10916
11135
  const w2 = n.posW ?? n.w ?? autoRowsWidth(n, size.w, textMetrics);
10917
11136
  const h2 = n.posH ?? n.h ?? rowH.get(n.stack) ?? size.h;
@@ -11034,6 +11253,9 @@ function dropUnsafeLaneDx(dxByLane, nodes) {
11034
11253
  function applyRowLevelNodeShifts(nodes, lanes, nodeShifts) {
11035
11254
  const dyByStack = aggregateRowDy(nodes, nodeShifts);
11036
11255
  const dxByLane = dropUnsafeLaneDx(aggregateLaneDx(nodes, nodeShifts), nodes);
11256
+ for (const lane of lanes) {
11257
+ if (lane.stage !== void 0) dxByLane.delete(lane.id);
11258
+ }
11037
11259
  let moved = false;
11038
11260
  const outNodes = nodes.map((n) => {
11039
11261
  const dx = dxByLane.get(n.lane) ?? 0;
@@ -11153,7 +11375,15 @@ function layout(diag) {
11153
11375
  );
11154
11376
  }
11155
11377
  }
11156
- const viewBox = computeViewBox(containedLanesFinal, nodesFinal, edges, bboxes, diag.viewport);
11378
+ const contentViewBox = computeViewBox(
11379
+ containedLanesFinal,
11380
+ nodesFinal,
11381
+ edges,
11382
+ bboxes,
11383
+ diag.viewport
11384
+ );
11385
+ const legendLayout = diag.legend?.length ? layoutLegend(diag.legend, contentViewBox, textMetrics, nodesFinal) : void 0;
11386
+ const viewBox = legendLayout?.viewBox ?? contentViewBox;
11157
11387
  return {
11158
11388
  id: diag.id,
11159
11389
  topic: diag.topic,
@@ -11180,6 +11410,7 @@ function layout(diag) {
11180
11410
  ...textMetrics === "monospace" ? { textMetrics: "monospace" } : {},
11181
11411
  // 種類も配色と同じく空の目印を出さない。 遮断をここに揃えると手書きの図にも効く
11182
11412
  ...diag.type ? { type: diag.type } : {},
11413
+ ...legendLayout ? { legend: legendLayout.legend } : {},
11183
11414
  bboxes,
11184
11415
  collisions,
11185
11416
  nearCollisions,
@@ -11368,6 +11599,11 @@ function validate(diag) {
11368
11599
  \u2192 \u4FEE\u6B63\u4F8B: .lane("${n.lane}", { x: 0, width: 400 }) \u3092 node \u5BA3\u8A00\u524D\u306B\u8FFD\u52A0`
11369
11600
  );
11370
11601
  }
11602
+ if (n.leaderTo !== void 0 && !nodeIds.has(n.leaderTo)) {
11603
+ errors.push(
11604
+ `[cdl] unknown-ref: node "${n.id}" \u306E leaderTo "${n.leaderTo}" \u304C node \u306B\u5B58\u5728\u3057\u307E\u305B\u3093${suggest(n.leaderTo, nodeIds, "node")}`
11605
+ );
11606
+ }
11371
11607
  }
11372
11608
  for (const e of diag.edges) {
11373
11609
  if (!nodeIds.has(e.from)) {
@@ -11986,6 +12222,10 @@ function uniformLifelineEndY(lanes, nodes, starts = lifelineStartYs(nodes)) {
11986
12222
 
11987
12223
  // src/render/edge-head.ts
11988
12224
  var EDGE_HEAD_MUTED_MARKER_ID = "cdl-arrow-muted";
12225
+ var METRO_RETURN_ARROW_D = "M 21 0 L 4 9.5 L 4 -9.5 Z";
12226
+ function metroReturnArrowMarkerId(tone) {
12227
+ return `cdl-metro-return-arrow-${tone}`;
12228
+ }
11989
12229
  var EDGE_HEAD_SHAPE = {
11990
12230
  // 何も描かない端。 marker 自体を出さないので `d` は読まれない
11991
12231
  none: { d: "", w: 10, h: 10, refX: 0, filled: false },
@@ -12040,6 +12280,7 @@ function \u7B49\u5E45\u306E\u5B57(text, fontSize) {
12040
12280
  });
12041
12281
  return out;
12042
12282
  }
12283
+ var METRO_PENDING_ATTRIBUTE = "data-cdl-pending";
12043
12284
  function defaultEdgeWidth(active) {
12044
12285
  return active ? 2.5 : 1.25;
12045
12286
  }
@@ -12058,12 +12299,14 @@ function CdlEdgeView({
12058
12299
  drawing,
12059
12300
  progress,
12060
12301
  stateValues,
12061
- flowing = false
12302
+ flowing = false,
12303
+ pending = false
12062
12304
  }) {
12063
12305
  const color = edgeColor(edge);
12064
- const marker = edgeHeadMarkerName(edge.tone, edge.head, !active, edge.headFill);
12065
- const tailMarker = edge.tailHead === void 0 ? void 0 : edgeHeadMarkerName(edge.tone, edge.tailHead, !active, edge.tailHeadFill);
12066
12306
  const style = edge.style ?? "solid";
12307
+ const isMetroReturn = edge.metroReturn === true;
12308
+ const marker = isMetroReturn ? metroReturnArrowMarkerId(edge.tone) : edgeHeadMarkerName(edge.tone, edge.head, !active, edge.headFill);
12309
+ const tailMarker = edge.tailHead === void 0 ? void 0 : edgeHeadMarkerName(edge.tone, edge.tailHead, !active, edge.tailHeadFill);
12067
12310
  const isDotted = style === "dotted-flow";
12068
12311
  const isPassThrough = isDotted && Boolean(edge.dThrough);
12069
12312
  const lineDotsD = isDotted ? shrinkPathEnd(edge.d, 12) : edge.d;
@@ -12073,6 +12316,7 @@ function CdlEdgeView({
12073
12316
  return /* @__PURE__ */ jsxRuntime.jsxs(
12074
12317
  "g",
12075
12318
  {
12319
+ ...pending ? { [METRO_PENDING_ATTRIBUTE]: "true" } : {},
12076
12320
  "data-cdl-edge": edge.id,
12077
12321
  "data-cdl-routing": edge.routing === "metro" ? "metro" : void 0,
12078
12322
  "data-cdl-from": edge.from,
@@ -12098,11 +12342,12 @@ function CdlEdgeView({
12098
12342
  fill: "none",
12099
12343
  stroke: color,
12100
12344
  strokeOpacity: active ? 0.95 : 0.9,
12101
- strokeWidth: active ? 5 : 4,
12345
+ strokeWidth: edge.routing === "metro" ? METRO_LINE_WIDTH : active ? 5 : 4,
12102
12346
  strokeDasharray: "0 18",
12103
12347
  strokeLinecap: "round",
12104
12348
  style: {
12105
- transition: "stroke 280ms ease, stroke-opacity 280ms ease, stroke-width 280ms ease"
12349
+ transition: "stroke 280ms ease, stroke-opacity 280ms ease, stroke-width 280ms ease",
12350
+ strokeWidth: edge.routing === "metro" ? METRO_LINE_WIDTH_CSS : void 0
12106
12351
  }
12107
12352
  }
12108
12353
  ),
@@ -12157,7 +12402,9 @@ function SolidEdgePath({
12157
12402
  const boundWidth = edge.widthBind && stateValues ? resolveNumericAttr(edge.widthBind, stateValues, defaultWidth) : defaultWidth;
12158
12403
  const boundStroke = edge.strokeBind && stateValues ? interpolate(edge.strokeBind, stateValues) : color;
12159
12404
  const boundDashOffset = edge.dashOffsetBind && stateValues ? resolveNumericAttr(edge.dashOffsetBind, stateValues, 0) : void 0;
12160
- const dashArray = dashed ? "10 8" : active ? boundDashOffset !== void 0 ? "8 6" : void 0 : "4 5";
12405
+ const strokeWidth = edge.routing === "metro" && edge.widthBind === void 0 ? edge.metroReturn ? 6 : METRO_LINE_WIDTH : boundWidth;
12406
+ const strokeWidthCss = edge.routing === "metro" && !edge.metroReturn ? edge.widthBind === void 0 ? METRO_LINE_WIDTH_CSS : `var(--cdl-metro-line-width, ${strokeWidth}px)` : void 0;
12407
+ const dashArray = dashed ? edge.metroReturn ? "0 9.6" : "10 8" : active ? boundDashOffset !== void 0 ? "8 6" : void 0 : "4 5";
12161
12408
  return /* @__PURE__ */ jsxRuntime.jsx(
12162
12409
  "path",
12163
12410
  {
@@ -12166,14 +12413,15 @@ function SolidEdgePath({
12166
12413
  fill: "none",
12167
12414
  stroke: boundStroke,
12168
12415
  strokeOpacity: active ? 0.95 : 0.9,
12169
- strokeWidth: boundWidth,
12416
+ strokeWidth,
12170
12417
  strokeDasharray: dashArray,
12171
12418
  strokeDashoffset: boundDashOffset,
12172
12419
  strokeLinecap: "round",
12173
12420
  markerEnd: marker === void 0 ? void 0 : `url(#${marker})`,
12174
12421
  markerStart: tailMarker === void 0 ? void 0 : `url(#${tailMarker})`,
12175
12422
  style: {
12176
- transition: "stroke 280ms ease, stroke-opacity 280ms ease, stroke-width 280ms ease"
12423
+ transition: "stroke 280ms ease, stroke-opacity 280ms ease, stroke-width 280ms ease",
12424
+ strokeWidth: strokeWidthCss
12177
12425
  }
12178
12426
  }
12179
12427
  );
@@ -12470,7 +12718,11 @@ function \u5272\u308B\u5019\u88DC(text) {
12470
12718
  function \u5165\u3063\u3066\u3044\u308B([\u5DE6, \u53F3]) {
12471
12719
  return \u5DE6 !== "" && \u53F3 !== "";
12472
12720
  }
12473
- function CardNode({ node, active }) {
12721
+ function CardNode({
12722
+ node,
12723
+ active,
12724
+ compactTitleInset
12725
+ }) {
12474
12726
  const accent = nodeAccent(node, "var(--cdl-tone-accent, #2d6a8f)");
12475
12727
  const x = node.cx - node.w / 2;
12476
12728
  const y = node.cy - node.h / 2;
@@ -12488,12 +12740,12 @@ function CardNode({ node, active }) {
12488
12740
  });
12489
12741
  const \u305A\u3089\u3057 = \u7E70\u308A\u4E0A\u3052 - \u4F59\u308A;
12490
12742
  const titleY = compact ? \u5C0F\u578B\u306B\u547C\u3073\u540D ? node.h / 2 - 2 : node.h / 2 + 8 : 86 - \u305A\u3089\u3057;
12491
- const titleX = compact ? node.w / 2 : 26;
12492
- const paddingX = compact ? 16 : 52;
12743
+ const titleX = compact ? compactTitleInset ?? node.w / 2 : 26;
12744
+ const paddingX = compact ? (compactTitleInset ?? 8) * 2 : 52;
12493
12745
  const availableW = node.w - paddingX;
12494
12746
  const initialTitleSize = compact ? 20 : 24;
12495
12747
  const titleSize = fitTextSize(node.title, availableW, initialTitleSize, 11);
12496
- const textAnchor = compact ? "middle" : "start";
12748
+ const textAnchor = compact && compactTitleInset === void 0 ? "middle" : "start";
12497
12749
  const subtitleY = 124 - \u305A\u3089\u3057;
12498
12750
  const \u6700\u5927\u306E\u884C\u9001\u308A = SUBTITLE_SIZE * \u884C\u9001\u308A\u306E\u500D\u7387;
12499
12751
  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;
@@ -13253,7 +13505,9 @@ function GenericNode({
13253
13505
  value,
13254
13506
  stateValues,
13255
13507
  \u5024\u306E\u540D\u524D,
13256
- textMetrics = "proportional"
13508
+ textMetrics = "proportional",
13509
+ showKindIcon = true,
13510
+ metroDecision = false
13257
13511
  }) {
13258
13512
  const base = STYLES[node.kind] ?? FALLBACK;
13259
13513
  const toned = toneColor3(node.tone);
@@ -13284,14 +13538,14 @@ function GenericNode({
13284
13538
  if (shape === "diamond") {
13285
13539
  const cx = w / 2;
13286
13540
  const cy = h / 2;
13287
- const pad = 8;
13541
+ const pad = metroDecision ? 0 : 8;
13288
13542
  return /* @__PURE__ */ jsxRuntime.jsx(
13289
13543
  "path",
13290
13544
  {
13291
13545
  d: `M ${cx} ${pad} L ${w - pad} ${cy} L ${cx} ${h - pad} L ${pad} ${cy} Z`,
13292
13546
  fill: "var(--cdl-node-fill, #ffffff)",
13293
13547
  stroke: active ? style.accent : "var(--cdl-divider, #d4d4d8)",
13294
- strokeWidth: active ? 3 : 1.25
13548
+ strokeWidth: metroDecision ? 1.5 : active ? 3 : 1.25
13295
13549
  }
13296
13550
  );
13297
13551
  }
@@ -13355,11 +13609,11 @@ function GenericNode({
13355
13609
  const \u305A\u3089\u3057 = \u7E70\u308A\u4E0A\u3052 - \u4F59\u308A;
13356
13610
  const \u7D75\u306E\u82AF = centered && STYLES[node.kind]?.shape === "cloud" ? h * CLOUD_ART_CENTER_RISE : 0;
13357
13611
  const eyebrowY = centered ? h / 2 - 26 - \u7D75\u306E\u82AF : 32 + cylinderShift + \u4F59\u308A;
13358
- const titleY = genericTitleBaselineY(node.kind, h, hasRows) - \u305A\u3089\u3057 - \u7D75\u306E\u82AF;
13612
+ const titleY = metroDecision ? h / 2 + 9 : genericTitleBaselineY(node.kind, h, hasRows) - \u305A\u3089\u3057 - \u7D75\u306E\u82AF;
13359
13613
  const subtitleY = centered ? h / 2 + 28 - \u7D75\u306E\u82AF : 96 + cylinderShift - \u305A\u3089\u3057;
13360
13614
  const iconX = centered ? w / 2 + 40 : w - 48;
13361
13615
  const iconY = centered ? h / 2 - 44 - \u7D75\u306E\u82AF : 18 + cylinderShift;
13362
- const dominantBaseline = centered ? "middle" : void 0;
13616
+ const dominantBaseline = centered && !metroDecision ? "middle" : void 0;
13363
13617
  return /* @__PURE__ */ jsxRuntime.jsxs(
13364
13618
  "g",
13365
13619
  {
@@ -13368,12 +13622,12 @@ function GenericNode({
13368
13622
  "data-cdl-look": style.look,
13369
13623
  children: [
13370
13624
  shapeEl,
13371
- /* @__PURE__ */ jsxRuntime.jsxs("g", { transform: `translate(${iconX} ${iconY}) scale(1)`, "data-cdl-role": "node-kind-icon", children: [
13625
+ showKindIcon && /* @__PURE__ */ jsxRuntime.jsxs("g", { transform: `translate(${iconX} ${iconY}) scale(1)`, "data-cdl-role": "node-kind-icon", children: [
13372
13626
  /* @__PURE__ */ jsxRuntime.jsx("rect", { width: 24, height: 24, fill: "transparent" }),
13373
13627
  /* @__PURE__ */ jsxRuntime.jsx("path", { d: style.icon, fill: style.eyebrowColor, opacity: 0.7, transform: "scale(1)" })
13374
13628
  ] }),
13375
13629
  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 }),
13376
- /* @__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 }),
13630
+ /* @__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 }),
13377
13631
  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) }),
13378
13632
  resolvedValue && /* @__PURE__ */ jsxRuntime.jsx(
13379
13633
  "text",
@@ -13422,50 +13676,176 @@ function GenericNode({
13422
13676
  var MARK_INNER_RATIO = 0.56;
13423
13677
  var RING_WIDTH_ACTIVE = 6;
13424
13678
  var RING_WIDTH_IDLE = 4;
13679
+ var METRO_START_RADIUS = 17;
13680
+ var METRO_END_RADIUS = 19;
13681
+ var METRO_END_RING_WIDTH = 3.5;
13682
+ var METRO_END_INNER_RADIUS = 10;
13425
13683
  var MARK_DEFAULT = "var(--cdl-tone-accent, #2d6a8f)";
13426
13684
  function markRadius(node) {
13427
13685
  return Math.min(node.w, node.h) / 2;
13428
13686
  }
13429
- function MarkStartNode({ node }) {
13687
+ function MarkStartNode({
13688
+ node,
13689
+ metro = false
13690
+ }) {
13430
13691
  const \u8272 = nodeAccent(node, MARK_DEFAULT);
13431
- return /* @__PURE__ */ jsxRuntime.jsx("g", { transform: `translate(${node.cx - node.w / 2} ${node.cy - node.h / 2})`, children: /* @__PURE__ */ jsxRuntime.jsx(
13432
- "circle",
13692
+ return /* @__PURE__ */ jsxRuntime.jsx(
13693
+ "g",
13433
13694
  {
13434
- "data-cdl-role": "node-body",
13435
- "data-cdl-mark": "start",
13436
- cx: node.w / 2,
13437
- cy: node.h / 2,
13438
- r: markRadius(node),
13439
- fill: \u8272
13695
+ "data-cdl-role": metro ? "mark-start" : void 0,
13696
+ transform: `translate(${node.cx - node.w / 2} ${node.cy - node.h / 2})`,
13697
+ children: /* @__PURE__ */ jsxRuntime.jsx(
13698
+ "circle",
13699
+ {
13700
+ "data-cdl-role": "node-body",
13701
+ "data-cdl-mark": "start",
13702
+ cx: node.w / 2,
13703
+ cy: node.h / 2,
13704
+ r: metro ? METRO_START_RADIUS : markRadius(node),
13705
+ fill: \u8272
13706
+ }
13707
+ )
13440
13708
  }
13441
- ) });
13709
+ );
13442
13710
  }
13443
- function MarkEndNode({ node, active }) {
13711
+ function MarkEndNode({
13712
+ node,
13713
+ active,
13714
+ metro = false
13715
+ }) {
13444
13716
  const \u8272 = nodeAccent(node, MARK_DEFAULT);
13445
- const r = markRadius(node);
13446
- const \u8F2A\u306E\u592A\u3055 = active ? RING_WIDTH_ACTIVE : RING_WIDTH_IDLE;
13447
- return /* @__PURE__ */ jsxRuntime.jsxs("g", { transform: `translate(${node.cx - node.w / 2} ${node.cy - node.h / 2})`, children: [
13717
+ const r = metro ? METRO_END_RADIUS : markRadius(node);
13718
+ const \u8F2A\u306E\u592A\u3055 = metro ? METRO_END_RING_WIDTH : active ? RING_WIDTH_ACTIVE : RING_WIDTH_IDLE;
13719
+ return /* @__PURE__ */ jsxRuntime.jsxs(
13720
+ "g",
13721
+ {
13722
+ "data-cdl-role": metro ? "mark-end" : void 0,
13723
+ transform: `translate(${node.cx - node.w / 2} ${node.cy - node.h / 2})`,
13724
+ children: [
13725
+ /* @__PURE__ */ jsxRuntime.jsx(
13726
+ "circle",
13727
+ {
13728
+ "data-cdl-role": "node-body",
13729
+ "data-cdl-mark": "end",
13730
+ cx: node.w / 2,
13731
+ cy: node.h / 2,
13732
+ r: metro ? r : Math.max(0, r - \u8F2A\u306E\u592A\u3055 / 2),
13733
+ fill: "var(--cdl-node-fill, #ffffff)",
13734
+ stroke: \u8272,
13735
+ strokeWidth: \u8F2A\u306E\u592A\u3055
13736
+ }
13737
+ ),
13738
+ /* @__PURE__ */ jsxRuntime.jsx(
13739
+ "circle",
13740
+ {
13741
+ "data-cdl-role": "node-inner",
13742
+ cx: node.w / 2,
13743
+ cy: node.h / 2,
13744
+ r: metro ? METRO_END_INNER_RADIUS : Math.max(0, r * MARK_INNER_RATIO),
13745
+ fill: \u8272
13746
+ }
13747
+ )
13748
+ ]
13749
+ }
13750
+ );
13751
+ }
13752
+ var METRO_STATION_RADIUS = 16;
13753
+ var METRO_STATION_STROKE_WIDTH = 6;
13754
+ var METRO_STATION_RADIUS_CSS = `var(--cdl-metro-station-radius, ${METRO_STATION_RADIUS}px)`;
13755
+ var METRO_STATION_STROKE_WIDTH_CSS = `var(--cdl-metro-station-stroke-width, ${METRO_STATION_STROKE_WIDTH}px)`;
13756
+ var STATION_DEFAULT = "var(--cdl-tone-accent, #2d6a8f)";
13757
+ var STATION_NAME_FONT_SIZE = 25;
13758
+ var STATION_NAME_GAP = 18;
13759
+ var STATION_NAME_RIGHT_GAP = 32;
13760
+ var STATION_NAME_RIGHT_BASELINE = 9;
13761
+ var STATION_SUBTITLE_FONT_SIZE = 18;
13762
+ var STATION_SUBTITLE_GAP = 22;
13763
+ function StationNode({
13764
+ node,
13765
+ nameRight = false
13766
+ }) {
13767
+ const name = node.title.trim();
13768
+ const subtitle = node.subtitle?.trim() ?? "";
13769
+ return /* @__PURE__ */ jsxRuntime.jsxs("g", { children: [
13448
13770
  /* @__PURE__ */ jsxRuntime.jsx(
13449
13771
  "circle",
13450
13772
  {
13451
13773
  "data-cdl-role": "node-body",
13452
- "data-cdl-mark": "end",
13453
- cx: node.w / 2,
13454
- cy: node.h / 2,
13455
- r: Math.max(0, r - \u8F2A\u306E\u592A\u3055 / 2),
13774
+ "data-cdl-mark": "station",
13775
+ cx: node.cx,
13776
+ cy: node.cy,
13777
+ r: METRO_STATION_RADIUS,
13456
13778
  fill: "var(--cdl-node-fill, #ffffff)",
13457
- stroke: \u8272,
13458
- strokeWidth: \u8F2A\u306E\u592A\u3055
13779
+ stroke: nodeAccent(node, STATION_DEFAULT),
13780
+ strokeWidth: METRO_STATION_STROKE_WIDTH,
13781
+ style: { r: METRO_STATION_RADIUS_CSS, strokeWidth: METRO_STATION_STROKE_WIDTH_CSS }
13782
+ }
13783
+ ),
13784
+ name !== "" && /* @__PURE__ */ jsxRuntime.jsx(
13785
+ "text",
13786
+ {
13787
+ "data-cdl-role": "shape-name",
13788
+ x: nameRight ? node.cx + STATION_NAME_RIGHT_GAP : node.cx,
13789
+ y: nameRight ? node.cy + STATION_NAME_RIGHT_BASELINE : node.cy - METRO_STATION_RADIUS - STATION_NAME_GAP,
13790
+ textAnchor: nameRight ? "start" : "middle",
13791
+ fontSize: STATION_NAME_FONT_SIZE,
13792
+ fontWeight: 700,
13793
+ fill: "var(--cdl-text, #23201b)",
13794
+ stroke: "var(--cdl-node-fill, #ffffff)",
13795
+ strokeWidth: 8,
13796
+ strokeLinejoin: "round",
13797
+ paintOrder: "stroke",
13798
+ children: name
13459
13799
  }
13460
13800
  ),
13801
+ subtitle !== "" && /* @__PURE__ */ jsxRuntime.jsx(
13802
+ "text",
13803
+ {
13804
+ "data-cdl-role": "shape-value",
13805
+ x: node.cx,
13806
+ y: node.cy + METRO_STATION_RADIUS + STATION_SUBTITLE_GAP,
13807
+ textAnchor: "middle",
13808
+ fontSize: STATION_SUBTITLE_FONT_SIZE,
13809
+ fill: "var(--cdl-text-dim, #5a6270)",
13810
+ children: subtitle
13811
+ }
13812
+ )
13813
+ ] });
13814
+ }
13815
+ function TimelineNumberNode({ node }) {
13816
+ const radius = Math.max(0, Math.min(node.w, node.h) / 2 - TIMELINE_NUMBER_STROKE / 2);
13817
+ const fontSize = fitTextSize(
13818
+ node.title,
13819
+ titleUsableWidth(node.kind, node.w, node.h),
13820
+ 18,
13821
+ MIN_TEXT_SIZE
13822
+ );
13823
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
13461
13824
  /* @__PURE__ */ jsxRuntime.jsx(
13462
13825
  "circle",
13463
13826
  {
13464
- "data-cdl-role": "node-inner",
13465
- cx: node.w / 2,
13466
- cy: node.h / 2,
13467
- r: Math.max(0, r * MARK_INNER_RATIO),
13468
- fill: \u8272
13827
+ "data-cdl-role": "node-body",
13828
+ "data-cdl-mark": "timeline-number",
13829
+ cx: node.cx,
13830
+ cy: node.cy,
13831
+ r: radius,
13832
+ fill: "var(--cdl-node-fill, #ffffff)",
13833
+ stroke: nodeAccent(node, STATION_DEFAULT),
13834
+ strokeWidth: TIMELINE_NUMBER_STROKE
13835
+ }
13836
+ ),
13837
+ /* @__PURE__ */ jsxRuntime.jsx(
13838
+ "text",
13839
+ {
13840
+ "data-cdl-role": "node-label",
13841
+ x: node.cx,
13842
+ y: node.cy,
13843
+ textAnchor: "middle",
13844
+ dominantBaseline: "central",
13845
+ fontSize,
13846
+ fontWeight: 700,
13847
+ fill: "var(--cdl-text, #1a1f2a)",
13848
+ children: node.title
13469
13849
  }
13470
13850
  )
13471
13851
  ] });
@@ -13797,27 +14177,6 @@ function interpolateColor(template, stateValues, fallback) {
13797
14177
  if (!template) return fallback;
13798
14178
  return interpolate(template, stateValues);
13799
14179
  }
13800
- var STATION_STROKE_ACTIVE = 4;
13801
- var STATION_STROKE_IDLE = 3;
13802
- var STATION_DEFAULT = "var(--cdl-tone-accent, #2d6a8f)";
13803
- function StationNode({ node, active }) {
13804
- const strokeWidth = active ? STATION_STROKE_ACTIVE : STATION_STROKE_IDLE;
13805
- const radius = Math.max(0, Math.min(node.w, node.h) / 2 - strokeWidth / 2);
13806
- const \u5F62 = /* @__PURE__ */ jsxRuntime.jsx(
13807
- "circle",
13808
- {
13809
- "data-cdl-role": "node-body",
13810
- "data-cdl-mark": "station",
13811
- cx: node.cx,
13812
- cy: node.cy,
13813
- r: radius,
13814
- fill: "var(--cdl-node-fill, #ffffff)",
13815
- stroke: nodeAccent(node, STATION_DEFAULT),
13816
- strokeWidth
13817
- }
13818
- );
13819
- return renderShapeWithOutsideLabels(\u5F62, node, "middle");
13820
- }
13821
14180
 
13822
14181
  // src/kinds/label-stack.ts
13823
14182
  function \u540D\u672D\u306E\u6BB5\u3092\u307B\u3069\u304F(\u771F\u306Ey, \u9593\u9694, \u4E0A\u9650, \u4E0B\u9650) {
@@ -20979,10 +21338,14 @@ function CdlNodeView({
20979
21338
  stateValues,
20980
21339
  currentPhaseId,
20981
21340
  chartLineScale,
21341
+ compactTitleInset,
20982
21342
  \u95A2\u4FC2\u306E\u53D7\u3051\u53E3,
20983
21343
  lang,
20984
21344
  textMetrics,
20985
- \u5024\u306E\u540D\u524D
21345
+ \u5024\u306E\u540D\u524D,
21346
+ metroDiagram = false,
21347
+ metroDecision = false,
21348
+ stationNameRight = false
20986
21349
  }) {
20987
21350
  const value = node.value ? interpolate(node.value, stateValues, \u5024\u306E\u540D\u524D) : void 0;
20988
21351
  const rows = (node.rows ?? []).map((r) => interpolate(r, stateValues, \u5024\u306E\u540D\u524D));
@@ -21023,7 +21386,7 @@ function CdlNodeView({
21023
21386
  case "event":
21024
21387
  return /* @__PURE__ */ jsxRuntime.jsx(EventNode, { node: resolvedNode, active, progress });
21025
21388
  case "card":
21026
- return /* @__PURE__ */ jsxRuntime.jsx(CardNode, { node: resolvedNode, active });
21389
+ return /* @__PURE__ */ jsxRuntime.jsx(CardNode, { node: resolvedNode, active, compactTitleInset });
21027
21390
  // 時系列のやり取り (#578)。 1 つの箱が図を丸ごと描く = 縦が時間で、参加者は箱を持たない
21028
21391
  case "sequence-board":
21029
21392
  return /* @__PURE__ */ jsxRuntime.jsx(
@@ -21310,11 +21673,13 @@ function CdlNodeView({
21310
21673
  case "shape-customer-service":
21311
21674
  return /* @__PURE__ */ jsxRuntime.jsx(ShapeCustomerServiceNode, { node: resolvedNode, active });
21312
21675
  case "mark-start":
21313
- return /* @__PURE__ */ jsxRuntime.jsx(MarkStartNode, { node: resolvedNode, active });
21676
+ return /* @__PURE__ */ jsxRuntime.jsx(MarkStartNode, { node: resolvedNode, active, metro: metroDiagram });
21314
21677
  case "mark-end":
21315
- return /* @__PURE__ */ jsxRuntime.jsx(MarkEndNode, { node: resolvedNode, active });
21678
+ return /* @__PURE__ */ jsxRuntime.jsx(MarkEndNode, { node: resolvedNode, active, metro: metroDiagram });
21316
21679
  case "station":
21317
- return /* @__PURE__ */ jsxRuntime.jsx(StationNode, { node: resolvedNode, active });
21680
+ return /* @__PURE__ */ jsxRuntime.jsx(StationNode, { node: resolvedNode, active, nameRight: stationNameRight });
21681
+ case "timeline-number":
21682
+ return /* @__PURE__ */ jsxRuntime.jsx(TimelineNumberNode, { node: resolvedNode });
21318
21683
  default:
21319
21684
  return /* @__PURE__ */ jsxRuntime.jsx(
21320
21685
  GenericNode,
@@ -21324,7 +21689,9 @@ function CdlNodeView({
21324
21689
  value: node.value,
21325
21690
  stateValues,
21326
21691
  \u5024\u306E\u540D\u524D,
21327
- textMetrics
21692
+ textMetrics,
21693
+ showKindIcon: !(metroDiagram && node.kind === "decision"),
21694
+ metroDecision
21328
21695
  }
21329
21696
  );
21330
21697
  }
@@ -21438,6 +21805,112 @@ function \u7BB1\u306E\u4E0A\u306E\u8A9E(node, textMetrics = "proportional") {
21438
21805
  )
21439
21806
  ] });
21440
21807
  }
21808
+ var ink = "var(--cdl-text-accent, #143a52)";
21809
+ var paper = "var(--cdl-stage-bg, #f8fafc)";
21810
+ var LEGEND_MARK_RENDERERS = {
21811
+ 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 }),
21812
+ "filled-circle": () => /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: 23, cy: -8, r: 11, fill: ink }),
21813
+ "double-circle": () => /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
21814
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: 23, cy: -8, r: 12, fill: "none", stroke: ink, strokeWidth: 2.5 }),
21815
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: 23, cy: -8, r: 6, fill: ink })
21816
+ ] }),
21817
+ "dotted-line": () => /* @__PURE__ */ jsxRuntime.jsx(
21818
+ "path",
21819
+ {
21820
+ d: `M 0 -8 H ${LEGEND_MARK_REFERENCE_WIDTH}`,
21821
+ fill: "none",
21822
+ stroke: ink,
21823
+ strokeWidth: 6,
21824
+ strokeLinecap: "round",
21825
+ strokeDasharray: "0 9.6"
21826
+ }
21827
+ ),
21828
+ "solid-line": () => /* @__PURE__ */ jsxRuntime.jsx(
21829
+ "path",
21830
+ {
21831
+ d: `M 0 -8 H ${LEGEND_MARK_REFERENCE_WIDTH}`,
21832
+ fill: "none",
21833
+ stroke: ink,
21834
+ strokeWidth: 4,
21835
+ strokeLinecap: "round"
21836
+ }
21837
+ ),
21838
+ "curved-line": () => /* @__PURE__ */ jsxRuntime.jsx(
21839
+ "path",
21840
+ {
21841
+ d: `M 0 6 C 12 -22, 34 -22, ${LEGEND_MARK_REFERENCE_WIDTH} 6`,
21842
+ fill: "none",
21843
+ stroke: ink,
21844
+ strokeWidth: 4,
21845
+ strokeLinecap: "round"
21846
+ }
21847
+ ),
21848
+ "rounded-label": () => /* @__PURE__ */ jsxRuntime.jsx(
21849
+ "rect",
21850
+ {
21851
+ x: 0,
21852
+ y: -23,
21853
+ width: LEGEND_MARK_REFERENCE_WIDTH,
21854
+ height: 30,
21855
+ rx: 9,
21856
+ fill: paper,
21857
+ stroke: ink,
21858
+ strokeWidth: 2
21859
+ }
21860
+ ),
21861
+ "numbered-circle": () => /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
21862
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: 23, cy: -8, r: 13, fill: paper, stroke: ink, strokeWidth: 2.5 }),
21863
+ /* @__PURE__ */ jsxRuntime.jsx("text", { x: 23, y: -2, textAnchor: "middle", fontSize: 17, fontWeight: 700, fill: ink, children: "1" })
21864
+ ] }),
21865
+ station: () => /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: 23, cy: -8, r: 11, fill: paper, stroke: ink, strokeWidth: 4 }),
21866
+ arrow: () => /* @__PURE__ */ jsxRuntime.jsx(
21867
+ "path",
21868
+ {
21869
+ d: `M 0 -8 H 42 M 32 -18 L ${LEGEND_MARK_REFERENCE_WIDTH} -8 L 32 2`,
21870
+ fill: "none",
21871
+ stroke: ink,
21872
+ strokeWidth: 3,
21873
+ strokeLinecap: "round",
21874
+ strokeLinejoin: "round"
21875
+ }
21876
+ )
21877
+ };
21878
+ function CdlLegend({ items }) {
21879
+ return /* @__PURE__ */ jsxRuntime.jsx("g", { "data-cdl-role": "legend", children: items.map((item, index) => {
21880
+ const Mark = LEGEND_MARK_RENDERERS[item.mark];
21881
+ return /* @__PURE__ */ jsxRuntime.jsxs(
21882
+ "g",
21883
+ {
21884
+ "data-cdl-role": "legend-item",
21885
+ "data-cdl-y": item.y,
21886
+ transform: `translate(${item.x} ${item.y})`,
21887
+ children: [
21888
+ /* @__PURE__ */ jsxRuntime.jsx(
21889
+ "g",
21890
+ {
21891
+ "data-cdl-role": "legend-mark",
21892
+ "data-cdl-legend-mark": item.mark,
21893
+ transform: `scale(${LEGEND_MARK_SCALE})`,
21894
+ children: /* @__PURE__ */ jsxRuntime.jsx(Mark, {})
21895
+ }
21896
+ ),
21897
+ /* @__PURE__ */ jsxRuntime.jsx(
21898
+ "text",
21899
+ {
21900
+ "data-cdl-role": "legend-text",
21901
+ x: LEGEND_MARK_WIDTH + LEGEND_MARK_TEXT_GAP,
21902
+ y: 0,
21903
+ fontSize: LEGEND_FONT_SIZE,
21904
+ fill: "var(--cdl-text-mute, #4d7187)",
21905
+ children: item.text
21906
+ }
21907
+ )
21908
+ ]
21909
+ },
21910
+ `${item.mark}-${index}`
21911
+ );
21912
+ }) });
21913
+ }
21441
21914
 
21442
21915
  // src/render/relation-focus.ts
21443
21916
  function relatedFocusSet(edges, nodeId) {
@@ -21450,10 +21923,17 @@ function relatedFocusSet(edges, nodeId) {
21450
21923
  }
21451
21924
  return \u96C6\u5408;
21452
21925
  }
21926
+ var METRO_LANE_BADGE_WIDTH = 200;
21927
+ var METRO_LANE_BADGE_HEIGHT = 88;
21928
+ var METRO_LANE_BADGE_INSET = 24;
21929
+ var METRO_LANE_GUIDE_GAP = 24;
21453
21930
  function \u63CF\u304F\u3082\u306E\u304C\u3042\u308B(laid) {
21931
+ if (laid.legend && laid.legend.length > 0) return true;
21454
21932
  if (laid.edges.length > 0) return true;
21455
21933
  if (laid.nodes.some((n) => isRenderedNode(n))) return true;
21456
- return laid.lanes.some((l) => l.contain === true || l.lifeline === true || Boolean(l.label));
21934
+ return laid.lanes.some(
21935
+ (l) => l.contain === true || l.lifeline === true || Boolean(l.label) || l.stage !== void 0 || l.metro !== void 0
21936
+ );
21457
21937
  }
21458
21938
  function CdlStage({
21459
21939
  laid,
@@ -21494,6 +21974,7 @@ function CdlStage({
21494
21974
  [\u95A2\u4FC2\u3092\u5149\u3089\u305B\u308B, \u7126\u70B9, laid.edges]
21495
21975
  );
21496
21976
  const activeSet = \u7126\u70B9\u306E\u96C6\u5408 ?? new Set(currentPhase?.activate ?? []);
21977
+ const \u8DEF\u7DDA\u56F3 = laid.edges.some((edge) => edge.routing === "metro") && laid.nodes.some((node) => node.kind === "station");
21497
21978
  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;
21498
21979
  const \u77E2\u5370\u306E\u51FA\u756A = react.useMemo(() => {
21499
21980
  const \u521D\u51FA = /* @__PURE__ */ new Map();
@@ -21522,9 +22003,15 @@ function CdlStage({
21522
22003
  const first = \u521D\u51FA.get(edgeId);
21523
22004
  if (first === void 0) return false;
21524
22005
  return \u4ECA === first;
22006
+ },
22007
+ /** 初出の段より前にある区間だけを、通過前として選べるようにする。 */
22008
+ \u307E\u3060\u304B: (edgeId) => {
22009
+ const first = \u521D\u51FA.get(edgeId);
22010
+ return first !== void 0 && \u4ECA >= 0 && \u4ECA < first;
21525
22011
  }
21526
22012
  };
21527
22013
  }, [laid.phases, currentPhase, laid.edgeReveal]);
22014
+ const \u7DDA\u3092\u63CF\u304F = (edge) => edge.routing === "metro" || \u77E2\u5370\u306E\u51FA\u756A.\u51FA\u308B\u304B(edge.id);
21528
22015
  const drawSet = new Set(currentPhase?.draw ?? []);
21529
22016
  const chartLineScales = react.useMemo(() => {
21530
22017
  const nodes = laid.nodes.filter((node) => node.kind === "chart-line");
@@ -21612,7 +22099,53 @@ function CdlStage({
21612
22099
  )
21613
22100
  );
21614
22101
  }, [laid]);
22102
+ const metroReturnArrowMarkers = react.useMemo(() => {
22103
+ const tones = new Set(
22104
+ laid.edges.filter((edge) => edge.metroReturn === true).map((edge) => edge.tone)
22105
+ );
22106
+ return [...tones].map((tone) => /* @__PURE__ */ jsxRuntime.jsx(
22107
+ "marker",
22108
+ {
22109
+ id: metroReturnArrowMarkerId(tone),
22110
+ viewBox: "0 -9.5 21 19",
22111
+ refX: "0",
22112
+ refY: "0",
22113
+ markerWidth: "21",
22114
+ markerHeight: "19",
22115
+ markerUnits: "userSpaceOnUse",
22116
+ orient: "auto",
22117
+ children: /* @__PURE__ */ jsxRuntime.jsx(
22118
+ "path",
22119
+ {
22120
+ "data-cdl-role": "edge-arrowhead",
22121
+ "data-cdl-mark": "metro-return-arrow",
22122
+ d: METRO_RETURN_ARROW_D,
22123
+ fill: TONE[tone]
22124
+ }
22125
+ )
22126
+ },
22127
+ tone
22128
+ ));
22129
+ }, [laid.edges]);
21615
22130
  const diagramScale = laid.diagramScale ?? 1;
22131
+ const stageLaneIds = new Set(
22132
+ laid.lanes.filter((lane) => lane.stage !== void 0).map((lane) => lane.id)
22133
+ );
22134
+ const stageNoteNodes = laid.nodes.filter(
22135
+ (node) => node.kind === "card" && (stageLaneIds.has(node.lane) || node.leaderTo !== void 0) && isRenderedNode(node) && Boolean(node.subtitle)
22136
+ );
22137
+ const stageNoteNodeIds = new Set(stageNoteNodes.map((node) => node.id));
22138
+ const nodesById = new Map(laid.nodes.map((node) => [node.id, node]));
22139
+ const metroDecisionIds = /* @__PURE__ */ new Set();
22140
+ const stationNamesRight = /* @__PURE__ */ new Set();
22141
+ for (const edge of laid.edges) {
22142
+ if (edge.routing !== "metro") continue;
22143
+ if (nodesById.get(edge.from)?.kind === "decision") metroDecisionIds.add(edge.from);
22144
+ if (nodesById.get(edge.to)?.kind === "decision") metroDecisionIds.add(edge.to);
22145
+ if (edge.toSide === "top" && nodesById.get(edge.to)?.kind === "station") {
22146
+ stationNamesRight.add(edge.to);
22147
+ }
22148
+ }
21616
22149
  if (!\u63CF\u304F\u3082\u306E\u304C\u3042\u308B(laid)) return null;
21617
22150
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: compact ? "px-3 py-3" : "px-6 py-6", style: compact ? void 0 : { minHeight: 460 }, children: [
21618
22151
  /* @__PURE__ */ jsxRuntime.jsxs(
@@ -21632,6 +22165,7 @@ function CdlStage({
21632
22165
  children: [
21633
22166
  /* @__PURE__ */ jsxRuntime.jsxs("defs", { children: [
21634
22167
  edgeHeadMarkers,
22168
+ metroReturnArrowMarkers,
21635
22169
  /* @__PURE__ */ jsxRuntime.jsx(
21636
22170
  "marker",
21637
22171
  {
@@ -21668,7 +22202,61 @@ function CdlStage({
21668
22202
  pointerEvents: "none"
21669
22203
  }
21670
22204
  ),
21671
- lane.contain && /* @__PURE__ */ jsxRuntime.jsx(
22205
+ lane.stage && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
22206
+ /* @__PURE__ */ jsxRuntime.jsx(
22207
+ "rect",
22208
+ {
22209
+ "data-cdl-role": "stage-column",
22210
+ x: lane.x + STAGE_COLUMN_GAP / 2,
22211
+ y: lane.y,
22212
+ width: lane.width - STAGE_COLUMN_GAP,
22213
+ height: lane.height,
22214
+ rx: 16,
22215
+ fill: "var(--cdl-stage-bg, #fafafa)",
22216
+ fillOpacity: 0.5,
22217
+ stroke: "var(--cdl-text-mute, #8a8678)",
22218
+ strokeWidth: 1.5
22219
+ }
22220
+ ),
22221
+ /* @__PURE__ */ jsxRuntime.jsx(
22222
+ "text",
22223
+ {
22224
+ "data-cdl-role": "stage-number",
22225
+ x: lane.x + STAGE_COLUMN_GAP / 2 + 20,
22226
+ y: lane.y + 24,
22227
+ fontSize: 14,
22228
+ fontWeight: 500,
22229
+ letterSpacing: 1.5,
22230
+ fill: "var(--cdl-text-mute, #8a8678)",
22231
+ children: `\u6BB5\u968E ${lane.stage.number}`
22232
+ }
22233
+ ),
22234
+ /* @__PURE__ */ jsxRuntime.jsx(
22235
+ "text",
22236
+ {
22237
+ "data-cdl-role": "stage-name",
22238
+ x: lane.x + STAGE_COLUMN_GAP / 2 + 20,
22239
+ y: lane.y + 55,
22240
+ fontSize: 22,
22241
+ fontWeight: 700,
22242
+ fill: "var(--cdl-text, #1a1f2a)",
22243
+ children: lane.stage.name
22244
+ }
22245
+ ),
22246
+ /* @__PURE__ */ jsxRuntime.jsx(
22247
+ "line",
22248
+ {
22249
+ "data-cdl-role": "stage-divider",
22250
+ x1: lane.x + STAGE_COLUMN_GAP / 2,
22251
+ y1: lane.y + 72,
22252
+ x2: lane.x + lane.width - STAGE_COLUMN_GAP / 2,
22253
+ y2: lane.y + 72,
22254
+ stroke: "var(--cdl-divider, #d4d4d8)",
22255
+ strokeWidth: 1.5
22256
+ }
22257
+ )
22258
+ ] }),
22259
+ lane.contain && lane.stage === void 0 && /* @__PURE__ */ jsxRuntime.jsx(
21672
22260
  "rect",
21673
22261
  {
21674
22262
  "data-cdl-role": "lane-container",
@@ -21704,7 +22292,85 @@ function CdlStage({
21704
22292
  }
21705
22293
  );
21706
22294
  })(),
21707
- lane.label && /* @__PURE__ */ jsxRuntime.jsx(
22295
+ lane.timelineAxis && (() => {
22296
+ const ys = laid.nodes.filter((node) => node.lane === lane.id && isRenderedNode(node)).map((node) => node.cy);
22297
+ if (ys.length < 2) return null;
22298
+ const y1 = Math.min(...ys);
22299
+ const y2 = Math.max(...ys);
22300
+ if (y2 <= y1) return null;
22301
+ return /* @__PURE__ */ jsxRuntime.jsx(
22302
+ "line",
22303
+ {
22304
+ "data-cdl-role": "timeline-axis",
22305
+ x1: lane.x + lane.width / 2,
22306
+ y1,
22307
+ x2: lane.x + lane.width / 2,
22308
+ y2,
22309
+ stroke: "var(--cdl-tone-accent, #2d6a8f)",
22310
+ strokeWidth: TIMELINE_NUMBER_STROKE
22311
+ }
22312
+ );
22313
+ })(),
22314
+ lane.metro !== void 0 && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
22315
+ /* @__PURE__ */ jsxRuntime.jsx(
22316
+ "line",
22317
+ {
22318
+ "data-cdl-role": "metro-lane-guide",
22319
+ x1: lane.x + METRO_LANE_BADGE_INSET + METRO_LANE_BADGE_WIDTH + METRO_LANE_GUIDE_GAP,
22320
+ y1: lane.y + lane.height / 2,
22321
+ x2: lane.x + lane.width - METRO_LANE_BADGE_INSET,
22322
+ y2: lane.y + lane.height / 2,
22323
+ stroke: "var(--cdl-line-mute, #a89d8f)",
22324
+ strokeWidth: 2,
22325
+ strokeDasharray: "22 7 5 7",
22326
+ opacity: 0.45
22327
+ }
22328
+ ),
22329
+ /* @__PURE__ */ jsxRuntime.jsxs(
22330
+ "g",
22331
+ {
22332
+ "data-cdl-role": "metro-lane-badge",
22333
+ transform: `translate(${lane.x + METRO_LANE_BADGE_INSET} ${lane.y + lane.height / 2 - METRO_LANE_BADGE_HEIGHT / 2})`,
22334
+ children: [
22335
+ /* @__PURE__ */ jsxRuntime.jsx(
22336
+ "rect",
22337
+ {
22338
+ width: METRO_LANE_BADGE_WIDTH,
22339
+ height: METRO_LANE_BADGE_HEIGHT,
22340
+ rx: 12,
22341
+ fill: "var(--cdl-node-fill, #ffffff)",
22342
+ stroke: "var(--cdl-tone-accent, #2d6a8f)",
22343
+ strokeWidth: 1.25
22344
+ }
22345
+ ),
22346
+ /* @__PURE__ */ jsxRuntime.jsx(
22347
+ "text",
22348
+ {
22349
+ x: METRO_LANE_BADGE_WIDTH / 2,
22350
+ y: lane.metro.subtitle ? 37 : 53,
22351
+ fontSize: 26,
22352
+ fontWeight: 700,
22353
+ fill: "var(--cdl-text, #1a1f2a)",
22354
+ textAnchor: "middle",
22355
+ children: lane.label
22356
+ }
22357
+ ),
22358
+ lane.metro.subtitle && /* @__PURE__ */ jsxRuntime.jsx(
22359
+ "text",
22360
+ {
22361
+ x: METRO_LANE_BADGE_WIDTH / 2,
22362
+ y: 67,
22363
+ fontSize: 18,
22364
+ fill: "var(--cdl-text-dim, #5a6270)",
22365
+ textAnchor: "middle",
22366
+ children: lane.metro.subtitle
22367
+ }
22368
+ )
22369
+ ]
22370
+ }
22371
+ )
22372
+ ] }),
22373
+ lane.label && lane.metro === void 0 && /* @__PURE__ */ jsxRuntime.jsx(
21708
22374
  "text",
21709
22375
  {
21710
22376
  "data-cdl-role": "lane-label",
@@ -21721,7 +22387,34 @@ function CdlStage({
21721
22387
  },
21722
22388
  lane.id
21723
22389
  )),
21724
- laid.edges.filter((edge) => \u77E2\u5370\u306E\u51FA\u756A.\u51FA\u308B\u304B(edge.id)).map((edge) => /* @__PURE__ */ jsxRuntime.jsx(
22390
+ laid.nodes.map((node) => {
22391
+ if (node.leaderTo === void 0) return null;
22392
+ const target = nodesById.get(node.leaderTo);
22393
+ if (target === void 0) return null;
22394
+ if (!isRenderedNode(node) || !isRenderedNode(target)) return null;
22395
+ const targetIsRight = target.cx >= node.cx;
22396
+ const x1 = node.cx + (targetIsRight ? node.w / 2 : -node.w / 2);
22397
+ const x2 = target.cx + (targetIsRight ? -target.w / 2 : target.w / 2);
22398
+ if (targetIsRight ? x2 <= x1 : x1 <= x2) return null;
22399
+ return /* @__PURE__ */ jsxRuntime.jsx(
22400
+ "line",
22401
+ {
22402
+ "data-cdl-role": "timeline-leader",
22403
+ "data-cdl-leader-from": node.id,
22404
+ "data-cdl-leader-to": node.leaderTo,
22405
+ x1,
22406
+ y1: node.cy,
22407
+ x2,
22408
+ y2: target.cy,
22409
+ stroke: "var(--cdl-line-mute, #a89d8f)",
22410
+ strokeWidth: 1.5,
22411
+ strokeDasharray: "2 4",
22412
+ strokeLinecap: "round"
22413
+ },
22414
+ `timeline-leader-${node.id}`
22415
+ );
22416
+ }),
22417
+ laid.edges.filter(\u7DDA\u3092\u63CF\u304F).map((edge) => /* @__PURE__ */ jsxRuntime.jsx(
21725
22418
  CdlEdgeView,
21726
22419
  {
21727
22420
  edge,
@@ -21729,16 +22422,19 @@ function CdlStage({
21729
22422
  flowing: \u7126\u70B9\u306E\u96C6\u5408?.has(edge.id) === true && !\u52D5\u304D\u3092\u63A7\u3048\u308B,
21730
22423
  drawing: \u77E2\u5370\u306E\u51FA\u756A.\u4F38\u3070\u3059\u304B(edge.id),
21731
22424
  progress: \u77E2\u5370\u306E\u51FA\u756A.\u4F38\u3070\u3059\u304B(edge.id) ? \u63CF\u304F\u9032\u307F : progress,
21732
- stateValues
22425
+ stateValues,
22426
+ pending: edge.routing === "metro" && \u77E2\u5370\u306E\u51FA\u756A.\u307E\u3060\u304B(edge.id)
21733
22427
  },
21734
22428
  edge.id
21735
22429
  )),
21736
22430
  laid.nodes.map((node) => {
21737
22431
  const \u63CF\u304F = drawSet.has(node.id);
22432
+ const \u6BB5\u968E\u306E\u88DC\u8DB3 = stageNoteNodeIds.has(node.id);
21738
22433
  const view = /* @__PURE__ */ jsxRuntime.jsx(
21739
22434
  CdlNodeView,
21740
22435
  {
21741
- node,
22436
+ node: \u6BB5\u968E\u306E\u88DC\u8DB3 ? { ...node, subtitle: void 0 } : node,
22437
+ compactTitleInset: \u6BB5\u968E\u306E\u88DC\u8DB3 ? STAGE_CARD_TEXT_INSET : void 0,
21742
22438
  active: activeSet.has(node.id),
21743
22439
  lang: laid.lang,
21744
22440
  textMetrics: laid.textMetrics,
@@ -21748,13 +22444,32 @@ function CdlStage({
21748
22444
  stateValues,
21749
22445
  \u5024\u306E\u540D\u524D,
21750
22446
  currentPhaseId: currentPhase?.id,
21751
- chartLineScale: chartLineScales.get(node.id)
22447
+ chartLineScale: chartLineScales.get(node.id),
22448
+ metroDiagram: \u8DEF\u7DDA\u56F3,
22449
+ metroDecision: metroDecisionIds.has(node.id),
22450
+ stationNameRight: stationNamesRight.has(node.id)
21752
22451
  }
21753
22452
  );
21754
22453
  return /* @__PURE__ */ jsxRuntime.jsx("g", { children: view }, node.id);
21755
22454
  }),
21756
- 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}`)),
21757
- laid.edges.filter((edge) => \u77E2\u5370\u306E\u51FA\u756A.\u51FA\u308B\u304B(edge.id)).map((edge) => /* @__PURE__ */ jsxRuntime.jsx(
22455
+ stageNoteNodes.map((node) => /* @__PURE__ */ jsxRuntime.jsx(
22456
+ "text",
22457
+ {
22458
+ "data-cdl-role": "stage-note",
22459
+ "data-cdl-stage-node": node.id,
22460
+ x: node.cx + node.w / 2 - STAGE_CARD_TEXT_INSET,
22461
+ y: node.cy + 1,
22462
+ fontSize: 14,
22463
+ fontWeight: 500,
22464
+ textAnchor: "end",
22465
+ dominantBaseline: "middle",
22466
+ fill: "var(--cdl-text-mute, #8a8678)",
22467
+ children: interpolate(node.subtitle, stateValues, \u5024\u306E\u540D\u524D)
22468
+ },
22469
+ `stage-note-${node.id}`
22470
+ )),
22471
+ 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}`)),
22472
+ laid.edges.filter(\u7DDA\u3092\u63CF\u304F).map((edge) => /* @__PURE__ */ jsxRuntime.jsx(
21758
22473
  CdlEdgeLabelView,
21759
22474
  {
21760
22475
  edge,
@@ -21763,7 +22478,7 @@ function CdlStage({
21763
22478
  },
21764
22479
  `label-${edge.id}`
21765
22480
  )),
21766
- laid.edges.filter((edge) => (edge.headLabel || edge.tailLabel) && \u77E2\u5370\u306E\u51FA\u756A.\u51FA\u308B\u304B(edge.id)).flatMap(
22481
+ laid.edges.filter((edge) => (edge.headLabel || edge.tailLabel) && \u7DDA\u3092\u63CF\u304F(edge)).flatMap(
21767
22482
  (edge) => ["head", "tail"].map((\u7AEF) => /* @__PURE__ */ jsxRuntime.jsx(
21768
22483
  CdlEdgeEndLabelView,
21769
22484
  {
@@ -21775,6 +22490,7 @@ function CdlStage({
21775
22490
  `${\u7AEF}-label-${edge.id}`
21776
22491
  ))
21777
22492
  ),
22493
+ laid.legend && /* @__PURE__ */ jsxRuntime.jsx(CdlLegend, { items: laid.legend }),
21778
22494
  debug && /* @__PURE__ */ jsxRuntime.jsxs("g", { pointerEvents: "none", children: [
21779
22495
  laid.bboxes.map((bbox, idx) => {
21780
22496
  const color = bbox.kind === "node" ? "#3b82f6" : bbox.kind === "lane-label" ? "#a855f7" : bbox.kind === "edge-label" ? "#10b981" : "#f59e0b";