@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/react.js CHANGED
@@ -1004,7 +1004,7 @@ function drawsTitleOutsideBox(kind) {
1004
1004
  }
1005
1005
  var TITLE_FONT_SIZE_DEFAULT = 18;
1006
1006
  var TITLE_FONT_SIZE = new Map([
1007
- ...["dyn-rect", "dyn-circle", "dyn-arc", "dyn-wave", "dyn-polygon", "station"].map(
1007
+ ...["dyn-rect", "dyn-circle", "dyn-arc", "dyn-wave", "dyn-polygon"].map(
1008
1008
  (k) => [k, 13]
1009
1009
  ),
1010
1010
  ...[
@@ -1049,8 +1049,10 @@ var TITLE_FONT_SIZE = new Map([
1049
1049
  "webhook"
1050
1050
  ].map((k) => [k, 20]),
1051
1051
  ...["card", "event", "function"].map((k) => [k, 24]),
1052
+ ["station", 25],
1052
1053
  ["actor", 32],
1053
- ["storage", 34]
1054
+ ["storage", 34],
1055
+ ["timeline-number", 18]
1054
1056
  ]);
1055
1057
  function titleFontSize(kind) {
1056
1058
  return TITLE_FONT_SIZE.get(kind ?? "") ?? TITLE_FONT_SIZE_DEFAULT;
@@ -1060,9 +1062,17 @@ function skipsNodeTitleWidthCheck(kind) {
1060
1062
  return KINDS_WITH_TITLE_WIDTH_OUTSIDE_NODE.has(kind ?? "");
1061
1063
  }
1062
1064
  var TITLE_BOX_PADDING = 52;
1065
+ var TIMELINE_NUMBER_STROKE = 4;
1066
+ function titleUsableWidth(kind, w, h) {
1067
+ if (kind === "timeline-number") {
1068
+ return Math.min(w, h) - 2 * TIMELINE_NUMBER_STROKE;
1069
+ }
1070
+ return w - (drawsTitleOutsideBox(kind) ? 0 : TITLE_BOX_PADDING);
1071
+ }
1063
1072
  var KINDS_SCALING_TITLE_WHEN_LOW = /* @__PURE__ */ new Set(["actor", "event", "function", "storage"]);
1064
1073
  var KINDS_SCALING_TITLE_ALWAYS = /* @__PURE__ */ new Set([
1065
1074
  "card",
1075
+ "timeline-number",
1066
1076
  "shape-file",
1067
1077
  "shape-folder",
1068
1078
  "shape-cloud",
@@ -1146,14 +1156,16 @@ var KINDS_DRAWING_ONLY_SHAPE = [
1146
1156
  ];
1147
1157
  var KINDS_WITHOUT_EYEBROW_TEXT = /* @__PURE__ */ new Set([
1148
1158
  ...KINDS_DRAWING_ONLY_SHAPE,
1149
- ...KINDS_WITHOUT_TITLE_TEXT
1159
+ ...KINDS_WITHOUT_TITLE_TEXT,
1160
+ "timeline-number"
1150
1161
  ]);
1151
1162
  function rendersEyebrow(kind) {
1152
1163
  return !KINDS_WITHOUT_EYEBROW_TEXT.has(kind ?? "");
1153
1164
  }
1154
1165
  var KINDS_WITHOUT_SUBTITLE_TEXT = /* @__PURE__ */ new Set([
1155
1166
  ...KINDS_DRAWING_OWN_PAYLOAD,
1156
- ...KINDS_WITHOUT_TITLE_TEXT
1167
+ ...KINDS_WITHOUT_TITLE_TEXT,
1168
+ "timeline-number"
1157
1169
  ]);
1158
1170
  function rendersSubtitle(kind) {
1159
1171
  return !KINDS_WITHOUT_SUBTITLE_TEXT.has(kind ?? "");
@@ -3087,6 +3099,14 @@ function curvePath(from, to, edge, labelMarginExtra = 0, textMetrics = "proporti
3087
3099
 
3088
3100
  // src/layout/metro-path.ts
3089
3101
  var METRO_CORNER_RADIUS = 24;
3102
+ var METRO_BRANCH_CORNER_RADIUS = 22;
3103
+ var METRO_BRANCH_STUB = 30;
3104
+ var METRO_DECISION_HEIGHT = 88;
3105
+ var METRO_DECISION_WIDTH = 116;
3106
+ var METRO_RETURN_END_OFFSET = 43;
3107
+ var METRO_RETURN_START_OFFSET = 24;
3108
+ var METRO_LINE_WIDTH = 10;
3109
+ var METRO_LINE_WIDTH_CSS = `var(--cdl-metro-line-width, ${METRO_LINE_WIDTH}px)`;
3090
3110
  var T = Math.SQRT2 - 1;
3091
3111
  function round2(v) {
3092
3112
  return Math.round(v * 100) / 100;
@@ -3104,7 +3124,63 @@ function polyline(points) {
3104
3124
  });
3105
3125
  return distinct.map((p, i) => `${i === 0 ? "M" : "L"} ${point(p)}`).join(" ");
3106
3126
  }
3107
- function metroPath(from, to, edge, labelMarginExtra = 0, textMetrics = "proportional") {
3127
+ function quadraticCornerPath(start, corner, end, radius) {
3128
+ const horizontalDir = Math.sign(corner.x - start.x);
3129
+ const verticalDir = Math.sign(end.y - corner.y);
3130
+ const r = Math.min(
3131
+ radius,
3132
+ Math.abs(corner.x - start.x),
3133
+ Math.abs(end.y - corner.y)
3134
+ );
3135
+ const before = { x: corner.x - horizontalDir * r, y: corner.y };
3136
+ const after = { x: corner.x, y: corner.y + verticalDir * r };
3137
+ return [
3138
+ `M ${point(start)}`,
3139
+ `L ${point(before)}`,
3140
+ `Q ${point(corner)} ${point(after)}`,
3141
+ `L ${point(end)}`
3142
+ ].join(" ");
3143
+ }
3144
+ function decisionBranchPath(start, end) {
3145
+ const horizontalDir = Math.sign(end.x - start.x);
3146
+ const verticalDir = Math.sign(end.y - start.y);
3147
+ const turnX = start.x + horizontalDir * (METRO_BRANCH_STUB + METRO_BRANCH_CORNER_RADIUS);
3148
+ const radius = Math.min(
3149
+ METRO_BRANCH_CORNER_RADIUS,
3150
+ Math.abs(end.y - start.y) / 2,
3151
+ Math.abs(end.x - turnX)
3152
+ );
3153
+ const firstCorner = { x: turnX, y: start.y };
3154
+ const secondCorner = { x: turnX, y: end.y };
3155
+ const beforeFirst = {
3156
+ x: turnX - horizontalDir * radius,
3157
+ y: start.y
3158
+ };
3159
+ const afterFirst = {
3160
+ x: turnX,
3161
+ y: start.y + verticalDir * radius
3162
+ };
3163
+ const beforeSecond = {
3164
+ x: turnX,
3165
+ y: end.y - verticalDir * radius
3166
+ };
3167
+ const afterSecond = {
3168
+ x: turnX + horizontalDir * radius,
3169
+ y: end.y
3170
+ };
3171
+ return {
3172
+ d: [
3173
+ `M ${point(start)}`,
3174
+ `L ${point(beforeFirst)}`,
3175
+ `Q ${point(firstCorner)} ${point(afterFirst)}`,
3176
+ `L ${point(beforeSecond)}`,
3177
+ `Q ${point(secondCorner)} ${point(afterSecond)}`,
3178
+ `L ${point(end)}`
3179
+ ].join(" "),
3180
+ turnX
3181
+ };
3182
+ }
3183
+ function metroPath(from, to, edge, _labelMarginExtra = 0, _textMetrics = "proportional") {
3108
3184
  const fromLeft = from.cx - from.w / 2;
3109
3185
  const fromRight = from.cx + from.w / 2;
3110
3186
  const toLeft = to.cx - to.w / 2;
@@ -3139,11 +3215,45 @@ function metroPath(from, to, edge, labelMarginExtra = 0, textMetrics = "proporti
3139
3215
  const sgn = Math.sign(dy);
3140
3216
  let d;
3141
3217
  let mid;
3142
- let tangent;
3143
- if (Math.abs(dy) < SAME_HEIGHT_THRESHOLD) {
3218
+ let metroReturn = false;
3219
+ if (from.kind === "decision" && Math.abs(dy) >= SAME_HEIGHT_THRESHOLD) {
3220
+ const verticalDir = Math.sign(dy);
3221
+ if (Math.abs(to.cx - from.cx) < SAME_HEIGHT_THRESHOLD) {
3222
+ const start = { x: from.cx, y: from.cy + verticalDir * from.h / 2 };
3223
+ const end = { x: to.cx, y: to.cy };
3224
+ d = polyline([start, end]);
3225
+ fromSide = verticalDir < 0 ? "top" : "bottom";
3226
+ toSide = verticalDir < 0 ? "bottom" : "top";
3227
+ mid = { x: start.x, y: (start.y + end.y) / 2 };
3228
+ } else {
3229
+ const horizontalDir = Math.sign(to.cx - from.cx);
3230
+ const start = {
3231
+ x: from.cx + horizontalDir * from.w / 2,
3232
+ y: from.cy
3233
+ };
3234
+ const end = {
3235
+ x: to.cx - horizontalDir * to.w / 2,
3236
+ y: to.cy
3237
+ };
3238
+ const branch = decisionBranchPath(start, end);
3239
+ d = branch.d;
3240
+ fromSide = horizontalDir > 0 ? "right" : "left";
3241
+ toSide = horizontalDir > 0 ? "left" : "right";
3242
+ mid = { x: branch.turnX, y: (start.y + end.y) / 2 };
3243
+ }
3244
+ } else if (edge?.style === "dashed" && from.kind === "station" && to.kind === "station" && to.cx < from.cx && Math.abs(dy) >= SAME_HEIGHT_THRESHOLD) {
3245
+ const verticalDir = Math.sign(to.cy - from.cy);
3246
+ const start = { x: from.cx - METRO_RETURN_START_OFFSET, y: from.cy };
3247
+ const end = { x: to.cx, y: to.cy - verticalDir * METRO_RETURN_END_OFFSET };
3248
+ const corner = { x: end.x, y: start.y };
3249
+ fromSide = "left";
3250
+ toSide = verticalDir < 0 ? "bottom" : "top";
3251
+ d = quadraticCornerPath(start, corner, end, METRO_BRANCH_CORNER_RADIUS);
3252
+ mid = { x: (start.x + corner.x) / 2, y: start.y };
3253
+ metroReturn = true;
3254
+ } else if (Math.abs(dy) < SAME_HEIGHT_THRESHOLD) {
3144
3255
  d = `M ${point({ x: x1, y: y1 })} L ${point({ x: x2, y: y2 })}`;
3145
3256
  mid = { x: (x1 + x2) / 2, y: (y1 + y2) / 2 };
3146
- tangent = { x: x2 - x1, y: y2 - y1 };
3147
3257
  } else if (fromSide !== toSide && Math.abs(x2 - x1) >= Math.abs(dy)) {
3148
3258
  const horizontal = Math.abs(x2 - x1);
3149
3259
  const s = (horizontal - Math.abs(dy)) / 2;
@@ -3173,7 +3283,6 @@ function metroPath(from, to, edge, labelMarginExtra = 0, textMetrics = "proporti
3173
3283
  d = polyline([p0, p1, p2, p3]);
3174
3284
  }
3175
3285
  mid = { x: (x1 + x2) / 2, y: (y1 + y2) / 2 };
3176
- tangent = w;
3177
3286
  } else {
3178
3287
  fromSide = "right";
3179
3288
  toSide = "right";
@@ -3203,28 +3312,15 @@ function metroPath(from, to, edge, labelMarginExtra = 0, textMetrics = "proporti
3203
3312
  `L ${point(p4)}`
3204
3313
  ].join(" ");
3205
3314
  mid = { x: p2.x - T * r, y: p2.y };
3206
- tangent = { x: 0, y: sgn };
3207
- }
3208
- if (edge?.overlay) {
3209
- return { d, fromSide, toSide, labelX: mid.x, labelY: mid.y, labelAnchor: "middle" };
3210
3315
  }
3211
- const tangentLength = Math.hypot(tangent.x, tangent.y);
3212
- let normalX = tangent.y / tangentLength;
3213
- let normalY = -tangent.x / tangentLength;
3214
- if (normalY > Number.EPSILON || Math.abs(normalY) <= Number.EPSILON && normalX < 0) {
3215
- normalX *= -1;
3216
- normalY *= -1;
3217
- }
3218
- const labelW = computeLabelBoxW(edge?.label, edge?.sub, textMetrics);
3219
- const labelH = labelPillH(edge?.sub);
3220
- const distance = labelPathClearanceHorizontal(edge) + (Math.abs(normalX) * labelW + Math.abs(normalY) * labelH) / 2 + labelMarginExtra;
3221
3316
  return {
3222
3317
  d,
3223
3318
  fromSide,
3224
3319
  toSide,
3225
- labelX: mid.x + normalX * distance,
3226
- labelY: mid.y + normalY * distance,
3227
- labelAnchor: "middle"
3320
+ labelX: mid.x,
3321
+ labelY: mid.y,
3322
+ labelAnchor: "middle",
3323
+ ...metroReturn ? { metroReturn: true } : {}
3228
3324
  };
3229
3325
  }
3230
3326
 
@@ -3351,12 +3447,14 @@ var NODE_KINDS = [
3351
3447
  "shape-lawyer",
3352
3448
  "shape-trader",
3353
3449
  "shape-customer-service",
3354
- // 流れの始まりと終わりの印 2 種 (#560) ... 塗った丸と輪で囲んだ丸。
3355
- // 状態遷移図が使う。 印そのものが意味を持つので文字は描かない
3450
+ // 流れの始まりと終わりの印 2 種 (#560) ... 始点と終点は線の端から推測せず、著者が
3451
+ // `kind: "mark-start"` / `kind: "mark-end"` の node を明示する。印は文字を描かない
3356
3452
  "mark-start",
3357
3453
  "mark-end",
3358
3454
  // 路線図の駅 (#996) ... 線路上の点を小さな丸で示し、名前と説明は丸の外に置く
3359
3455
  "station",
3456
+ // 時間軸の番号 (#1017) ... 実線の軸上に番号を入れた丸を置く。路線図の駅とは別の意味を持つ。
3457
+ "timeline-number",
3360
3458
  // 時系列のやり取り (#578) ... 1 つの箱が図を丸ごと描く。 縦が時間で、参加者は箱を持たない
3361
3459
  "sequence-board"
3362
3460
  ];
@@ -6548,7 +6646,7 @@ function runAxes(laid, diag, violations, counts, push, profile, skippedAxes, \u5
6548
6646
  const LIFELINE_W_THRESHOLD = 8;
6549
6647
  for (const n of laid.nodes) {
6550
6648
  if (n.w < LIFELINE_W_THRESHOLD) continue;
6551
- if (n.kind === "station") continue;
6649
+ if (n.kind === "station" || n.kind === "timeline-number") continue;
6552
6650
  if (n.w < MIN_NODE_W || n.h < MIN_NODE_H) {
6553
6651
  push(
6554
6652
  "node-visibility",
@@ -6574,8 +6672,7 @@ function runAxes(laid, diag, violations, counts, push, profile, skippedAxes, \u5
6574
6672
  if (!isRenderedNode(n)) continue;
6575
6673
  const titleTrimmed = n.title?.trim() ?? "";
6576
6674
  if (!titleTrimmed) continue;
6577
- const \u4F59\u767D = drawsTitleOutsideBox(n.kind) ? 0 : TITLE_BOX_PADDING;
6578
- const \u4F7F\u3048\u308B\u5E452 = n.w - \u4F59\u767D;
6675
+ const \u4F7F\u3048\u308B\u5E452 = titleUsableWidth(n.kind, n.w, n.h);
6579
6676
  const \u5B9F\u5E45 = measureTextWidth(titleTrimmed, {
6580
6677
  fontSize: titleFontSize(n.kind),
6581
6678
  bound: "drawn",
@@ -6594,7 +6691,7 @@ function runAxes(laid, diag, violations, counts, push, profile, skippedAxes, \u5
6594
6691
  push(
6595
6692
  "text-readability",
6596
6693
  // 字ごとの実幅は小数になるので、読む人に出す時だけ丸める (判定は丸めない)
6597
- `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)`,
6694
+ `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)`,
6598
6695
  "warn"
6599
6696
  );
6600
6697
  }
@@ -8154,6 +8251,8 @@ var TOKENS = {
8154
8251
  lane_label_h: 40,
8155
8252
  lane_top_pad: 100,
8156
8253
  contain_pad: 28,
8254
+ stage_column_gap: 48,
8255
+ stage_card_text_inset: 18,
8157
8256
  // node sizes (kind ごと、 unified row h は engine が max() で算出)
8158
8257
  node: {
8159
8258
  actor: { w: 340, h: 165 },
@@ -8280,7 +8379,10 @@ var TOKENS = {
8280
8379
  "mark-start": { w: 96, h: 96 },
8281
8380
  "mark-end": { w: 96, h: 96 },
8282
8381
  // 路線図の駅 (#996) ... 正方形にして小さな丸が縦横どちらにも歪まないようにする。
8283
- station: { w: 24, h: 24 },
8382
+ // 半径 16 と縁 6 の外径を、線路との接点を含めて収める。
8383
+ station: { w: 38, h: 38 },
8384
+ // 時間軸の番号 (#1017) ... 半径 24 の丸と太さ 4 の縁を収める。
8385
+ "timeline-number": { w: 52, h: 52 },
8284
8386
  // 時系列のやり取り (#578)。 実寸は中身から決まる (`sequenceBoardMetrics`) ので、
8285
8387
  // ここは書かなかった時の受け皿
8286
8388
  "sequence-board": { w: 960, h: 640 }
@@ -8295,6 +8397,8 @@ var LANE_TOP_PAD = TOKENS.lane_top_pad;
8295
8397
  var STACK_GAP = TOKENS.row_gap;
8296
8398
  var NODE_SIZE = TOKENS.node;
8297
8399
  var CONTAIN_PAD = TOKENS.contain_pad;
8400
+ var STAGE_COLUMN_GAP = TOKENS.stage_column_gap;
8401
+ var STAGE_CARD_TEXT_INSET = TOKENS.stage_card_text_inset;
8298
8402
  var VIEW_PAD = TOKENS.view_pad;
8299
8403
  var HEADER_RESERVE = TOKENS.header_reserve;
8300
8404
  var DEFAULT_LANE_GAP = 80;
@@ -8395,12 +8499,24 @@ function layoutEdges(diag, nodes, lanes = [], textMetrics = "proportional") {
8395
8499
  if (!from) throw new Error(`cdl layout: edge "${e.id}" \u306E from "${e.from}" \u304C node \u306B\u7121\u3044`);
8396
8500
  if (!to) throw new Error(`cdl layout: edge "${e.id}" \u306E to "${e.to}" \u304C node \u306B\u7121\u3044`);
8397
8501
  const c = curvePath(from, to, e, labelMarginExtra, textMetrics);
8502
+ const fromLane = lmap.get(from.lane);
8503
+ const toLane = lmap.get(to.lane);
8504
+ let labelX = c.labelX;
8505
+ if (fromLane?.stage !== void 0 && toLane?.stage !== void 0 && fromLane.id !== toLane.id && (e.label || e.sub)) {
8506
+ const [leftLane, rightLane] = fromLane.x <= toLane.x ? [fromLane, toLane] : [toLane, fromLane];
8507
+ const gapLeft = leftLane.x + leftLane.width - STAGE_COLUMN_GAP / 2;
8508
+ const gapRight = rightLane.x + STAGE_COLUMN_GAP / 2;
8509
+ const labelHalfWidth = computeLabelBoxW(e.label, e.sub, textMetrics) / 2;
8510
+ const minX = gapLeft + labelHalfWidth;
8511
+ const maxX = gapRight - labelHalfWidth;
8512
+ if (minX <= maxX) labelX = Math.min(maxX, Math.max(minX, labelX));
8513
+ }
8398
8514
  positionedEdges.push({
8399
8515
  ...e,
8400
8516
  d: c.d,
8401
8517
  fromSide: c.fromSide,
8402
8518
  toSide: c.toSide,
8403
- labelX: c.labelX + (e.labelOffsetX ?? 0),
8519
+ labelX: labelX + (e.labelOffsetX ?? 0),
8404
8520
  labelY: c.labelY + (e.labelOffsetY ?? 0),
8405
8521
  labelAnchor: c.labelAnchor
8406
8522
  });
@@ -8417,7 +8533,8 @@ function layoutEdges(diag, nodes, lanes = [], textMetrics = "proportional") {
8417
8533
  toSide: m.toSide,
8418
8534
  labelX: m.labelX + (e.labelOffsetX ?? 0),
8419
8535
  labelY: m.labelY + (e.labelOffsetY ?? 0),
8420
- labelAnchor: m.labelAnchor
8536
+ labelAnchor: m.labelAnchor,
8537
+ ...m.metroReturn ? { metroReturn: true } : {}
8421
8538
  });
8422
8539
  } else {
8423
8540
  autoEdges.push(e);
@@ -9105,6 +9222,57 @@ function bowSharedStraightPaths(edges, nodes, skipIds, textMetrics = "proportion
9105
9222
  function nodeRect(n) {
9106
9223
  return { x: n.cx - n.w / 2, y: n.cy - n.h / 2, w: n.w, h: n.h };
9107
9224
  }
9225
+ function countPathObstacleCrossings(d, obstacles) {
9226
+ const segments = flattenPathSegments(extractPathSegments(d));
9227
+ return obstacles.filter((obstacle) => segmentsCrossRect(segments, nodeRect(obstacle))).length;
9228
+ }
9229
+ function makeBackDetourCandidate(A, B, from, to, obstacles, detourY, side, columnXOverride) {
9230
+ const lowIsA = A.y >= B.y;
9231
+ const low = lowIsA ? A : B;
9232
+ const lowBendY = low.y - VERTICAL_DETOUR_EXIT;
9233
+ const bandTop = Math.min(detourY, lowBendY);
9234
+ const bandBottom = Math.max(detourY, lowBendY);
9235
+ const bandObstacles = obstacles.map(nodeRect).filter((rect) => rect.y + rect.h >= bandTop && rect.y <= bandBottom);
9236
+ let columnX = columnXOverride ?? detourColumnFor([from, to], side);
9237
+ if (columnXOverride === void 0) {
9238
+ while (true) {
9239
+ const blockers = bandObstacles.filter(
9240
+ (rect) => rect.x - VERTICAL_DETOUR_GAP < columnX && columnX < rect.x + rect.w + VERTICAL_DETOUR_GAP
9241
+ );
9242
+ if (blockers.length === 0) break;
9243
+ 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));
9244
+ }
9245
+ }
9246
+ const bendAY = lowIsA ? lowBendY : detourY;
9247
+ const bendBY = lowIsA ? detourY : lowBendY;
9248
+ const vertices = [
9249
+ A,
9250
+ { x: A.x, y: bendAY },
9251
+ { x: columnX, y: bendAY },
9252
+ { x: columnX, y: bendBY },
9253
+ { x: B.x, y: bendBY },
9254
+ B
9255
+ ];
9256
+ const d = verticesToPath(vertices);
9257
+ const high = lowIsA ? B : A;
9258
+ return {
9259
+ d,
9260
+ labelX: (high.x + columnX) / 2,
9261
+ crossings: countPathObstacleCrossings(d, obstacles),
9262
+ length: vertices.slice(1).reduce((sum, vertex, index) => {
9263
+ const previous = vertices[index];
9264
+ return sum + Math.hypot(vertex.x - previous.x, vertex.y - previous.y);
9265
+ }, 0)
9266
+ };
9267
+ }
9268
+ function chooseBackDetourCandidate(A, B, from, to, obstacles, detourY) {
9269
+ const left = makeBackDetourCandidate(A, B, from, to, obstacles, detourY, "left");
9270
+ const right = makeBackDetourCandidate(A, B, from, to, obstacles, detourY, "right");
9271
+ if (left.crossings !== right.crossings) return left.crossings < right.crossings ? left : right;
9272
+ if (Math.abs(left.length - right.length) >= 0.5) return left.length < right.length ? left : right;
9273
+ const preferred = Math.abs(A.x - B.x) < 0.5 ? "right" : A.x < B.x ? "left" : "right";
9274
+ return preferred === "left" ? left : right;
9275
+ }
9108
9276
  function rectGap(a, b) {
9109
9277
  const dx = Math.max(0, a.x - (b.x + b.w), b.x - (a.x + a.w));
9110
9278
  const dy = Math.max(0, a.y - (b.y + b.h), b.y - (a.y + a.h));
@@ -9182,7 +9350,7 @@ function sidePoint(n, side) {
9182
9350
  return { x: n.cx + n.w / 2, y: n.cy };
9183
9351
  }
9184
9352
  }
9185
- 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") {
9353
+ 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") {
9186
9354
  const labelBoxW = edge ? computeLabelBoxW(edge.label, edge.sub, textMetrics) : 0;
9187
9355
  const labelHalfW = labelBoxW / 2;
9188
9356
  const labelInit = labelInitClearance(edge);
@@ -9552,23 +9720,52 @@ function routePath(from, to, fromSide, toSide, obstacles = [], detourSide, _from
9552
9720
  };
9553
9721
  }
9554
9722
  if (topTopPair) {
9555
- const detourY = Math.min(A.y, B.y) - 100;
9723
+ const stagePoint = (point2, node, lane) => lane?.stage === void 0 ? point2 : {
9724
+ x: Math.min(node.cx + node.w / 2 - 24, lane.x + lane.width - 24),
9725
+ y: point2.y
9726
+ };
9727
+ const backA = stagePoint(A, from, fromLane);
9728
+ const backB = stagePoint(B, to, toLane);
9729
+ const stageTops = [fromLane, toLane].filter((lane) => lane?.stage !== void 0).map((lane) => lane.y);
9730
+ const detourY = Math.min(
9731
+ Math.min(backA.y, backB.y) - 100,
9732
+ ...stageTops.map((top) => top - VERTICAL_DETOUR_GAP)
9733
+ );
9556
9734
  const corner = 14;
9557
- const xDir = B.x > A.x ? 1 : -1;
9558
- return {
9559
- d: `M ${A.x} ${A.y}
9560
- L ${A.x} ${detourY + corner}
9561
- Q ${A.x} ${detourY}, ${A.x + xDir * corner} ${detourY}
9562
- L ${B.x - xDir * corner} ${detourY}
9563
- Q ${B.x} ${detourY}, ${B.x} ${detourY + corner}
9564
- L ${B.x} ${B.y}`,
9565
- labelX: (A.x + B.x) / 2,
9735
+ const xDir = backB.x > backA.x ? 1 : -1;
9736
+ const existing = {
9737
+ d: `M ${backA.x} ${backA.y}
9738
+ L ${backA.x} ${detourY + corner}
9739
+ Q ${backA.x} ${detourY}, ${backA.x + xDir * corner} ${detourY}
9740
+ L ${backB.x - xDir * corner} ${detourY}
9741
+ Q ${backB.x} ${detourY}, ${backB.x} ${detourY + corner}
9742
+ L ${backB.x} ${backB.y}`,
9743
+ labelX: (backA.x + backB.x) / 2,
9566
9744
  // CAR-468 SSOT ... 旧 detourY - 30 magic number を LABEL_INIT_CLEARANCE (26、 CAR-570 で 34 → 26 縮小) に置換。
9567
9745
  // CAR-518 SSOT ... dotted-flow は 62 world (marker glow radius 分拡張) を labelInit で参照。
9568
9746
  // top-top detour の水平 segment 上方 labelInit world = spec 準拠距離。
9569
9747
  labelY: detourY - labelInit,
9570
9748
  labelAnchor: "middle"
9571
9749
  };
9750
+ const existingCrossings = countPathObstacleCrossings(existing.d, obstacles);
9751
+ if (existingCrossings === 0) return existing;
9752
+ const candidate = from.lane === to.lane && fromLane?.stage !== void 0 ? makeBackDetourCandidate(
9753
+ backA,
9754
+ backB,
9755
+ from,
9756
+ to,
9757
+ obstacles,
9758
+ detourY,
9759
+ "right",
9760
+ fromLane.x + fromLane.width
9761
+ ) : chooseBackDetourCandidate(backA, backB, from, to, obstacles, detourY);
9762
+ if (candidate.crossings >= existingCrossings) return existing;
9763
+ return {
9764
+ d: candidate.d,
9765
+ labelX: candidate.labelX,
9766
+ labelY: detourY - labelInit,
9767
+ labelAnchor: "middle"
9768
+ };
9572
9769
  }
9573
9770
  const midX = fromSide === "left" || fromSide === "right" ? B.x : A.x;
9574
9771
  const midY = fromSide === "top" || fromSide === "bottom" ? B.y : A.y;
@@ -10495,12 +10692,56 @@ function \u76F4\u524D\u306E\u6298\u308C\u3054\u3068\u305A\u3089\u3059(segs, \u6A
10495
10692
  return parts.join(" ");
10496
10693
  }
10497
10694
 
10695
+ // src/layout/legend.ts
10696
+ var LEGEND_FONT_SIZE = 17;
10697
+ var LEGEND_MARK_SCALE = LEGEND_FONT_SIZE / 21;
10698
+ var LEGEND_MARK_REFERENCE_WIDTH = 46;
10699
+ var LEGEND_MARK_WIDTH = LEGEND_MARK_REFERENCE_WIDTH * LEGEND_MARK_SCALE;
10700
+ var LEGEND_MARK_TEXT_GAP = 12 * LEGEND_MARK_SCALE;
10701
+ var LEGEND_ITEM_GAP_RATIO = 0.07;
10702
+ var LEGEND_BASELINE_GAP = 32;
10703
+ var LEGEND_BOTTOM_EXTENT = 10;
10704
+ function layoutLegend(items, viewBox, textMetrics, nodes) {
10705
+ const renderedNodes = nodes.filter(isRenderedNode);
10706
+ const startX = renderedNodes.length > 0 ? Math.min(...renderedNodes.map((node) => node.cx - node.w / 2)) : viewBox.x + VIEW_PAD;
10707
+ const diagramRight = renderedNodes.length > 0 ? Math.max(...renderedNodes.map((node) => node.cx + node.w / 2)) : viewBox.x + viewBox.w - VIEW_PAD;
10708
+ const itemGap = (diagramRight - startX) * LEGEND_ITEM_GAP_RATIO;
10709
+ const y = viewBox.y + viewBox.h - VIEW_PAD + LEGEND_BASELINE_GAP;
10710
+ let x = startX;
10711
+ const legend = items.map((item) => {
10712
+ const laid = { ...item, x, y };
10713
+ x += LEGEND_MARK_WIDTH + LEGEND_MARK_TEXT_GAP + textWidth(item.text, LEGEND_FONT_SIZE, 0, textMetrics) + itemGap;
10714
+ return laid;
10715
+ });
10716
+ const right = x - itemGap;
10717
+ const bottom = y + LEGEND_BOTTOM_EXTENT + VIEW_PAD;
10718
+ return {
10719
+ legend,
10720
+ viewBox: {
10721
+ ...viewBox,
10722
+ w: Math.max(viewBox.w, right + VIEW_PAD - viewBox.x),
10723
+ h: Math.max(viewBox.h, bottom - viewBox.y)
10724
+ }
10725
+ };
10726
+ }
10727
+
10498
10728
  // src/layout/lanes.ts
10499
10729
  var LANE_POSITION_EPSILON = 1e-9;
10730
+ function equalizeStageColumns(lanes, nodes = []) {
10731
+ const stages = lanes.filter((lane) => lane.stage !== void 0);
10732
+ if (stages.length === 0) return lanes;
10733
+ const top = Math.min(...stages.map((lane) => lane.y));
10734
+ const stageIds = new Set(stages.map((lane) => lane.id));
10735
+ const nodeBottoms = nodes.filter((node) => stageIds.has(node.lane)).map((node) => node.cy + node.h / 2 + CONTAIN_PAD);
10736
+ const bottom = Math.max(...stages.map((lane) => lane.y + lane.height), ...nodeBottoms);
10737
+ return lanes.map(
10738
+ (lane) => lane.stage === void 0 ? lane : { ...lane, y: top, height: bottom - top }
10739
+ );
10740
+ }
10500
10741
  function layoutLanes(diag) {
10501
10742
  const laneGap = diag.viewport?.laneGap ?? diag.viewport?.gap ?? DEFAULT_LANE_GAP;
10502
10743
  let cursorX = 0;
10503
- return diag.lanes.map((lane) => {
10744
+ const lanes = diag.lanes.map((lane, index) => {
10504
10745
  if (lane.posX !== void 0 && lane.posY !== void 0) {
10505
10746
  return {
10506
10747
  ...lane,
@@ -10511,7 +10752,9 @@ function layoutLanes(diag) {
10511
10752
  };
10512
10753
  }
10513
10754
  const resolvedX = lane.x ?? cursorX;
10514
- cursorX = resolvedX + lane.width + laneGap;
10755
+ const nextLane = diag.lanes[index + 1];
10756
+ const gapAfter = lane.stage !== void 0 && nextLane?.stage !== void 0 ? 0 : laneGap;
10757
+ cursorX = resolvedX + lane.width + gapAfter;
10515
10758
  return {
10516
10759
  ...lane,
10517
10760
  x: resolvedX,
@@ -10519,6 +10762,7 @@ function layoutLanes(diag) {
10519
10762
  height: LANE_LABEL_H
10520
10763
  };
10521
10764
  });
10765
+ return equalizeStageColumns(lanes);
10522
10766
  }
10523
10767
  function expandLanesForNodes(lanes, nodes) {
10524
10768
  const targetNodeGap = targetNodeNodeClearance();
@@ -10547,7 +10791,8 @@ function expandLanesForNodes(lanes, nodes) {
10547
10791
  if (lane.posX !== void 0 && lane.posY !== void 0) continue;
10548
10792
  const inside = nodes.filter((n) => n.lane === lane.id);
10549
10793
  const maxNodeW = inside.length > 0 ? Math.max(...inside.map((n) => n.w)) : 0;
10550
- const newWidth = Math.max(lane.width, maxNodeW + padFor(lane) * 2);
10794
+ const stageMinWidth = lane.stage === void 0 ? 0 : maxNodeW + CONTAIN_PAD + STAGE_COLUMN_GAP;
10795
+ const newWidth = Math.max(lane.width, maxNodeW + padFor(lane) * 2, stageMinWidth);
10551
10796
  widthOf.set(lane.id, newWidth);
10552
10797
  deltaOf.set(lane.id, newWidth - lane.width);
10553
10798
  }
@@ -10572,6 +10817,34 @@ function expandLaneGapsForEdgeLabels(lanes, nodes, edges, textMetrics = "proport
10572
10817
  if (lanes.length < 2) return lanes;
10573
10818
  const layoutedLanes = lanes.filter((l) => !(l.posX !== void 0 && l.posY !== void 0));
10574
10819
  if (layoutedLanes.length < 2) return lanes;
10820
+ if (layoutedLanes.length === lanes.length && layoutedLanes.every((lane) => lane.stage !== void 0)) {
10821
+ const sortedStages = [...layoutedLanes].sort((a, b) => a.x - b.x);
10822
+ const stageIndexById = new Map(sortedStages.map((lane, index) => [lane.id, index]));
10823
+ const nodeLaneById = new Map(nodes.map((node) => [node.id, node.lane]));
10824
+ const requiredSurfaceGaps = new Array(sortedStages.length - 1).fill(STAGE_COLUMN_GAP);
10825
+ for (const edge of edges) {
10826
+ if (edge.routing !== "curve" || !edge.label && !edge.sub) continue;
10827
+ const fromIndex = stageIndexById.get(nodeLaneById.get(edge.from) ?? "");
10828
+ const toIndex = stageIndexById.get(nodeLaneById.get(edge.to) ?? "");
10829
+ if (fromIndex === void 0 || toIndex === void 0 || fromIndex === toIndex) continue;
10830
+ const lo = Math.min(fromIndex, toIndex);
10831
+ const hi = Math.max(fromIndex, toIndex);
10832
+ const labelWidth = computeLabelBoxW(edge.label, edge.sub, textMetrics);
10833
+ for (let pair = lo; pair < hi; pair++) {
10834
+ requiredSurfaceGaps[pair] = Math.max(requiredSurfaceGaps[pair], labelWidth);
10835
+ }
10836
+ }
10837
+ const shiftedXById2 = /* @__PURE__ */ new Map([[sortedStages[0].id, sortedStages[0].x]]);
10838
+ for (let index = 1; index < sortedStages.length; index++) {
10839
+ const previous = sortedStages[index - 1];
10840
+ const lane = sortedStages[index];
10841
+ const previousX = shiftedXById2.get(previous.id);
10842
+ const authoredGap = Math.max(0, lane.x - (previous.x + previous.width));
10843
+ const laneGap = Math.max(authoredGap, requiredSurfaceGaps[index - 1] - STAGE_COLUMN_GAP);
10844
+ shiftedXById2.set(lane.id, previousX + previous.width + laneGap);
10845
+ }
10846
+ return lanes.map((lane) => ({ ...lane, x: shiftedXById2.get(lane.id) ?? lane.x }));
10847
+ }
10575
10848
  const nodeById = new Map(nodes.map((n) => [n.id, n]));
10576
10849
  const sortedByX = [...layoutedLanes].sort((a, b) => a.x - b.x);
10577
10850
  const laneIndexById = new Map(sortedByX.map((lane, i) => [lane.id, i]));
@@ -10643,7 +10916,7 @@ function recomputeContainedLanes(lanes, nodes) {
10643
10916
  lifelineBottom = Math.max(lifelineBottom, bottom);
10644
10917
  }
10645
10918
  }
10646
- return lanes.map((lane) => {
10919
+ const contained = lanes.map((lane) => {
10647
10920
  if (lane.lifeline) {
10648
10921
  return {
10649
10922
  ...lane,
@@ -10661,6 +10934,7 @@ function recomputeContainedLanes(lanes, nodes) {
10661
10934
  height: bottom - top + CONTAIN_PAD * 2 + LANE_LABEL_H
10662
10935
  };
10663
10936
  });
10937
+ return equalizeStageColumns(contained, nodes);
10664
10938
  }
10665
10939
 
10666
10940
  // src/layout/head-label.ts
@@ -10810,11 +11084,15 @@ function requiredStackGapForEdgeLabels(diag, allStacks) {
10810
11084
  }
10811
11085
  function layoutNodes(diag, lanes, textMetrics) {
10812
11086
  const laneMap = new Map(lanes.map((l) => [l.id, l]));
11087
+ const metroDecisionIds = new Set(
11088
+ 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"))
11089
+ );
11090
+ const nodeSize = (node) => metroDecisionIds.has(node.id) ? { w: METRO_DECISION_WIDTH, h: METRO_DECISION_HEIGHT } : NODE_SIZE[node.kind];
10813
11091
  const allStacks = Array.from(new Set(diag.nodes.map((n) => n.stack))).sort((a, b) => a - b);
10814
11092
  const rowH = /* @__PURE__ */ new Map();
10815
11093
  for (const stack of allStacks) {
10816
11094
  const nodesAtStack = diag.nodes.filter((n) => n.stack === stack);
10817
- const h = Math.max(...nodesAtStack.map((n) => \u7BB1\u306E\u9AD8\u30552(n, NODE_SIZE[n.kind].h)));
11095
+ const h = Math.max(...nodesAtStack.map((n) => \u7BB1\u306E\u9AD8\u30552(n, nodeSize(n).h)));
10818
11096
  rowH.set(stack, h);
10819
11097
  }
10820
11098
  const rowCy = /* @__PURE__ */ new Map();
@@ -10850,7 +11128,7 @@ function layoutNodes(diag, lanes, textMetrics) {
10850
11128
  if (!lane) {
10851
11129
  throw new Error(`cdl layout: node "${n.id}" \u306E lane "${n.lane}" \u304C\u5B9A\u7FA9\u3055\u308C\u3066\u3044\u306A\u3044`);
10852
11130
  }
10853
- const size = NODE_SIZE[n.kind];
11131
+ const size = nodeSize(n);
10854
11132
  if (n.posX !== void 0 && n.posY !== void 0) {
10855
11133
  const w2 = n.posW ?? n.w ?? autoRowsWidth(n, size.w, textMetrics);
10856
11134
  const h2 = n.posH ?? n.h ?? rowH.get(n.stack) ?? size.h;
@@ -10973,6 +11251,9 @@ function dropUnsafeLaneDx(dxByLane, nodes) {
10973
11251
  function applyRowLevelNodeShifts(nodes, lanes, nodeShifts) {
10974
11252
  const dyByStack = aggregateRowDy(nodes, nodeShifts);
10975
11253
  const dxByLane = dropUnsafeLaneDx(aggregateLaneDx(nodes, nodeShifts), nodes);
11254
+ for (const lane of lanes) {
11255
+ if (lane.stage !== void 0) dxByLane.delete(lane.id);
11256
+ }
10976
11257
  let moved = false;
10977
11258
  const outNodes = nodes.map((n) => {
10978
11259
  const dx = dxByLane.get(n.lane) ?? 0;
@@ -11092,7 +11373,15 @@ function layout(diag) {
11092
11373
  );
11093
11374
  }
11094
11375
  }
11095
- const viewBox = computeViewBox(containedLanesFinal, nodesFinal, edges, bboxes, diag.viewport);
11376
+ const contentViewBox = computeViewBox(
11377
+ containedLanesFinal,
11378
+ nodesFinal,
11379
+ edges,
11380
+ bboxes,
11381
+ diag.viewport
11382
+ );
11383
+ const legendLayout = diag.legend?.length ? layoutLegend(diag.legend, contentViewBox, textMetrics, nodesFinal) : void 0;
11384
+ const viewBox = legendLayout?.viewBox ?? contentViewBox;
11096
11385
  return {
11097
11386
  id: diag.id,
11098
11387
  topic: diag.topic,
@@ -11119,6 +11408,7 @@ function layout(diag) {
11119
11408
  ...textMetrics === "monospace" ? { textMetrics: "monospace" } : {},
11120
11409
  // 種類も配色と同じく空の目印を出さない。 遮断をここに揃えると手書きの図にも効く
11121
11410
  ...diag.type ? { type: diag.type } : {},
11411
+ ...legendLayout ? { legend: legendLayout.legend } : {},
11122
11412
  bboxes,
11123
11413
  collisions,
11124
11414
  nearCollisions,
@@ -11307,6 +11597,11 @@ function validate(diag) {
11307
11597
  \u2192 \u4FEE\u6B63\u4F8B: .lane("${n.lane}", { x: 0, width: 400 }) \u3092 node \u5BA3\u8A00\u524D\u306B\u8FFD\u52A0`
11308
11598
  );
11309
11599
  }
11600
+ if (n.leaderTo !== void 0 && !nodeIds.has(n.leaderTo)) {
11601
+ errors.push(
11602
+ `[cdl] unknown-ref: node "${n.id}" \u306E leaderTo "${n.leaderTo}" \u304C node \u306B\u5B58\u5728\u3057\u307E\u305B\u3093${suggest(n.leaderTo, nodeIds, "node")}`
11603
+ );
11604
+ }
11310
11605
  }
11311
11606
  for (const e of diag.edges) {
11312
11607
  if (!nodeIds.has(e.from)) {
@@ -11925,6 +12220,10 @@ function uniformLifelineEndY(lanes, nodes, starts = lifelineStartYs(nodes)) {
11925
12220
 
11926
12221
  // src/render/edge-head.ts
11927
12222
  var EDGE_HEAD_MUTED_MARKER_ID = "cdl-arrow-muted";
12223
+ var METRO_RETURN_ARROW_D = "M 21 0 L 4 9.5 L 4 -9.5 Z";
12224
+ function metroReturnArrowMarkerId(tone) {
12225
+ return `cdl-metro-return-arrow-${tone}`;
12226
+ }
11928
12227
  var EDGE_HEAD_SHAPE = {
11929
12228
  // 何も描かない端。 marker 自体を出さないので `d` は読まれない
11930
12229
  none: { d: "", w: 10, h: 10, refX: 0, filled: false },
@@ -11979,6 +12278,7 @@ function \u7B49\u5E45\u306E\u5B57(text, fontSize) {
11979
12278
  });
11980
12279
  return out;
11981
12280
  }
12281
+ var METRO_PENDING_ATTRIBUTE = "data-cdl-pending";
11982
12282
  function defaultEdgeWidth(active) {
11983
12283
  return active ? 2.5 : 1.25;
11984
12284
  }
@@ -11997,12 +12297,14 @@ function CdlEdgeView({
11997
12297
  drawing,
11998
12298
  progress,
11999
12299
  stateValues,
12000
- flowing = false
12300
+ flowing = false,
12301
+ pending = false
12001
12302
  }) {
12002
12303
  const color = edgeColor(edge);
12003
- const marker = edgeHeadMarkerName(edge.tone, edge.head, !active, edge.headFill);
12004
- const tailMarker = edge.tailHead === void 0 ? void 0 : edgeHeadMarkerName(edge.tone, edge.tailHead, !active, edge.tailHeadFill);
12005
12304
  const style = edge.style ?? "solid";
12305
+ const isMetroReturn = edge.metroReturn === true;
12306
+ const marker = isMetroReturn ? metroReturnArrowMarkerId(edge.tone) : edgeHeadMarkerName(edge.tone, edge.head, !active, edge.headFill);
12307
+ const tailMarker = edge.tailHead === void 0 ? void 0 : edgeHeadMarkerName(edge.tone, edge.tailHead, !active, edge.tailHeadFill);
12006
12308
  const isDotted = style === "dotted-flow";
12007
12309
  const isPassThrough = isDotted && Boolean(edge.dThrough);
12008
12310
  const lineDotsD = isDotted ? shrinkPathEnd(edge.d, 12) : edge.d;
@@ -12012,6 +12314,7 @@ function CdlEdgeView({
12012
12314
  return /* @__PURE__ */ jsxs(
12013
12315
  "g",
12014
12316
  {
12317
+ ...pending ? { [METRO_PENDING_ATTRIBUTE]: "true" } : {},
12015
12318
  "data-cdl-edge": edge.id,
12016
12319
  "data-cdl-routing": edge.routing === "metro" ? "metro" : void 0,
12017
12320
  "data-cdl-from": edge.from,
@@ -12037,11 +12340,12 @@ function CdlEdgeView({
12037
12340
  fill: "none",
12038
12341
  stroke: color,
12039
12342
  strokeOpacity: active ? 0.95 : 0.9,
12040
- strokeWidth: active ? 5 : 4,
12343
+ strokeWidth: edge.routing === "metro" ? METRO_LINE_WIDTH : active ? 5 : 4,
12041
12344
  strokeDasharray: "0 18",
12042
12345
  strokeLinecap: "round",
12043
12346
  style: {
12044
- transition: "stroke 280ms ease, stroke-opacity 280ms ease, stroke-width 280ms ease"
12347
+ transition: "stroke 280ms ease, stroke-opacity 280ms ease, stroke-width 280ms ease",
12348
+ strokeWidth: edge.routing === "metro" ? METRO_LINE_WIDTH_CSS : void 0
12045
12349
  }
12046
12350
  }
12047
12351
  ),
@@ -12096,7 +12400,9 @@ function SolidEdgePath({
12096
12400
  const boundWidth = edge.widthBind && stateValues ? resolveNumericAttr(edge.widthBind, stateValues, defaultWidth) : defaultWidth;
12097
12401
  const boundStroke = edge.strokeBind && stateValues ? interpolate(edge.strokeBind, stateValues) : color;
12098
12402
  const boundDashOffset = edge.dashOffsetBind && stateValues ? resolveNumericAttr(edge.dashOffsetBind, stateValues, 0) : void 0;
12099
- const dashArray = dashed ? "10 8" : active ? boundDashOffset !== void 0 ? "8 6" : void 0 : "4 5";
12403
+ const strokeWidth = edge.routing === "metro" && edge.widthBind === void 0 ? edge.metroReturn ? 6 : METRO_LINE_WIDTH : boundWidth;
12404
+ const strokeWidthCss = edge.routing === "metro" && !edge.metroReturn ? edge.widthBind === void 0 ? METRO_LINE_WIDTH_CSS : `var(--cdl-metro-line-width, ${strokeWidth}px)` : void 0;
12405
+ const dashArray = dashed ? edge.metroReturn ? "0 9.6" : "10 8" : active ? boundDashOffset !== void 0 ? "8 6" : void 0 : "4 5";
12100
12406
  return /* @__PURE__ */ jsx(
12101
12407
  "path",
12102
12408
  {
@@ -12105,14 +12411,15 @@ function SolidEdgePath({
12105
12411
  fill: "none",
12106
12412
  stroke: boundStroke,
12107
12413
  strokeOpacity: active ? 0.95 : 0.9,
12108
- strokeWidth: boundWidth,
12414
+ strokeWidth,
12109
12415
  strokeDasharray: dashArray,
12110
12416
  strokeDashoffset: boundDashOffset,
12111
12417
  strokeLinecap: "round",
12112
12418
  markerEnd: marker === void 0 ? void 0 : `url(#${marker})`,
12113
12419
  markerStart: tailMarker === void 0 ? void 0 : `url(#${tailMarker})`,
12114
12420
  style: {
12115
- transition: "stroke 280ms ease, stroke-opacity 280ms ease, stroke-width 280ms ease"
12421
+ transition: "stroke 280ms ease, stroke-opacity 280ms ease, stroke-width 280ms ease",
12422
+ strokeWidth: strokeWidthCss
12116
12423
  }
12117
12424
  }
12118
12425
  );
@@ -12409,7 +12716,11 @@ function \u5272\u308B\u5019\u88DC(text) {
12409
12716
  function \u5165\u3063\u3066\u3044\u308B([\u5DE6, \u53F3]) {
12410
12717
  return \u5DE6 !== "" && \u53F3 !== "";
12411
12718
  }
12412
- function CardNode({ node, active }) {
12719
+ function CardNode({
12720
+ node,
12721
+ active,
12722
+ compactTitleInset
12723
+ }) {
12413
12724
  const accent = nodeAccent(node, "var(--cdl-tone-accent, #2d6a8f)");
12414
12725
  const x = node.cx - node.w / 2;
12415
12726
  const y = node.cy - node.h / 2;
@@ -12427,12 +12738,12 @@ function CardNode({ node, active }) {
12427
12738
  });
12428
12739
  const \u305A\u3089\u3057 = \u7E70\u308A\u4E0A\u3052 - \u4F59\u308A;
12429
12740
  const titleY = compact ? \u5C0F\u578B\u306B\u547C\u3073\u540D ? node.h / 2 - 2 : node.h / 2 + 8 : 86 - \u305A\u3089\u3057;
12430
- const titleX = compact ? node.w / 2 : 26;
12431
- const paddingX = compact ? 16 : 52;
12741
+ const titleX = compact ? compactTitleInset ?? node.w / 2 : 26;
12742
+ const paddingX = compact ? (compactTitleInset ?? 8) * 2 : 52;
12432
12743
  const availableW = node.w - paddingX;
12433
12744
  const initialTitleSize = compact ? 20 : 24;
12434
12745
  const titleSize = fitTextSize(node.title, availableW, initialTitleSize, 11);
12435
- const textAnchor = compact ? "middle" : "start";
12746
+ const textAnchor = compact && compactTitleInset === void 0 ? "middle" : "start";
12436
12747
  const subtitleY = 124 - \u305A\u3089\u3057;
12437
12748
  const \u6700\u5927\u306E\u884C\u9001\u308A = SUBTITLE_SIZE * \u884C\u9001\u308A\u306E\u500D\u7387;
12438
12749
  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;
@@ -13192,7 +13503,9 @@ function GenericNode({
13192
13503
  value,
13193
13504
  stateValues,
13194
13505
  \u5024\u306E\u540D\u524D,
13195
- textMetrics = "proportional"
13506
+ textMetrics = "proportional",
13507
+ showKindIcon = true,
13508
+ metroDecision = false
13196
13509
  }) {
13197
13510
  const base = STYLES[node.kind] ?? FALLBACK;
13198
13511
  const toned = toneColor3(node.tone);
@@ -13223,14 +13536,14 @@ function GenericNode({
13223
13536
  if (shape === "diamond") {
13224
13537
  const cx = w / 2;
13225
13538
  const cy = h / 2;
13226
- const pad = 8;
13539
+ const pad = metroDecision ? 0 : 8;
13227
13540
  return /* @__PURE__ */ jsx(
13228
13541
  "path",
13229
13542
  {
13230
13543
  d: `M ${cx} ${pad} L ${w - pad} ${cy} L ${cx} ${h - pad} L ${pad} ${cy} Z`,
13231
13544
  fill: "var(--cdl-node-fill, #ffffff)",
13232
13545
  stroke: active ? style.accent : "var(--cdl-divider, #d4d4d8)",
13233
- strokeWidth: active ? 3 : 1.25
13546
+ strokeWidth: metroDecision ? 1.5 : active ? 3 : 1.25
13234
13547
  }
13235
13548
  );
13236
13549
  }
@@ -13294,11 +13607,11 @@ function GenericNode({
13294
13607
  const \u305A\u3089\u3057 = \u7E70\u308A\u4E0A\u3052 - \u4F59\u308A;
13295
13608
  const \u7D75\u306E\u82AF = centered && STYLES[node.kind]?.shape === "cloud" ? h * CLOUD_ART_CENTER_RISE : 0;
13296
13609
  const eyebrowY = centered ? h / 2 - 26 - \u7D75\u306E\u82AF : 32 + cylinderShift + \u4F59\u308A;
13297
- const titleY = genericTitleBaselineY(node.kind, h, hasRows) - \u305A\u3089\u3057 - \u7D75\u306E\u82AF;
13610
+ const titleY = metroDecision ? h / 2 + 9 : genericTitleBaselineY(node.kind, h, hasRows) - \u305A\u3089\u3057 - \u7D75\u306E\u82AF;
13298
13611
  const subtitleY = centered ? h / 2 + 28 - \u7D75\u306E\u82AF : 96 + cylinderShift - \u305A\u3089\u3057;
13299
13612
  const iconX = centered ? w / 2 + 40 : w - 48;
13300
13613
  const iconY = centered ? h / 2 - 44 - \u7D75\u306E\u82AF : 18 + cylinderShift;
13301
- const dominantBaseline = centered ? "middle" : void 0;
13614
+ const dominantBaseline = centered && !metroDecision ? "middle" : void 0;
13302
13615
  return /* @__PURE__ */ jsxs(
13303
13616
  "g",
13304
13617
  {
@@ -13307,12 +13620,12 @@ function GenericNode({
13307
13620
  "data-cdl-look": style.look,
13308
13621
  children: [
13309
13622
  shapeEl,
13310
- /* @__PURE__ */ jsxs("g", { transform: `translate(${iconX} ${iconY}) scale(1)`, "data-cdl-role": "node-kind-icon", children: [
13623
+ showKindIcon && /* @__PURE__ */ jsxs("g", { transform: `translate(${iconX} ${iconY}) scale(1)`, "data-cdl-role": "node-kind-icon", children: [
13311
13624
  /* @__PURE__ */ jsx("rect", { width: 24, height: 24, fill: "transparent" }),
13312
13625
  /* @__PURE__ */ jsx("path", { d: style.icon, fill: style.eyebrowColor, opacity: 0.7, transform: "scale(1)" })
13313
13626
  ] }),
13314
13627
  node.eyebrow && /* @__PURE__ */ 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 }),
13315
- /* @__PURE__ */ 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 }),
13628
+ /* @__PURE__ */ 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 }),
13316
13629
  node.subtitle && /* @__PURE__ */ 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) }),
13317
13630
  resolvedValue && /* @__PURE__ */ jsx(
13318
13631
  "text",
@@ -13361,50 +13674,176 @@ function GenericNode({
13361
13674
  var MARK_INNER_RATIO = 0.56;
13362
13675
  var RING_WIDTH_ACTIVE = 6;
13363
13676
  var RING_WIDTH_IDLE = 4;
13677
+ var METRO_START_RADIUS = 17;
13678
+ var METRO_END_RADIUS = 19;
13679
+ var METRO_END_RING_WIDTH = 3.5;
13680
+ var METRO_END_INNER_RADIUS = 10;
13364
13681
  var MARK_DEFAULT = "var(--cdl-tone-accent, #2d6a8f)";
13365
13682
  function markRadius(node) {
13366
13683
  return Math.min(node.w, node.h) / 2;
13367
13684
  }
13368
- function MarkStartNode({ node }) {
13685
+ function MarkStartNode({
13686
+ node,
13687
+ metro = false
13688
+ }) {
13369
13689
  const \u8272 = nodeAccent(node, MARK_DEFAULT);
13370
- return /* @__PURE__ */ jsx("g", { transform: `translate(${node.cx - node.w / 2} ${node.cy - node.h / 2})`, children: /* @__PURE__ */ jsx(
13371
- "circle",
13690
+ return /* @__PURE__ */ jsx(
13691
+ "g",
13372
13692
  {
13373
- "data-cdl-role": "node-body",
13374
- "data-cdl-mark": "start",
13375
- cx: node.w / 2,
13376
- cy: node.h / 2,
13377
- r: markRadius(node),
13378
- fill: \u8272
13693
+ "data-cdl-role": metro ? "mark-start" : void 0,
13694
+ transform: `translate(${node.cx - node.w / 2} ${node.cy - node.h / 2})`,
13695
+ children: /* @__PURE__ */ jsx(
13696
+ "circle",
13697
+ {
13698
+ "data-cdl-role": "node-body",
13699
+ "data-cdl-mark": "start",
13700
+ cx: node.w / 2,
13701
+ cy: node.h / 2,
13702
+ r: metro ? METRO_START_RADIUS : markRadius(node),
13703
+ fill: \u8272
13704
+ }
13705
+ )
13379
13706
  }
13380
- ) });
13707
+ );
13381
13708
  }
13382
- function MarkEndNode({ node, active }) {
13709
+ function MarkEndNode({
13710
+ node,
13711
+ active,
13712
+ metro = false
13713
+ }) {
13383
13714
  const \u8272 = nodeAccent(node, MARK_DEFAULT);
13384
- const r = markRadius(node);
13385
- const \u8F2A\u306E\u592A\u3055 = active ? RING_WIDTH_ACTIVE : RING_WIDTH_IDLE;
13386
- return /* @__PURE__ */ jsxs("g", { transform: `translate(${node.cx - node.w / 2} ${node.cy - node.h / 2})`, children: [
13715
+ const r = metro ? METRO_END_RADIUS : markRadius(node);
13716
+ const \u8F2A\u306E\u592A\u3055 = metro ? METRO_END_RING_WIDTH : active ? RING_WIDTH_ACTIVE : RING_WIDTH_IDLE;
13717
+ return /* @__PURE__ */ jsxs(
13718
+ "g",
13719
+ {
13720
+ "data-cdl-role": metro ? "mark-end" : void 0,
13721
+ transform: `translate(${node.cx - node.w / 2} ${node.cy - node.h / 2})`,
13722
+ children: [
13723
+ /* @__PURE__ */ jsx(
13724
+ "circle",
13725
+ {
13726
+ "data-cdl-role": "node-body",
13727
+ "data-cdl-mark": "end",
13728
+ cx: node.w / 2,
13729
+ cy: node.h / 2,
13730
+ r: metro ? r : Math.max(0, r - \u8F2A\u306E\u592A\u3055 / 2),
13731
+ fill: "var(--cdl-node-fill, #ffffff)",
13732
+ stroke: \u8272,
13733
+ strokeWidth: \u8F2A\u306E\u592A\u3055
13734
+ }
13735
+ ),
13736
+ /* @__PURE__ */ jsx(
13737
+ "circle",
13738
+ {
13739
+ "data-cdl-role": "node-inner",
13740
+ cx: node.w / 2,
13741
+ cy: node.h / 2,
13742
+ r: metro ? METRO_END_INNER_RADIUS : Math.max(0, r * MARK_INNER_RATIO),
13743
+ fill: \u8272
13744
+ }
13745
+ )
13746
+ ]
13747
+ }
13748
+ );
13749
+ }
13750
+ var METRO_STATION_RADIUS = 16;
13751
+ var METRO_STATION_STROKE_WIDTH = 6;
13752
+ var METRO_STATION_RADIUS_CSS = `var(--cdl-metro-station-radius, ${METRO_STATION_RADIUS}px)`;
13753
+ var METRO_STATION_STROKE_WIDTH_CSS = `var(--cdl-metro-station-stroke-width, ${METRO_STATION_STROKE_WIDTH}px)`;
13754
+ var STATION_DEFAULT = "var(--cdl-tone-accent, #2d6a8f)";
13755
+ var STATION_NAME_FONT_SIZE = 25;
13756
+ var STATION_NAME_GAP = 18;
13757
+ var STATION_NAME_RIGHT_GAP = 32;
13758
+ var STATION_NAME_RIGHT_BASELINE = 9;
13759
+ var STATION_SUBTITLE_FONT_SIZE = 18;
13760
+ var STATION_SUBTITLE_GAP = 22;
13761
+ function StationNode({
13762
+ node,
13763
+ nameRight = false
13764
+ }) {
13765
+ const name = node.title.trim();
13766
+ const subtitle = node.subtitle?.trim() ?? "";
13767
+ return /* @__PURE__ */ jsxs("g", { children: [
13387
13768
  /* @__PURE__ */ jsx(
13388
13769
  "circle",
13389
13770
  {
13390
13771
  "data-cdl-role": "node-body",
13391
- "data-cdl-mark": "end",
13392
- cx: node.w / 2,
13393
- cy: node.h / 2,
13394
- r: Math.max(0, r - \u8F2A\u306E\u592A\u3055 / 2),
13772
+ "data-cdl-mark": "station",
13773
+ cx: node.cx,
13774
+ cy: node.cy,
13775
+ r: METRO_STATION_RADIUS,
13395
13776
  fill: "var(--cdl-node-fill, #ffffff)",
13396
- stroke: \u8272,
13397
- strokeWidth: \u8F2A\u306E\u592A\u3055
13777
+ stroke: nodeAccent(node, STATION_DEFAULT),
13778
+ strokeWidth: METRO_STATION_STROKE_WIDTH,
13779
+ style: { r: METRO_STATION_RADIUS_CSS, strokeWidth: METRO_STATION_STROKE_WIDTH_CSS }
13398
13780
  }
13399
13781
  ),
13782
+ name !== "" && /* @__PURE__ */ jsx(
13783
+ "text",
13784
+ {
13785
+ "data-cdl-role": "shape-name",
13786
+ x: nameRight ? node.cx + STATION_NAME_RIGHT_GAP : node.cx,
13787
+ y: nameRight ? node.cy + STATION_NAME_RIGHT_BASELINE : node.cy - METRO_STATION_RADIUS - STATION_NAME_GAP,
13788
+ textAnchor: nameRight ? "start" : "middle",
13789
+ fontSize: STATION_NAME_FONT_SIZE,
13790
+ fontWeight: 700,
13791
+ fill: "var(--cdl-text, #23201b)",
13792
+ stroke: "var(--cdl-node-fill, #ffffff)",
13793
+ strokeWidth: 8,
13794
+ strokeLinejoin: "round",
13795
+ paintOrder: "stroke",
13796
+ children: name
13797
+ }
13798
+ ),
13799
+ subtitle !== "" && /* @__PURE__ */ jsx(
13800
+ "text",
13801
+ {
13802
+ "data-cdl-role": "shape-value",
13803
+ x: node.cx,
13804
+ y: node.cy + METRO_STATION_RADIUS + STATION_SUBTITLE_GAP,
13805
+ textAnchor: "middle",
13806
+ fontSize: STATION_SUBTITLE_FONT_SIZE,
13807
+ fill: "var(--cdl-text-dim, #5a6270)",
13808
+ children: subtitle
13809
+ }
13810
+ )
13811
+ ] });
13812
+ }
13813
+ function TimelineNumberNode({ node }) {
13814
+ const radius = Math.max(0, Math.min(node.w, node.h) / 2 - TIMELINE_NUMBER_STROKE / 2);
13815
+ const fontSize = fitTextSize(
13816
+ node.title,
13817
+ titleUsableWidth(node.kind, node.w, node.h),
13818
+ 18,
13819
+ MIN_TEXT_SIZE
13820
+ );
13821
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
13400
13822
  /* @__PURE__ */ jsx(
13401
13823
  "circle",
13402
13824
  {
13403
- "data-cdl-role": "node-inner",
13404
- cx: node.w / 2,
13405
- cy: node.h / 2,
13406
- r: Math.max(0, r * MARK_INNER_RATIO),
13407
- fill: \u8272
13825
+ "data-cdl-role": "node-body",
13826
+ "data-cdl-mark": "timeline-number",
13827
+ cx: node.cx,
13828
+ cy: node.cy,
13829
+ r: radius,
13830
+ fill: "var(--cdl-node-fill, #ffffff)",
13831
+ stroke: nodeAccent(node, STATION_DEFAULT),
13832
+ strokeWidth: TIMELINE_NUMBER_STROKE
13833
+ }
13834
+ ),
13835
+ /* @__PURE__ */ jsx(
13836
+ "text",
13837
+ {
13838
+ "data-cdl-role": "node-label",
13839
+ x: node.cx,
13840
+ y: node.cy,
13841
+ textAnchor: "middle",
13842
+ dominantBaseline: "central",
13843
+ fontSize,
13844
+ fontWeight: 700,
13845
+ fill: "var(--cdl-text, #1a1f2a)",
13846
+ children: node.title
13408
13847
  }
13409
13848
  )
13410
13849
  ] });
@@ -13736,27 +14175,6 @@ function interpolateColor(template, stateValues, fallback) {
13736
14175
  if (!template) return fallback;
13737
14176
  return interpolate(template, stateValues);
13738
14177
  }
13739
- var STATION_STROKE_ACTIVE = 4;
13740
- var STATION_STROKE_IDLE = 3;
13741
- var STATION_DEFAULT = "var(--cdl-tone-accent, #2d6a8f)";
13742
- function StationNode({ node, active }) {
13743
- const strokeWidth = active ? STATION_STROKE_ACTIVE : STATION_STROKE_IDLE;
13744
- const radius = Math.max(0, Math.min(node.w, node.h) / 2 - strokeWidth / 2);
13745
- const \u5F62 = /* @__PURE__ */ jsx(
13746
- "circle",
13747
- {
13748
- "data-cdl-role": "node-body",
13749
- "data-cdl-mark": "station",
13750
- cx: node.cx,
13751
- cy: node.cy,
13752
- r: radius,
13753
- fill: "var(--cdl-node-fill, #ffffff)",
13754
- stroke: nodeAccent(node, STATION_DEFAULT),
13755
- strokeWidth
13756
- }
13757
- );
13758
- return renderShapeWithOutsideLabels(\u5F62, node, "middle");
13759
- }
13760
14178
 
13761
14179
  // src/kinds/label-stack.ts
13762
14180
  function \u540D\u672D\u306E\u6BB5\u3092\u307B\u3069\u304F(\u771F\u306Ey, \u9593\u9694, \u4E0A\u9650, \u4E0B\u9650) {
@@ -20918,10 +21336,14 @@ function CdlNodeView({
20918
21336
  stateValues,
20919
21337
  currentPhaseId,
20920
21338
  chartLineScale,
21339
+ compactTitleInset,
20921
21340
  \u95A2\u4FC2\u306E\u53D7\u3051\u53E3,
20922
21341
  lang,
20923
21342
  textMetrics,
20924
- \u5024\u306E\u540D\u524D
21343
+ \u5024\u306E\u540D\u524D,
21344
+ metroDiagram = false,
21345
+ metroDecision = false,
21346
+ stationNameRight = false
20925
21347
  }) {
20926
21348
  const value = node.value ? interpolate(node.value, stateValues, \u5024\u306E\u540D\u524D) : void 0;
20927
21349
  const rows = (node.rows ?? []).map((r) => interpolate(r, stateValues, \u5024\u306E\u540D\u524D));
@@ -20962,7 +21384,7 @@ function CdlNodeView({
20962
21384
  case "event":
20963
21385
  return /* @__PURE__ */ jsx(EventNode, { node: resolvedNode, active, progress });
20964
21386
  case "card":
20965
- return /* @__PURE__ */ jsx(CardNode, { node: resolvedNode, active });
21387
+ return /* @__PURE__ */ jsx(CardNode, { node: resolvedNode, active, compactTitleInset });
20966
21388
  // 時系列のやり取り (#578)。 1 つの箱が図を丸ごと描く = 縦が時間で、参加者は箱を持たない
20967
21389
  case "sequence-board":
20968
21390
  return /* @__PURE__ */ jsx(
@@ -21249,11 +21671,13 @@ function CdlNodeView({
21249
21671
  case "shape-customer-service":
21250
21672
  return /* @__PURE__ */ jsx(ShapeCustomerServiceNode, { node: resolvedNode, active });
21251
21673
  case "mark-start":
21252
- return /* @__PURE__ */ jsx(MarkStartNode, { node: resolvedNode, active });
21674
+ return /* @__PURE__ */ jsx(MarkStartNode, { node: resolvedNode, active, metro: metroDiagram });
21253
21675
  case "mark-end":
21254
- return /* @__PURE__ */ jsx(MarkEndNode, { node: resolvedNode, active });
21676
+ return /* @__PURE__ */ jsx(MarkEndNode, { node: resolvedNode, active, metro: metroDiagram });
21255
21677
  case "station":
21256
- return /* @__PURE__ */ jsx(StationNode, { node: resolvedNode, active });
21678
+ return /* @__PURE__ */ jsx(StationNode, { node: resolvedNode, active, nameRight: stationNameRight });
21679
+ case "timeline-number":
21680
+ return /* @__PURE__ */ jsx(TimelineNumberNode, { node: resolvedNode });
21257
21681
  default:
21258
21682
  return /* @__PURE__ */ jsx(
21259
21683
  GenericNode,
@@ -21263,7 +21687,9 @@ function CdlNodeView({
21263
21687
  value: node.value,
21264
21688
  stateValues,
21265
21689
  \u5024\u306E\u540D\u524D,
21266
- textMetrics
21690
+ textMetrics,
21691
+ showKindIcon: !(metroDiagram && node.kind === "decision"),
21692
+ metroDecision
21267
21693
  }
21268
21694
  );
21269
21695
  }
@@ -21377,6 +21803,112 @@ function \u7BB1\u306E\u4E0A\u306E\u8A9E(node, textMetrics = "proportional") {
21377
21803
  )
21378
21804
  ] });
21379
21805
  }
21806
+ var ink = "var(--cdl-text-accent, #143a52)";
21807
+ var paper = "var(--cdl-stage-bg, #f8fafc)";
21808
+ var LEGEND_MARK_RENDERERS = {
21809
+ diamond: () => /* @__PURE__ */ jsx("path", { d: "M 23 -26 L 46 -8 L 23 10 L 0 -8 Z", fill: "none", stroke: ink, strokeWidth: 2 }),
21810
+ "filled-circle": () => /* @__PURE__ */ jsx("circle", { cx: 23, cy: -8, r: 11, fill: ink }),
21811
+ "double-circle": () => /* @__PURE__ */ jsxs(Fragment, { children: [
21812
+ /* @__PURE__ */ jsx("circle", { cx: 23, cy: -8, r: 12, fill: "none", stroke: ink, strokeWidth: 2.5 }),
21813
+ /* @__PURE__ */ jsx("circle", { cx: 23, cy: -8, r: 6, fill: ink })
21814
+ ] }),
21815
+ "dotted-line": () => /* @__PURE__ */ jsx(
21816
+ "path",
21817
+ {
21818
+ d: `M 0 -8 H ${LEGEND_MARK_REFERENCE_WIDTH}`,
21819
+ fill: "none",
21820
+ stroke: ink,
21821
+ strokeWidth: 6,
21822
+ strokeLinecap: "round",
21823
+ strokeDasharray: "0 9.6"
21824
+ }
21825
+ ),
21826
+ "solid-line": () => /* @__PURE__ */ jsx(
21827
+ "path",
21828
+ {
21829
+ d: `M 0 -8 H ${LEGEND_MARK_REFERENCE_WIDTH}`,
21830
+ fill: "none",
21831
+ stroke: ink,
21832
+ strokeWidth: 4,
21833
+ strokeLinecap: "round"
21834
+ }
21835
+ ),
21836
+ "curved-line": () => /* @__PURE__ */ jsx(
21837
+ "path",
21838
+ {
21839
+ d: `M 0 6 C 12 -22, 34 -22, ${LEGEND_MARK_REFERENCE_WIDTH} 6`,
21840
+ fill: "none",
21841
+ stroke: ink,
21842
+ strokeWidth: 4,
21843
+ strokeLinecap: "round"
21844
+ }
21845
+ ),
21846
+ "rounded-label": () => /* @__PURE__ */ jsx(
21847
+ "rect",
21848
+ {
21849
+ x: 0,
21850
+ y: -23,
21851
+ width: LEGEND_MARK_REFERENCE_WIDTH,
21852
+ height: 30,
21853
+ rx: 9,
21854
+ fill: paper,
21855
+ stroke: ink,
21856
+ strokeWidth: 2
21857
+ }
21858
+ ),
21859
+ "numbered-circle": () => /* @__PURE__ */ jsxs(Fragment, { children: [
21860
+ /* @__PURE__ */ jsx("circle", { cx: 23, cy: -8, r: 13, fill: paper, stroke: ink, strokeWidth: 2.5 }),
21861
+ /* @__PURE__ */ jsx("text", { x: 23, y: -2, textAnchor: "middle", fontSize: 17, fontWeight: 700, fill: ink, children: "1" })
21862
+ ] }),
21863
+ station: () => /* @__PURE__ */ jsx("circle", { cx: 23, cy: -8, r: 11, fill: paper, stroke: ink, strokeWidth: 4 }),
21864
+ arrow: () => /* @__PURE__ */ jsx(
21865
+ "path",
21866
+ {
21867
+ d: `M 0 -8 H 42 M 32 -18 L ${LEGEND_MARK_REFERENCE_WIDTH} -8 L 32 2`,
21868
+ fill: "none",
21869
+ stroke: ink,
21870
+ strokeWidth: 3,
21871
+ strokeLinecap: "round",
21872
+ strokeLinejoin: "round"
21873
+ }
21874
+ )
21875
+ };
21876
+ function CdlLegend({ items }) {
21877
+ return /* @__PURE__ */ jsx("g", { "data-cdl-role": "legend", children: items.map((item, index) => {
21878
+ const Mark = LEGEND_MARK_RENDERERS[item.mark];
21879
+ return /* @__PURE__ */ jsxs(
21880
+ "g",
21881
+ {
21882
+ "data-cdl-role": "legend-item",
21883
+ "data-cdl-y": item.y,
21884
+ transform: `translate(${item.x} ${item.y})`,
21885
+ children: [
21886
+ /* @__PURE__ */ jsx(
21887
+ "g",
21888
+ {
21889
+ "data-cdl-role": "legend-mark",
21890
+ "data-cdl-legend-mark": item.mark,
21891
+ transform: `scale(${LEGEND_MARK_SCALE})`,
21892
+ children: /* @__PURE__ */ jsx(Mark, {})
21893
+ }
21894
+ ),
21895
+ /* @__PURE__ */ jsx(
21896
+ "text",
21897
+ {
21898
+ "data-cdl-role": "legend-text",
21899
+ x: LEGEND_MARK_WIDTH + LEGEND_MARK_TEXT_GAP,
21900
+ y: 0,
21901
+ fontSize: LEGEND_FONT_SIZE,
21902
+ fill: "var(--cdl-text-mute, #4d7187)",
21903
+ children: item.text
21904
+ }
21905
+ )
21906
+ ]
21907
+ },
21908
+ `${item.mark}-${index}`
21909
+ );
21910
+ }) });
21911
+ }
21380
21912
 
21381
21913
  // src/render/relation-focus.ts
21382
21914
  function relatedFocusSet(edges, nodeId) {
@@ -21389,10 +21921,17 @@ function relatedFocusSet(edges, nodeId) {
21389
21921
  }
21390
21922
  return \u96C6\u5408;
21391
21923
  }
21924
+ var METRO_LANE_BADGE_WIDTH = 200;
21925
+ var METRO_LANE_BADGE_HEIGHT = 88;
21926
+ var METRO_LANE_BADGE_INSET = 24;
21927
+ var METRO_LANE_GUIDE_GAP = 24;
21392
21928
  function \u63CF\u304F\u3082\u306E\u304C\u3042\u308B(laid) {
21929
+ if (laid.legend && laid.legend.length > 0) return true;
21393
21930
  if (laid.edges.length > 0) return true;
21394
21931
  if (laid.nodes.some((n) => isRenderedNode(n))) return true;
21395
- return laid.lanes.some((l) => l.contain === true || l.lifeline === true || Boolean(l.label));
21932
+ return laid.lanes.some(
21933
+ (l) => l.contain === true || l.lifeline === true || Boolean(l.label) || l.stage !== void 0 || l.metro !== void 0
21934
+ );
21396
21935
  }
21397
21936
  function CdlStage({
21398
21937
  laid,
@@ -21433,6 +21972,7 @@ function CdlStage({
21433
21972
  [\u95A2\u4FC2\u3092\u5149\u3089\u305B\u308B, \u7126\u70B9, laid.edges]
21434
21973
  );
21435
21974
  const activeSet = \u7126\u70B9\u306E\u96C6\u5408 ?? new Set(currentPhase?.activate ?? []);
21975
+ const \u8DEF\u7DDA\u56F3 = laid.edges.some((edge) => edge.routing === "metro") && laid.nodes.some((node) => node.kind === "station");
21436
21976
  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;
21437
21977
  const \u77E2\u5370\u306E\u51FA\u756A = useMemo(() => {
21438
21978
  const \u521D\u51FA = /* @__PURE__ */ new Map();
@@ -21461,9 +22001,15 @@ function CdlStage({
21461
22001
  const first = \u521D\u51FA.get(edgeId);
21462
22002
  if (first === void 0) return false;
21463
22003
  return \u4ECA === first;
22004
+ },
22005
+ /** 初出の段より前にある区間だけを、通過前として選べるようにする。 */
22006
+ \u307E\u3060\u304B: (edgeId) => {
22007
+ const first = \u521D\u51FA.get(edgeId);
22008
+ return first !== void 0 && \u4ECA >= 0 && \u4ECA < first;
21464
22009
  }
21465
22010
  };
21466
22011
  }, [laid.phases, currentPhase, laid.edgeReveal]);
22012
+ const \u7DDA\u3092\u63CF\u304F = (edge) => edge.routing === "metro" || \u77E2\u5370\u306E\u51FA\u756A.\u51FA\u308B\u304B(edge.id);
21467
22013
  const drawSet = new Set(currentPhase?.draw ?? []);
21468
22014
  const chartLineScales = useMemo(() => {
21469
22015
  const nodes = laid.nodes.filter((node) => node.kind === "chart-line");
@@ -21551,7 +22097,53 @@ function CdlStage({
21551
22097
  )
21552
22098
  );
21553
22099
  }, [laid]);
22100
+ const metroReturnArrowMarkers = useMemo(() => {
22101
+ const tones = new Set(
22102
+ laid.edges.filter((edge) => edge.metroReturn === true).map((edge) => edge.tone)
22103
+ );
22104
+ return [...tones].map((tone) => /* @__PURE__ */ jsx(
22105
+ "marker",
22106
+ {
22107
+ id: metroReturnArrowMarkerId(tone),
22108
+ viewBox: "0 -9.5 21 19",
22109
+ refX: "0",
22110
+ refY: "0",
22111
+ markerWidth: "21",
22112
+ markerHeight: "19",
22113
+ markerUnits: "userSpaceOnUse",
22114
+ orient: "auto",
22115
+ children: /* @__PURE__ */ jsx(
22116
+ "path",
22117
+ {
22118
+ "data-cdl-role": "edge-arrowhead",
22119
+ "data-cdl-mark": "metro-return-arrow",
22120
+ d: METRO_RETURN_ARROW_D,
22121
+ fill: TONE[tone]
22122
+ }
22123
+ )
22124
+ },
22125
+ tone
22126
+ ));
22127
+ }, [laid.edges]);
21554
22128
  const diagramScale = laid.diagramScale ?? 1;
22129
+ const stageLaneIds = new Set(
22130
+ laid.lanes.filter((lane) => lane.stage !== void 0).map((lane) => lane.id)
22131
+ );
22132
+ const stageNoteNodes = laid.nodes.filter(
22133
+ (node) => node.kind === "card" && (stageLaneIds.has(node.lane) || node.leaderTo !== void 0) && isRenderedNode(node) && Boolean(node.subtitle)
22134
+ );
22135
+ const stageNoteNodeIds = new Set(stageNoteNodes.map((node) => node.id));
22136
+ const nodesById = new Map(laid.nodes.map((node) => [node.id, node]));
22137
+ const metroDecisionIds = /* @__PURE__ */ new Set();
22138
+ const stationNamesRight = /* @__PURE__ */ new Set();
22139
+ for (const edge of laid.edges) {
22140
+ if (edge.routing !== "metro") continue;
22141
+ if (nodesById.get(edge.from)?.kind === "decision") metroDecisionIds.add(edge.from);
22142
+ if (nodesById.get(edge.to)?.kind === "decision") metroDecisionIds.add(edge.to);
22143
+ if (edge.toSide === "top" && nodesById.get(edge.to)?.kind === "station") {
22144
+ stationNamesRight.add(edge.to);
22145
+ }
22146
+ }
21555
22147
  if (!\u63CF\u304F\u3082\u306E\u304C\u3042\u308B(laid)) return null;
21556
22148
  return /* @__PURE__ */ jsxs("div", { className: compact ? "px-3 py-3" : "px-6 py-6", style: compact ? void 0 : { minHeight: 460 }, children: [
21557
22149
  /* @__PURE__ */ jsxs(
@@ -21571,6 +22163,7 @@ function CdlStage({
21571
22163
  children: [
21572
22164
  /* @__PURE__ */ jsxs("defs", { children: [
21573
22165
  edgeHeadMarkers,
22166
+ metroReturnArrowMarkers,
21574
22167
  /* @__PURE__ */ jsx(
21575
22168
  "marker",
21576
22169
  {
@@ -21607,7 +22200,61 @@ function CdlStage({
21607
22200
  pointerEvents: "none"
21608
22201
  }
21609
22202
  ),
21610
- lane.contain && /* @__PURE__ */ jsx(
22203
+ lane.stage && /* @__PURE__ */ jsxs(Fragment, { children: [
22204
+ /* @__PURE__ */ jsx(
22205
+ "rect",
22206
+ {
22207
+ "data-cdl-role": "stage-column",
22208
+ x: lane.x + STAGE_COLUMN_GAP / 2,
22209
+ y: lane.y,
22210
+ width: lane.width - STAGE_COLUMN_GAP,
22211
+ height: lane.height,
22212
+ rx: 16,
22213
+ fill: "var(--cdl-stage-bg, #fafafa)",
22214
+ fillOpacity: 0.5,
22215
+ stroke: "var(--cdl-text-mute, #8a8678)",
22216
+ strokeWidth: 1.5
22217
+ }
22218
+ ),
22219
+ /* @__PURE__ */ jsx(
22220
+ "text",
22221
+ {
22222
+ "data-cdl-role": "stage-number",
22223
+ x: lane.x + STAGE_COLUMN_GAP / 2 + 20,
22224
+ y: lane.y + 24,
22225
+ fontSize: 14,
22226
+ fontWeight: 500,
22227
+ letterSpacing: 1.5,
22228
+ fill: "var(--cdl-text-mute, #8a8678)",
22229
+ children: `\u6BB5\u968E ${lane.stage.number}`
22230
+ }
22231
+ ),
22232
+ /* @__PURE__ */ jsx(
22233
+ "text",
22234
+ {
22235
+ "data-cdl-role": "stage-name",
22236
+ x: lane.x + STAGE_COLUMN_GAP / 2 + 20,
22237
+ y: lane.y + 55,
22238
+ fontSize: 22,
22239
+ fontWeight: 700,
22240
+ fill: "var(--cdl-text, #1a1f2a)",
22241
+ children: lane.stage.name
22242
+ }
22243
+ ),
22244
+ /* @__PURE__ */ jsx(
22245
+ "line",
22246
+ {
22247
+ "data-cdl-role": "stage-divider",
22248
+ x1: lane.x + STAGE_COLUMN_GAP / 2,
22249
+ y1: lane.y + 72,
22250
+ x2: lane.x + lane.width - STAGE_COLUMN_GAP / 2,
22251
+ y2: lane.y + 72,
22252
+ stroke: "var(--cdl-divider, #d4d4d8)",
22253
+ strokeWidth: 1.5
22254
+ }
22255
+ )
22256
+ ] }),
22257
+ lane.contain && lane.stage === void 0 && /* @__PURE__ */ jsx(
21611
22258
  "rect",
21612
22259
  {
21613
22260
  "data-cdl-role": "lane-container",
@@ -21643,7 +22290,85 @@ function CdlStage({
21643
22290
  }
21644
22291
  );
21645
22292
  })(),
21646
- lane.label && /* @__PURE__ */ jsx(
22293
+ lane.timelineAxis && (() => {
22294
+ const ys = laid.nodes.filter((node) => node.lane === lane.id && isRenderedNode(node)).map((node) => node.cy);
22295
+ if (ys.length < 2) return null;
22296
+ const y1 = Math.min(...ys);
22297
+ const y2 = Math.max(...ys);
22298
+ if (y2 <= y1) return null;
22299
+ return /* @__PURE__ */ jsx(
22300
+ "line",
22301
+ {
22302
+ "data-cdl-role": "timeline-axis",
22303
+ x1: lane.x + lane.width / 2,
22304
+ y1,
22305
+ x2: lane.x + lane.width / 2,
22306
+ y2,
22307
+ stroke: "var(--cdl-tone-accent, #2d6a8f)",
22308
+ strokeWidth: TIMELINE_NUMBER_STROKE
22309
+ }
22310
+ );
22311
+ })(),
22312
+ lane.metro !== void 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
22313
+ /* @__PURE__ */ jsx(
22314
+ "line",
22315
+ {
22316
+ "data-cdl-role": "metro-lane-guide",
22317
+ x1: lane.x + METRO_LANE_BADGE_INSET + METRO_LANE_BADGE_WIDTH + METRO_LANE_GUIDE_GAP,
22318
+ y1: lane.y + lane.height / 2,
22319
+ x2: lane.x + lane.width - METRO_LANE_BADGE_INSET,
22320
+ y2: lane.y + lane.height / 2,
22321
+ stroke: "var(--cdl-line-mute, #a89d8f)",
22322
+ strokeWidth: 2,
22323
+ strokeDasharray: "22 7 5 7",
22324
+ opacity: 0.45
22325
+ }
22326
+ ),
22327
+ /* @__PURE__ */ jsxs(
22328
+ "g",
22329
+ {
22330
+ "data-cdl-role": "metro-lane-badge",
22331
+ transform: `translate(${lane.x + METRO_LANE_BADGE_INSET} ${lane.y + lane.height / 2 - METRO_LANE_BADGE_HEIGHT / 2})`,
22332
+ children: [
22333
+ /* @__PURE__ */ jsx(
22334
+ "rect",
22335
+ {
22336
+ width: METRO_LANE_BADGE_WIDTH,
22337
+ height: METRO_LANE_BADGE_HEIGHT,
22338
+ rx: 12,
22339
+ fill: "var(--cdl-node-fill, #ffffff)",
22340
+ stroke: "var(--cdl-tone-accent, #2d6a8f)",
22341
+ strokeWidth: 1.25
22342
+ }
22343
+ ),
22344
+ /* @__PURE__ */ jsx(
22345
+ "text",
22346
+ {
22347
+ x: METRO_LANE_BADGE_WIDTH / 2,
22348
+ y: lane.metro.subtitle ? 37 : 53,
22349
+ fontSize: 26,
22350
+ fontWeight: 700,
22351
+ fill: "var(--cdl-text, #1a1f2a)",
22352
+ textAnchor: "middle",
22353
+ children: lane.label
22354
+ }
22355
+ ),
22356
+ lane.metro.subtitle && /* @__PURE__ */ jsx(
22357
+ "text",
22358
+ {
22359
+ x: METRO_LANE_BADGE_WIDTH / 2,
22360
+ y: 67,
22361
+ fontSize: 18,
22362
+ fill: "var(--cdl-text-dim, #5a6270)",
22363
+ textAnchor: "middle",
22364
+ children: lane.metro.subtitle
22365
+ }
22366
+ )
22367
+ ]
22368
+ }
22369
+ )
22370
+ ] }),
22371
+ lane.label && lane.metro === void 0 && /* @__PURE__ */ jsx(
21647
22372
  "text",
21648
22373
  {
21649
22374
  "data-cdl-role": "lane-label",
@@ -21660,7 +22385,34 @@ function CdlStage({
21660
22385
  },
21661
22386
  lane.id
21662
22387
  )),
21663
- laid.edges.filter((edge) => \u77E2\u5370\u306E\u51FA\u756A.\u51FA\u308B\u304B(edge.id)).map((edge) => /* @__PURE__ */ jsx(
22388
+ laid.nodes.map((node) => {
22389
+ if (node.leaderTo === void 0) return null;
22390
+ const target = nodesById.get(node.leaderTo);
22391
+ if (target === void 0) return null;
22392
+ if (!isRenderedNode(node) || !isRenderedNode(target)) return null;
22393
+ const targetIsRight = target.cx >= node.cx;
22394
+ const x1 = node.cx + (targetIsRight ? node.w / 2 : -node.w / 2);
22395
+ const x2 = target.cx + (targetIsRight ? -target.w / 2 : target.w / 2);
22396
+ if (targetIsRight ? x2 <= x1 : x1 <= x2) return null;
22397
+ return /* @__PURE__ */ jsx(
22398
+ "line",
22399
+ {
22400
+ "data-cdl-role": "timeline-leader",
22401
+ "data-cdl-leader-from": node.id,
22402
+ "data-cdl-leader-to": node.leaderTo,
22403
+ x1,
22404
+ y1: node.cy,
22405
+ x2,
22406
+ y2: target.cy,
22407
+ stroke: "var(--cdl-line-mute, #a89d8f)",
22408
+ strokeWidth: 1.5,
22409
+ strokeDasharray: "2 4",
22410
+ strokeLinecap: "round"
22411
+ },
22412
+ `timeline-leader-${node.id}`
22413
+ );
22414
+ }),
22415
+ laid.edges.filter(\u7DDA\u3092\u63CF\u304F).map((edge) => /* @__PURE__ */ jsx(
21664
22416
  CdlEdgeView,
21665
22417
  {
21666
22418
  edge,
@@ -21668,16 +22420,19 @@ function CdlStage({
21668
22420
  flowing: \u7126\u70B9\u306E\u96C6\u5408?.has(edge.id) === true && !\u52D5\u304D\u3092\u63A7\u3048\u308B,
21669
22421
  drawing: \u77E2\u5370\u306E\u51FA\u756A.\u4F38\u3070\u3059\u304B(edge.id),
21670
22422
  progress: \u77E2\u5370\u306E\u51FA\u756A.\u4F38\u3070\u3059\u304B(edge.id) ? \u63CF\u304F\u9032\u307F : progress,
21671
- stateValues
22423
+ stateValues,
22424
+ pending: edge.routing === "metro" && \u77E2\u5370\u306E\u51FA\u756A.\u307E\u3060\u304B(edge.id)
21672
22425
  },
21673
22426
  edge.id
21674
22427
  )),
21675
22428
  laid.nodes.map((node) => {
21676
22429
  const \u63CF\u304F = drawSet.has(node.id);
22430
+ const \u6BB5\u968E\u306E\u88DC\u8DB3 = stageNoteNodeIds.has(node.id);
21677
22431
  const view = /* @__PURE__ */ jsx(
21678
22432
  CdlNodeView,
21679
22433
  {
21680
- node,
22434
+ node: \u6BB5\u968E\u306E\u88DC\u8DB3 ? { ...node, subtitle: void 0 } : node,
22435
+ compactTitleInset: \u6BB5\u968E\u306E\u88DC\u8DB3 ? STAGE_CARD_TEXT_INSET : void 0,
21681
22436
  active: activeSet.has(node.id),
21682
22437
  lang: laid.lang,
21683
22438
  textMetrics: laid.textMetrics,
@@ -21687,13 +22442,32 @@ function CdlStage({
21687
22442
  stateValues,
21688
22443
  \u5024\u306E\u540D\u524D,
21689
22444
  currentPhaseId: currentPhase?.id,
21690
- chartLineScale: chartLineScales.get(node.id)
22445
+ chartLineScale: chartLineScales.get(node.id),
22446
+ metroDiagram: \u8DEF\u7DDA\u56F3,
22447
+ metroDecision: metroDecisionIds.has(node.id),
22448
+ stationNameRight: stationNamesRight.has(node.id)
21691
22449
  }
21692
22450
  );
21693
22451
  return /* @__PURE__ */ jsx("g", { children: view }, node.id);
21694
22452
  }),
21695
- laid.edges.filter((edge) => Boolean(edge.dThrough) && \u77E2\u5370\u306E\u51FA\u756A.\u51FA\u308B\u304B(edge.id)).map((edge) => /* @__PURE__ */ jsx(CdlEdgeGlowView, { edge, active: activeSet.has(edge.id) }, `glow-${edge.id}`)),
21696
- laid.edges.filter((edge) => \u77E2\u5370\u306E\u51FA\u756A.\u51FA\u308B\u304B(edge.id)).map((edge) => /* @__PURE__ */ jsx(
22453
+ stageNoteNodes.map((node) => /* @__PURE__ */ jsx(
22454
+ "text",
22455
+ {
22456
+ "data-cdl-role": "stage-note",
22457
+ "data-cdl-stage-node": node.id,
22458
+ x: node.cx + node.w / 2 - STAGE_CARD_TEXT_INSET,
22459
+ y: node.cy + 1,
22460
+ fontSize: 14,
22461
+ fontWeight: 500,
22462
+ textAnchor: "end",
22463
+ dominantBaseline: "middle",
22464
+ fill: "var(--cdl-text-mute, #8a8678)",
22465
+ children: interpolate(node.subtitle, stateValues, \u5024\u306E\u540D\u524D)
22466
+ },
22467
+ `stage-note-${node.id}`
22468
+ )),
22469
+ laid.edges.filter((edge) => Boolean(edge.dThrough) && \u7DDA\u3092\u63CF\u304F(edge)).map((edge) => /* @__PURE__ */ jsx(CdlEdgeGlowView, { edge, active: activeSet.has(edge.id) }, `glow-${edge.id}`)),
22470
+ laid.edges.filter(\u7DDA\u3092\u63CF\u304F).map((edge) => /* @__PURE__ */ jsx(
21697
22471
  CdlEdgeLabelView,
21698
22472
  {
21699
22473
  edge,
@@ -21702,7 +22476,7 @@ function CdlStage({
21702
22476
  },
21703
22477
  `label-${edge.id}`
21704
22478
  )),
21705
- laid.edges.filter((edge) => (edge.headLabel || edge.tailLabel) && \u77E2\u5370\u306E\u51FA\u756A.\u51FA\u308B\u304B(edge.id)).flatMap(
22479
+ laid.edges.filter((edge) => (edge.headLabel || edge.tailLabel) && \u7DDA\u3092\u63CF\u304F(edge)).flatMap(
21706
22480
  (edge) => ["head", "tail"].map((\u7AEF) => /* @__PURE__ */ jsx(
21707
22481
  CdlEdgeEndLabelView,
21708
22482
  {
@@ -21714,6 +22488,7 @@ function CdlStage({
21714
22488
  `${\u7AEF}-label-${edge.id}`
21715
22489
  ))
21716
22490
  ),
22491
+ laid.legend && /* @__PURE__ */ jsx(CdlLegend, { items: laid.legend }),
21717
22492
  debug && /* @__PURE__ */ jsxs("g", { pointerEvents: "none", children: [
21718
22493
  laid.bboxes.map((bbox, idx) => {
21719
22494
  const color = bbox.kind === "node" ? "#3b82f6" : bbox.kind === "lane-label" ? "#a855f7" : bbox.kind === "edge-label" ? "#10b981" : "#f59e0b";