@cardenelabs/cdl 0.120.0 → 0.122.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -12388,6 +12388,7 @@ var KINDS_DRAWING_TITLE_OUTSIDE = /* @__PURE__ */ new Set([
12388
12388
  "dyn-arc",
12389
12389
  "dyn-wave",
12390
12390
  "dyn-polygon",
12391
+ "station",
12391
12392
  ...KINDS_DRAWING_OWN_PAYLOAD
12392
12393
  ]);
12393
12394
  function drawsTitleOutsideBox(kind) {
@@ -12395,7 +12396,7 @@ function drawsTitleOutsideBox(kind) {
12395
12396
  }
12396
12397
  var TITLE_FONT_SIZE_DEFAULT = 18;
12397
12398
  var TITLE_FONT_SIZE = new Map([
12398
- ...["dyn-rect", "dyn-circle", "dyn-arc", "dyn-wave", "dyn-polygon"].map(
12399
+ ...["dyn-rect", "dyn-circle", "dyn-arc", "dyn-wave", "dyn-polygon", "station"].map(
12399
12400
  (k) => [k, 13]
12400
12401
  ),
12401
12402
  ...[
@@ -12446,6 +12447,10 @@ var TITLE_FONT_SIZE = new Map([
12446
12447
  function titleFontSize(kind) {
12447
12448
  return TITLE_FONT_SIZE.get(kind ?? "") ?? TITLE_FONT_SIZE_DEFAULT;
12448
12449
  }
12450
+ var KINDS_WITH_TITLE_WIDTH_OUTSIDE_NODE = /* @__PURE__ */ new Set(["station"]);
12451
+ function skipsNodeTitleWidthCheck(kind) {
12452
+ return KINDS_WITH_TITLE_WIDTH_OUTSIDE_NODE.has(kind ?? "");
12453
+ }
12449
12454
  var TITLE_BOX_PADDING = 52;
12450
12455
  var KINDS_SCALING_TITLE_WHEN_LOW = /* @__PURE__ */ new Set(["actor", "event", "function", "storage"]);
12451
12456
  var KINDS_SCALING_TITLE_ALWAYS = /* @__PURE__ */ new Set([
@@ -12528,7 +12533,8 @@ var KINDS_DRAWING_ONLY_SHAPE = [
12528
12533
  "dyn-circle",
12529
12534
  "dyn-arc",
12530
12535
  "dyn-wave",
12531
- "dyn-polygon"
12536
+ "dyn-polygon",
12537
+ "station"
12532
12538
  ];
12533
12539
  var KINDS_WITHOUT_EYEBROW_TEXT = /* @__PURE__ */ new Set([
12534
12540
  ...KINDS_DRAWING_ONLY_SHAPE,
@@ -13308,13 +13314,13 @@ function detectCollisions(bboxes, edges) {
13308
13314
  }
13309
13315
  function extractPathSegments(d) {
13310
13316
  const segs = [];
13311
- const commandRegex = /([MLCQZmlcqz])([^MLCQZmlcqz]*)/g;
13317
+ const commandRegex = /([MLCQAZmlcqaz])([^MLCQAZmlcqaz]*)/g;
13312
13318
  let cur = { x: 0, y: 0 };
13313
13319
  let m;
13314
13320
  while ((m = commandRegex.exec(d)) !== null) {
13315
13321
  const cmd = m[1];
13316
13322
  const args = (m[2] ?? "").trim().split(/[\s,]+/).filter(Boolean).map(parseFloat);
13317
- const need = cmd === "M" || cmd === "L" ? 2 : cmd === "Q" ? 4 : cmd === "C" ? 6 : 0;
13323
+ const need = cmd === "M" || cmd === "L" ? 2 : cmd === "Q" ? 4 : cmd === "C" ? 6 : cmd === "A" ? 7 : 0;
13318
13324
  if (need > 0 && (args.length < need || !args.slice(0, need).every(Number.isFinite))) continue;
13319
13325
  if (cmd === "M") {
13320
13326
  cur = { x: args[0], y: args[1] };
@@ -13344,6 +13350,10 @@ function extractPathSegments(d) {
13344
13350
  ctrl: [{ x: args[0], y: args[1] }, { x: args[2], y: args[3] }]
13345
13351
  });
13346
13352
  cur = next;
13353
+ } else if (cmd === "A") {
13354
+ const next = { x: args[5], y: args[6] };
13355
+ segs.push({ x1: cur.x, y1: cur.y, x2: next.x, y2: next.y, cmd: "A" });
13356
+ cur = next;
13347
13357
  }
13348
13358
  }
13349
13359
  return segs;
@@ -14546,6 +14556,232 @@ function applyEdgeLabelAndPathShifts(edges, labelShiftMap, edgePathShiftMap) {
14546
14556
  });
14547
14557
  }
14548
14558
 
14559
+ // src/layout/curve-path.ts
14560
+ var SAME_HEIGHT_THRESHOLD = 1;
14561
+ function curvePath(from, to, edge, labelMarginExtra = 0) {
14562
+ const fromLeft = from.cx - from.w / 2;
14563
+ const fromRight = from.cx + from.w / 2;
14564
+ const toLeft = to.cx - to.w / 2;
14565
+ const toRight = to.cx + to.w / 2;
14566
+ let fromSide;
14567
+ let toSide;
14568
+ let x1;
14569
+ let x2;
14570
+ if (toLeft >= fromRight) {
14571
+ fromSide = "right";
14572
+ toSide = "left";
14573
+ x1 = fromRight;
14574
+ x2 = toLeft;
14575
+ } else if (toRight <= fromLeft) {
14576
+ fromSide = "left";
14577
+ toSide = "right";
14578
+ x1 = fromLeft;
14579
+ x2 = toRight;
14580
+ } else {
14581
+ fromSide = "right";
14582
+ toSide = "right";
14583
+ x1 = fromRight;
14584
+ x2 = toRight;
14585
+ }
14586
+ const y1 = from.cy;
14587
+ const y2 = to.cy;
14588
+ const sameHeight = Math.abs(y2 - y1) < SAME_HEIGHT_THRESHOLD;
14589
+ let d;
14590
+ let midX;
14591
+ let midY;
14592
+ let tangentX;
14593
+ let tangentY;
14594
+ if (sameHeight) {
14595
+ d = `M ${x1} ${y1} L ${x2} ${y2}`;
14596
+ midX = (x1 + x2) / 2;
14597
+ midY = (y1 + y2) / 2;
14598
+ tangentX = x2 - x1;
14599
+ tangentY = y2 - y1;
14600
+ } else {
14601
+ let c1x;
14602
+ let c2x;
14603
+ if (fromSide !== toSide) {
14604
+ const direction = x2 > x1 ? 1 : -1;
14605
+ const bulge = Math.max(Math.abs(x2 - x1) / 2, EDGE_STUB_OUT);
14606
+ c1x = x1 + direction * bulge;
14607
+ c2x = x2 - direction * bulge;
14608
+ } else {
14609
+ const bulgeX = Math.max(x1, x2) + EDGE_STUB_OUT;
14610
+ c1x = bulgeX;
14611
+ c2x = bulgeX;
14612
+ }
14613
+ d = `M ${x1} ${y1} C ${c1x} ${y1}, ${c2x} ${y2}, ${x2} ${y2}`;
14614
+ midX = (x1 + 3 * c1x + 3 * c2x + x2) / 8;
14615
+ midY = (y1 + 3 * y1 + 3 * y2 + y2) / 8;
14616
+ tangentX = x2 + c2x - c1x - x1;
14617
+ tangentY = y2 + y2 - y1 - y1;
14618
+ }
14619
+ if (edge?.overlay) {
14620
+ return { d, fromSide, toSide, labelX: midX, labelY: midY, labelAnchor: "middle" };
14621
+ }
14622
+ const tangentLength = Math.hypot(tangentX, tangentY);
14623
+ let normalX = tangentY / tangentLength;
14624
+ let normalY = -tangentX / tangentLength;
14625
+ if (normalY > Number.EPSILON || Math.abs(normalY) <= Number.EPSILON && normalX < 0) {
14626
+ normalX *= -1;
14627
+ normalY *= -1;
14628
+ }
14629
+ const labelW = computeLabelBoxW(edge?.label, edge?.sub);
14630
+ const labelH = labelPillH(edge?.sub);
14631
+ const distance = labelPathClearanceHorizontal(edge) + (Math.abs(normalX) * labelW + Math.abs(normalY) * labelH) / 2 + labelMarginExtra;
14632
+ return {
14633
+ d,
14634
+ fromSide,
14635
+ toSide,
14636
+ labelX: midX + normalX * distance,
14637
+ labelY: midY + normalY * distance,
14638
+ labelAnchor: "middle"
14639
+ };
14640
+ }
14641
+
14642
+ // src/layout/metro-path.ts
14643
+ var METRO_CORNER_RADIUS = 24;
14644
+ var T = Math.SQRT2 - 1;
14645
+ function round2(v) {
14646
+ return Math.round(v * 100) / 100;
14647
+ }
14648
+ function point(p) {
14649
+ return `${round2(p.x)} ${round2(p.y)}`;
14650
+ }
14651
+ function add(p, v, scale) {
14652
+ return { x: p.x + v.x * scale, y: p.y + v.y * scale };
14653
+ }
14654
+ function polyline(points) {
14655
+ const distinct = points.filter((p, i) => {
14656
+ const previous = points[i - 1];
14657
+ return previous === void 0 || p.x !== previous.x || p.y !== previous.y;
14658
+ });
14659
+ return distinct.map((p, i) => `${i === 0 ? "M" : "L"} ${point(p)}`).join(" ");
14660
+ }
14661
+ function metroPath(from, to, edge, labelMarginExtra = 0) {
14662
+ const fromLeft = from.cx - from.w / 2;
14663
+ const fromRight = from.cx + from.w / 2;
14664
+ const toLeft = to.cx - to.w / 2;
14665
+ const toRight = to.cx + to.w / 2;
14666
+ let fromSide;
14667
+ let toSide;
14668
+ let x1;
14669
+ let x2;
14670
+ let dir;
14671
+ if (toLeft >= fromRight) {
14672
+ fromSide = "right";
14673
+ toSide = "left";
14674
+ x1 = fromRight;
14675
+ x2 = toLeft;
14676
+ dir = 1;
14677
+ } else if (toRight <= fromLeft) {
14678
+ fromSide = "left";
14679
+ toSide = "right";
14680
+ x1 = fromLeft;
14681
+ x2 = toRight;
14682
+ dir = -1;
14683
+ } else {
14684
+ fromSide = "right";
14685
+ toSide = "right";
14686
+ x1 = fromRight;
14687
+ x2 = toRight;
14688
+ dir = 1;
14689
+ }
14690
+ const y1 = from.cy;
14691
+ const y2 = to.cy;
14692
+ const dy = y2 - y1;
14693
+ const sgn = Math.sign(dy);
14694
+ let d;
14695
+ let mid;
14696
+ let tangent;
14697
+ if (Math.abs(dy) < SAME_HEIGHT_THRESHOLD) {
14698
+ d = `M ${point({ x: x1, y: y1 })} L ${point({ x: x2, y: y2 })}`;
14699
+ mid = { x: (x1 + x2) / 2, y: (y1 + y2) / 2 };
14700
+ tangent = { x: x2 - x1, y: y2 - y1 };
14701
+ } else if (fromSide !== toSide && Math.abs(x2 - x1) >= Math.abs(dy)) {
14702
+ const horizontal = Math.abs(x2 - x1);
14703
+ const s = (horizontal - Math.abs(dy)) / 2;
14704
+ const p0 = { x: x1, y: y1 };
14705
+ const p1 = { x: x1 + dir * s, y: y1 };
14706
+ const p2 = { x: x2 - dir * s, y: y2 };
14707
+ const p3 = { x: x2, y: y2 };
14708
+ const u = { x: dir, y: 0 };
14709
+ const w = { x: dir / Math.SQRT2, y: sgn / Math.SQRT2 };
14710
+ const r = Math.min(
14711
+ METRO_CORNER_RADIUS,
14712
+ s / T,
14713
+ Math.SQRT2 * Math.abs(dy) / (2 * T)
14714
+ );
14715
+ const t = r * T;
14716
+ if (r >= 0.5) {
14717
+ const f1 = dir * sgn > 0 ? 1 : 0;
14718
+ d = [
14719
+ `M ${point(p0)}`,
14720
+ `L ${point(add(p1, u, -t))}`,
14721
+ `A ${round2(r)} ${round2(r)} 0 0 ${f1} ${point(add(p1, w, t))}`,
14722
+ `L ${point(add(p2, w, -t))}`,
14723
+ `A ${round2(r)} ${round2(r)} 0 0 ${1 - f1} ${point(add(p2, u, t))}`,
14724
+ `L ${point(p3)}`
14725
+ ].join(" ");
14726
+ } else {
14727
+ d = polyline([p0, p1, p2, p3]);
14728
+ }
14729
+ mid = { x: (x1 + x2) / 2, y: (y1 + y2) / 2 };
14730
+ tangent = w;
14731
+ } else {
14732
+ fromSide = "right";
14733
+ toSide = "right";
14734
+ x1 = fromRight;
14735
+ x2 = toRight;
14736
+ const xa = Math.max(x1, x2) + EDGE_STUB_OUT;
14737
+ const h = Math.abs(dy) / 2;
14738
+ const p0 = { x: x1, y: y1 };
14739
+ const p1 = { x: xa, y: y1 };
14740
+ const p2 = { x: xa + h, y: y1 + sgn * h };
14741
+ const p3 = { x: xa, y: y2 };
14742
+ const p4 = { x: x2, y: y2 };
14743
+ const u = { x: 1, y: 0 };
14744
+ const w1 = { x: 1 / Math.SQRT2, y: sgn / Math.SQRT2 };
14745
+ const w2 = { x: -1 / Math.SQRT2, y: sgn / Math.SQRT2 };
14746
+ const r = Math.min(METRO_CORNER_RADIUS, h);
14747
+ const t = r * T;
14748
+ const sweep = sgn > 0 ? 1 : 0;
14749
+ d = [
14750
+ `M ${point(p0)}`,
14751
+ `L ${point(add(p1, u, -t))}`,
14752
+ `A ${round2(r)} ${round2(r)} 0 0 ${sweep} ${point(add(p1, w1, t))}`,
14753
+ `L ${point(add(p2, w1, -r))}`,
14754
+ `A ${round2(r)} ${round2(r)} 0 0 ${sweep} ${point(add(p2, w2, r))}`,
14755
+ `L ${point(add(p3, w2, -t))}`,
14756
+ `A ${round2(r)} ${round2(r)} 0 0 ${sweep} ${point(add(p3, u, -t))}`,
14757
+ `L ${point(p4)}`
14758
+ ].join(" ");
14759
+ mid = { x: p2.x - T * r, y: p2.y };
14760
+ tangent = { x: 0, y: sgn };
14761
+ }
14762
+ if (edge?.overlay) {
14763
+ return { d, fromSide, toSide, labelX: mid.x, labelY: mid.y, labelAnchor: "middle" };
14764
+ }
14765
+ const tangentLength = Math.hypot(tangent.x, tangent.y);
14766
+ let normalX = tangent.y / tangentLength;
14767
+ let normalY = -tangent.x / tangentLength;
14768
+ if (normalY > Number.EPSILON || Math.abs(normalY) <= Number.EPSILON && normalX < 0) {
14769
+ normalX *= -1;
14770
+ normalY *= -1;
14771
+ }
14772
+ const labelW = computeLabelBoxW(edge?.label, edge?.sub);
14773
+ const labelH = labelPillH(edge?.sub);
14774
+ const distance = labelPathClearanceHorizontal(edge) + (Math.abs(normalX) * labelW + Math.abs(normalY) * labelH) / 2 + labelMarginExtra;
14775
+ return {
14776
+ d,
14777
+ fromSide,
14778
+ toSide,
14779
+ labelX: mid.x + normalX * distance,
14780
+ labelY: mid.y + normalY * distance,
14781
+ labelAnchor: "middle"
14782
+ };
14783
+ }
14784
+
14549
14785
  // src/types.ts
14550
14786
  var TONES = ["accent", "teal", "success", "error", "warning", "info"];
14551
14787
  var NODE_KINDS = [
@@ -14673,6 +14909,8 @@ var NODE_KINDS = [
14673
14909
  // 状態遷移図が使う。 印そのものが意味を持つので文字は描かない
14674
14910
  "mark-start",
14675
14911
  "mark-end",
14912
+ // 路線図の駅 (#996) ... 線路上の点を小さな丸で示し、名前と説明は丸の外に置く
14913
+ "station",
14676
14914
  // 時系列のやり取り (#578) ... 1 つの箱が図を丸ごと描く。 縦が時間で、参加者は箱を持たない
14677
14915
  "sequence-board"
14678
14916
  ];
@@ -17384,6 +17622,7 @@ function runAxes(laid, diag, violations, counts, push, profile, skippedAxes, \u5
17384
17622
  const LIFELINE_W_THRESHOLD = 8;
17385
17623
  for (const n of laid.nodes) {
17386
17624
  if (n.w < LIFELINE_W_THRESHOLD) continue;
17625
+ if (n.kind === "station") continue;
17387
17626
  if (n.w < MIN_NODE_W || n.h < MIN_NODE_H) {
17388
17627
  push(
17389
17628
  "node-visibility",
@@ -17405,6 +17644,7 @@ function runAxes(laid, diag, violations, counts, push, profile, skippedAxes, \u5
17405
17644
  if (n.id.endsWith("-spacer")) continue;
17406
17645
  if (!rendersTitleText(n.kind)) continue;
17407
17646
  if (fitsOwnTitleWidth(n.kind)) continue;
17647
+ if (skipsNodeTitleWidthCheck(n.kind)) continue;
17408
17648
  if (!isRenderedNode(n)) continue;
17409
17649
  const titleTrimmed = n.title?.trim() ?? "";
17410
17650
  if (!titleTrimmed) continue;
@@ -18009,6 +18249,9 @@ function runAxes(laid, diag, violations, counts, push, profile, skippedAxes, \u5
18009
18249
  for (let i = 0; i + 1 < sorted.length; i++) {
18010
18250
  const upper = sorted[i];
18011
18251
  const lower = sorted[i + 1];
18252
+ if (upper.cx + upper.w / 2 <= lower.cx - lower.w / 2 || lower.cx + lower.w / 2 <= upper.cx - upper.w / 2) {
18253
+ continue;
18254
+ }
18012
18255
  const upperBottom = upper.cy + upper.h / 2;
18013
18256
  const lowerTop = lower.cy - lower.h / 2;
18014
18257
  const gap = lowerTop - upperBottom;
@@ -18640,11 +18883,18 @@ function runAxes(laid, diag, violations, counts, push, profile, skippedAxes, \u5
18640
18883
  {
18641
18884
  const crestSpans = (segs, peakY) => {
18642
18885
  const out = [];
18886
+ let \u5186\u5F27\u306E\u59CB\u70B9 = null;
18643
18887
  for (const s of segs) {
18644
- if (Math.abs(s.y1 - s.y2) > 1) continue;
18645
- if (Math.abs(s.y1 - peakY) > 1) continue;
18888
+ if (s.cmd === "A") {
18889
+ \u5186\u5F27\u306E\u59CB\u70B9 ??= { x: s.x1, y: s.y1 };
18890
+ continue;
18891
+ }
18892
+ const \u59CB\u70B9 = \u5186\u5F27\u306E\u59CB\u70B9 ?? { x: s.x1, y: s.y1 };
18893
+ \u5186\u5F27\u306E\u59CB\u70B9 = null;
18894
+ if (Math.abs(\u59CB\u70B9.y - s.y2) > 1) continue;
18895
+ if (Math.abs(\u59CB\u70B9.y - peakY) > 1) continue;
18646
18896
  if (s.cmd !== "L" && !(s.ctrl ?? []).every((c) => Math.abs(c.y - peakY) <= 1)) continue;
18647
- out.push({ minX: Math.min(s.x1, s.x2), maxX: Math.max(s.x1, s.x2) });
18897
+ out.push({ minX: Math.min(\u59CB\u70B9.x, s.x2), maxX: Math.max(\u59CB\u70B9.x, s.x2) });
18648
18898
  }
18649
18899
  return out;
18650
18900
  };
@@ -19335,6 +19585,8 @@ var TOKENS = {
19335
19585
  // 箱としての最小 (80x40) より大きく、名前を持つ箱 (card 320x150) よりは明らかに小さい
19336
19586
  "mark-start": { w: 96, h: 96 },
19337
19587
  "mark-end": { w: 96, h: 96 },
19588
+ // 路線図の駅 (#996) ... 正方形にして小さな丸が縦横どちらにも歪まないようにする。
19589
+ station: { w: 24, h: 24 },
19338
19590
  // 時系列のやり取り (#578)。 実寸は中身から決まる (`sequenceBoardMetrics`) ので、
19339
19591
  // ここは書かなかった時の受け皿
19340
19592
  "sequence-board": { w: 960, h: 640 }
@@ -19359,6 +19611,7 @@ var LANE_PAD_MIN = 25;
19359
19611
  // src/layout/edges.ts
19360
19612
  var VERTICAL_DETOUR_GAP = 50;
19361
19613
  var VERTICAL_DETOUR_CORNER = 14;
19614
+ var OVERLAY_LABEL_ON_PATH_TOLERANCE = 1;
19362
19615
  function verticalCornerDir(from, to) {
19363
19616
  return to < from ? -1 : 1;
19364
19617
  }
@@ -19442,6 +19695,36 @@ function layoutEdges(diag, nodes, lanes = []) {
19442
19695
  });
19443
19696
  } else if (isSelfLoop(e)) {
19444
19697
  selfLoopEdges.push(e);
19698
+ } else if (e.routing === "curve") {
19699
+ const from = nmap.get(e.from);
19700
+ const to = nmap.get(e.to);
19701
+ if (!from) throw new Error(`cdl layout: edge "${e.id}" \u306E from "${e.from}" \u304C node \u306B\u7121\u3044`);
19702
+ if (!to) throw new Error(`cdl layout: edge "${e.id}" \u306E to "${e.to}" \u304C node \u306B\u7121\u3044`);
19703
+ const c = curvePath(from, to, e, labelMarginExtra);
19704
+ positionedEdges.push({
19705
+ ...e,
19706
+ d: c.d,
19707
+ fromSide: c.fromSide,
19708
+ toSide: c.toSide,
19709
+ labelX: c.labelX + (e.labelOffsetX ?? 0),
19710
+ labelY: c.labelY + (e.labelOffsetY ?? 0),
19711
+ labelAnchor: c.labelAnchor
19712
+ });
19713
+ } else if (e.routing === "metro") {
19714
+ const from = nmap.get(e.from);
19715
+ const to = nmap.get(e.to);
19716
+ if (!from) throw new Error(`cdl layout: edge "${e.id}" \u306E from "${e.from}" \u304C node \u306B\u7121\u3044`);
19717
+ if (!to) throw new Error(`cdl layout: edge "${e.id}" \u306E to "${e.to}" \u304C node \u306B\u7121\u3044`);
19718
+ const m = metroPath(from, to, e, labelMarginExtra);
19719
+ positionedEdges.push({
19720
+ ...e,
19721
+ d: m.d,
19722
+ fromSide: m.fromSide,
19723
+ toSide: m.toSide,
19724
+ labelX: m.labelX + (e.labelOffsetX ?? 0),
19725
+ labelY: m.labelY + (e.labelOffsetY ?? 0),
19726
+ labelAnchor: m.labelAnchor
19727
+ });
19445
19728
  } else {
19446
19729
  autoEdges.push(e);
19447
19730
  }
@@ -20946,9 +21229,11 @@ function hopEdgeCrossings(edges) {
20946
21229
  ends.set(e.id, pathEnds(segs));
20947
21230
  }
20948
21231
  for (const e of edges) {
21232
+ if (e.routing === "metro") continue;
20949
21233
  if ((e.d.match(/M/g) ?? []).length !== 1) continue;
20950
21234
  const own = extractPathSegments(e.d);
20951
21235
  if (own.length === 0) continue;
21236
+ if (own.some((s) => s.cmd === "A")) continue;
20952
21237
  const myEnds = ends.get(e.id) ?? [];
20953
21238
  const hops = /* @__PURE__ */ new Map();
20954
21239
  for (let i = 0; i < own.length; i++) {
@@ -21036,7 +21321,7 @@ function pathVertices(d) {
21036
21321
  const out = [{ x: first.x1, y: first.y1 }];
21037
21322
  for (let i = 0; i < segs.length; i++) {
21038
21323
  const s = segs[i];
21039
- if (s.cmd === "C") return null;
21324
+ if (s.cmd === "C" || s.cmd === "A") return null;
21040
21325
  if (s.cmd === "Q") {
21041
21326
  const c = s.ctrl?.[0];
21042
21327
  if (!c) return null;
@@ -21099,7 +21384,7 @@ function separateSharedStraightRuns(edges, nodes) {
21099
21384
  const v = pathVertices(e.d);
21100
21385
  if (v && v.length >= 2) verts.set(e.id, v);
21101
21386
  }
21102
- if (verts.size < 2) return;
21387
+ if (verts.size === 0) return;
21103
21388
  const moved = /* @__PURE__ */ new Set();
21104
21389
  const fits = (e, pts, k, ax, delta) => {
21105
21390
  const \u8A66\u3057 = pts.map(
@@ -21117,33 +21402,40 @@ function separateSharedStraightRuns(edges, nodes) {
21117
21402
  for (let j = i + 1; j < edges.length; j++) {
21118
21403
  const \u524D = edges[i];
21119
21404
  const \u5F8C = edges[j];
21120
- const A = verts.get(\u524D.id);
21121
- const B = verts.get(\u5F8C.id);
21122
- if (!A || !B) continue;
21123
- if (\u524D.from === \u5F8C.from) continue;
21405
+ if (\u524D.routing === "metro" && \u5F8C.routing === "metro") continue;
21406
+ const \u56FA\u5B9A = \u5F8C.routing === "metro" ? \u5F8C : \u524D;
21407
+ const \u52D5\u304B\u3059 = \u5F8C.routing === "metro" ? \u524D : \u5F8C;
21408
+ const B = verts.get(\u52D5\u304B\u3059.id);
21409
+ if (!B) continue;
21410
+ const \u56FA\u5B9A\u306E\u89D2 = verts.get(\u56FA\u5B9A.id);
21411
+ const \u56FA\u5B9A\u306E\u7D4C\u8DEF = \u56FA\u5B9A\u306E\u89D2 ? [\u56FA\u5B9A\u306E\u89D2] : \u56FA\u5B9A.routing === "metro" ? extractPathSegments(\u56FA\u5B9A.d).filter((s) => s.cmd === "L").map((s) => [{ x: s.x1, y: s.y1 }, { x: s.x2, y: s.y2 }]) : [];
21412
+ if (\u56FA\u5B9A\u306E\u7D4C\u8DEF.length === 0) continue;
21413
+ if (\u56FA\u5B9A.from === \u52D5\u304B\u3059.from) continue;
21124
21414
  for (const ax of [0, 1]) {
21125
- for (let ka = 0; ka + 1 < A.length; ka++) {
21126
- if (!runIsAligned(A, ka, ax)) continue;
21127
- for (let kb = 0; kb + 1 < B.length; kb++) {
21128
- if (!runIsAligned(B, kb, ax)) continue;
21129
- if (kb === 0 || kb + 1 === B.length - 1) continue;
21130
- const \u5EA7\u6A19A = ax === 0 ? A[ka].x : A[ka].y;
21131
- const \u5EA7\u6A19B = ax === 0 ? B[kb].x : B[kb].y;
21132
- if (Math.abs(\u5EA7\u6A19A - \u5EA7\u6A19B) > SHARED_RUN_EPS) continue;
21133
- const sa = runSpan(A, ka, ax);
21134
- const sb = runSpan(B, kb, ax);
21135
- if (Math.min(sa.hi, sb.hi) - Math.max(sa.lo, sb.lo) < SHARED_RUN_MIN) continue;
21136
- for (const delta of [SHARED_RUN_SEPARATION, -SHARED_RUN_SEPARATION]) {
21137
- if (!fits(\u5F8C, B, kb, ax, delta)) continue;
21138
- if (ax === 0) {
21139
- B[kb].x += delta;
21140
- B[kb + 1].x += delta;
21141
- } else {
21142
- B[kb].y += delta;
21143
- B[kb + 1].y += delta;
21415
+ for (const A of \u56FA\u5B9A\u306E\u7D4C\u8DEF) {
21416
+ for (let ka = 0; ka + 1 < A.length; ka++) {
21417
+ if (!runIsAligned(A, ka, ax)) continue;
21418
+ for (let kb = 0; kb + 1 < B.length; kb++) {
21419
+ if (!runIsAligned(B, kb, ax)) continue;
21420
+ if (kb === 0 || kb + 1 === B.length - 1) continue;
21421
+ const \u5EA7\u6A19A = ax === 0 ? A[ka].x : A[ka].y;
21422
+ const \u5EA7\u6A19B = ax === 0 ? B[kb].x : B[kb].y;
21423
+ if (Math.abs(\u5EA7\u6A19A - \u5EA7\u6A19B) > SHARED_RUN_EPS) continue;
21424
+ const sa = runSpan(A, ka, ax);
21425
+ const sb = runSpan(B, kb, ax);
21426
+ if (Math.min(sa.hi, sb.hi) - Math.max(sa.lo, sb.lo) < SHARED_RUN_MIN) continue;
21427
+ for (const delta of [SHARED_RUN_SEPARATION, -SHARED_RUN_SEPARATION]) {
21428
+ if (!fits(\u52D5\u304B\u3059, B, kb, ax, delta)) continue;
21429
+ if (ax === 0) {
21430
+ B[kb].x += delta;
21431
+ B[kb + 1].x += delta;
21432
+ } else {
21433
+ B[kb].y += delta;
21434
+ B[kb + 1].y += delta;
21435
+ }
21436
+ moved.add(\u52D5\u304B\u3059.id);
21437
+ break;
21144
21438
  }
21145
- moved.add(\u5F8C.id);
21146
- break;
21147
21439
  }
21148
21440
  }
21149
21441
  }
@@ -21170,12 +21462,19 @@ function pullOutgoingFirstCorner(edges, nodes) {
21170
21462
  let \u672D\u3092\u65E7\u7D4C\u8DEF\u3078\u8FFD\u5F93\u6E08\u307F = false;
21171
21463
  for (const list of \u675F.values()) {
21172
21464
  if (list.length < 2) continue;
21173
- const \u4E26\u3073 = list.flatMap((e) => {
21465
+ const \u4E26\u3073 = list.map((e) => {
21174
21466
  const pts = pathVertices(e.d);
21175
- if (!pts || pts.length < 2) return [];
21176
- return [{ e, pts, \u6700\u521D\u306E\u9577\u3055: Math.hypot(pts[1].x - pts[0].x, pts[1].y - pts[0].y) }];
21177
- }).sort((a, b) => b.\u6700\u521D\u306E\u9577\u3055 - a.\u6700\u521D\u306E\u9577\u3055);
21467
+ if (pts && pts.length >= 2) {
21468
+ return { e, pts, \u6700\u521D\u306E\u9577\u3055: Math.hypot(pts[1].x - pts[0].x, pts[1].y - pts[0].y) };
21469
+ }
21470
+ if (e.routing !== "metro") return null;
21471
+ const first = extractPathSegments(e.d)[0];
21472
+ if (!first) return null;
21473
+ return { e, pts: null, \u6700\u521D\u306E\u9577\u3055: Math.hypot(first.x2 - first.x1, first.y2 - first.y1) };
21474
+ }).filter((x) => x !== null).sort((a, b) => b.\u6700\u521D\u306E\u9577\u3055 - a.\u6700\u521D\u306E\u9577\u3055);
21178
21475
  for (const { e, pts, \u6700\u521D\u306E\u9577\u3055 } of \u4E26\u3073.slice(1)) {
21476
+ if (e.routing === "metro") continue;
21477
+ if (!pts) continue;
21179
21478
  if ((\u540C\u3058\u7D44\u306E\u6570.get(`${e.from}->${e.to}`) ?? 0) > 1) continue;
21180
21479
  if (pts.length < 3 || \u6700\u521D\u306E\u9577\u3055 <= EDGE_STUB_OUT + VERTICAL_DETOUR_CORNER) continue;
21181
21480
  const \u6A2A = e.fromSide === "left" || e.fromSide === "right";
@@ -21229,7 +21528,8 @@ var ENDPOINT_FAN_MIN = 20;
21229
21528
  var ENDPOINT_FAN_RUNUP = 40;
21230
21529
  var MIXED_ROOT_OVERLAP_MIN = 24;
21231
21530
  function fanIncomingEndpoints(edges, nodes) {
21232
- if (edges.length < 2) return;
21531
+ const \u7D42\u70B9\u306E\u79FB\u52D5 = /* @__PURE__ */ new Map();
21532
+ if (edges.length < 2) return \u7D42\u70B9\u306E\u79FB\u52D5;
21233
21533
  const nmap = new Map(nodes.map((n) => [n.id, n]));
21234
21534
  const \u5E73\u5766\u306A\u533A\u9593 = new Map(edges.map((e) => [e.id, flattenPathSegments(extractPathSegments(e.d))]));
21235
21535
  const \u8EF8\u306B\u5E73\u884C\u306A\u91CD\u306A\u308A\u9577\u3055 = (\u4E00\u65B9, \u4ED6\u65B9) => {
@@ -21299,6 +21599,7 @@ function fanIncomingEndpoints(edges, nodes) {
21299
21599
  const d = \u4E2D\u592E\u304C\u585E\u304C\u3063\u3066\u3044\u308B ? \u4E26\u3073[k].\u6DF1\u3055 < \u8FBA\u306E\u4E2D\u5FC3 ? -(++\u8CA0\u5074\u306E\u5E2D * \u9593\u9694) : ++\u6B63\u5074\u306E\u5E2D * \u9593\u9694 : k * \u9593\u9694 - \u4E2D\u5FC3;
21300
21600
  if (Math.abs(d) < 1e-3) continue;
21301
21601
  const e = \u4E26\u3073[k].e;
21602
+ if (e.routing === "metro") continue;
21302
21603
  const \u524D = e.d;
21303
21604
  const \u524D\u306E\u4EA4\u5DEE = countEdgeCrossings(edges).count;
21304
21605
  const \u9577\u3044\u52A9\u8D70 = \u4E26\u3073[k].\u9577\u3044\u52A9\u8D70;
@@ -21310,6 +21611,68 @@ function fanIncomingEndpoints(edges, nodes) {
21310
21611
  if (countEdgeCrossings(edges).count <= \u524D\u306E\u4EA4\u5DEE) break;
21311
21612
  e.d = \u524D;
21312
21613
  }
21614
+ if (e.d !== \u524D) \u7D42\u70B9\u306E\u79FB\u52D5.set(e.id, \u6A2A ? { dx: 0, dy: d } : { dx: d, dy: 0 });
21615
+ }
21616
+ }
21617
+ return \u7D42\u70B9\u306E\u79FB\u52D5;
21618
+ }
21619
+ function followLabelsToFannedTails(edges, obstacles, shifts) {
21620
+ if (shifts.size === 0) return;
21621
+ const \u6E2C\u308A\u65B9 = \u7DDA\u306E\u6E2C\u308A\u65B9\u3092\u4F5C\u308B();
21622
+ const labels = edges.filter(hasRenderedLabel).map((e) => ({
21623
+ kind: "edge-label",
21624
+ id: e.id,
21625
+ ...computeLabelBBoxWorld(e)
21626
+ }));
21627
+ const labelIndexByEdgeId = new Map(labels.map((label, index) => [label.id, index]));
21628
+ const edgeById = new Map(edges.map((e) => [e.id, e]));
21629
+ const \u5468\u308A = {
21630
+ \u7BB1\u305F\u3061: obstacles.filter((o) => o.kind === "node" || o.kind === "lane-label"),
21631
+ labels,
21632
+ edges,
21633
+ edgeById,
21634
+ \u6E2C\u308A\u65B9
21635
+ };
21636
+ for (const e of edges) {
21637
+ const \u79FB\u52D5 = shifts.get(e.id);
21638
+ if (!\u79FB\u52D5 || !hasRenderedLabel(e)) continue;
21639
+ const labelIndex = labelIndexByEdgeId.get(e.id);
21640
+ if (labelIndex === void 0) continue;
21641
+ const \u5143\u306E\u672D = labels[labelIndex];
21642
+ const \u81EA\u5206\u306E\u533A\u9593 = \u6E2C\u308A\u65B9.\u533A\u9593(e.id, e.d);
21643
+ if (e.overlay && measurePathLabelGapSegs(\u81EA\u5206\u306E\u533A\u9593, {
21644
+ x: \u5143\u306E\u672D.x + \u5143\u306E\u672D.w / 2,
21645
+ y: \u5143\u306E\u672D.y + \u5143\u306E\u672D.h / 2,
21646
+ w: 0,
21647
+ h: 0
21648
+ }) <= OVERLAY_LABEL_ON_PATH_TOLERANCE) continue;
21649
+ const \u6700\u5F8C = \u81EA\u5206\u306E\u533A\u9593.at(-1);
21650
+ if (!\u6700\u5F8C) continue;
21651
+ if (!segmentsCrossRect([\u6700\u5F8C], \u5143\u306E\u672D)) continue;
21652
+ const \u5019\u88DCa = {
21653
+ labelX: e.labelX + \u79FB\u52D5.dx,
21654
+ labelY: e.labelY + \u79FB\u52D5.dy,
21655
+ labelAnchor: e.labelAnchor
21656
+ };
21657
+ const \u5019\u88DCa\u306E\u7BB1 = computeLabelBBoxWorld({ ...e, ...\u5019\u88DCa });
21658
+ const \u6700\u5F8C\u306F\u7E26 = Math.abs(\u6700\u5F8C.x2 - \u6700\u5F8C.x1) <= Math.abs(\u6700\u5F8C.y2 - \u6700\u5F8C.y1);
21659
+ const \u5019\u88DC = [
21660
+ \u5019\u88DCa,
21661
+ {
21662
+ labelX: \u6700\u5F8C\u306F\u7E26 ? \u6700\u5F8C.x1 * 2 - (\u5019\u88DCa\u306E\u7BB1.x + \u5019\u88DCa\u306E\u7BB1.w / 2) : \u5019\u88DCa\u306E\u7BB1.x + \u5019\u88DCa\u306E\u7BB1.w / 2,
21663
+ labelY: \u6700\u5F8C\u306F\u7E26 ? \u5019\u88DCa\u306E\u7BB1.y + \u5019\u88DCa\u306E\u7BB1.h / 2 : \u6700\u5F8C.y1 * 2 - (\u5019\u88DCa\u306E\u7BB1.y + \u5019\u88DCa\u306E\u7BB1.h / 2),
21664
+ labelAnchor: "middle"
21665
+ }
21666
+ ];
21667
+ for (const c of \u5019\u88DC) {
21668
+ const \u672D = { kind: "edge-label", id: e.id, ...computeLabelBBoxWorld({ ...e, ...c }) };
21669
+ if (segmentsCrossRect([\u6700\u5F8C], \u672D)) continue;
21670
+ if (\u672D\u306E\u79FB\u52D5\u3092\u585E\u3050\u76F8\u624B\u3092\u63A2\u3059(\u5468\u308A, \u5143\u306E\u672D, \u672D) !== null) continue;
21671
+ e.labelX = c.labelX;
21672
+ e.labelY = c.labelY;
21673
+ e.labelAnchor = c.labelAnchor;
21674
+ labels[labelIndex] = \u672D;
21675
+ break;
21313
21676
  }
21314
21677
  }
21315
21678
  }
@@ -21326,7 +21689,7 @@ function \u672B\u5C3E\u3092\u8FBA\u306B\u6CBF\u3063\u3066\u305A\u3089\u3059(e, \
21326
21689
  if ((e.d.match(/M/g) ?? []).length !== 1) return null;
21327
21690
  const segs = extractPathSegments(e.d);
21328
21691
  if (segs.length === 0) return null;
21329
- if (segs.some((s) => s.cmd === "C")) return null;
21692
+ if (segs.some((s) => s.cmd === "C" || s.cmd === "A")) return null;
21330
21693
  const last = segs[segs.length - 1];
21331
21694
  if (last.cmd !== "L") return null;
21332
21695
  const \u5782\u76F4\u306B\u5165\u308B = \u6A2A ? Math.abs(last.y2 - last.y1) < 0.5 && Math.abs(last.x2 - last.x1) > 0.5 : Math.abs(last.x2 - last.x1) < 0.5 && Math.abs(last.y2 - last.y1) > 0.5;
@@ -21592,9 +21955,9 @@ function recomputeContainedLanes(lanes, nodes) {
21592
21955
  }
21593
21956
 
21594
21957
  // src/layout/head-label.ts
21595
- var \u7AEF\u306E\u5B57\u306E\u5927\u304D\u3055 = EDGE_LABEL_TEXT.sub.fontSize;
21958
+ var \u7AEF\u306E\u5B57\u306E\u5927\u304D\u3055 = 26;
21596
21959
  var \u7BB1\u306E\u9AD8\u3055 = Math.round(\u7AEF\u306E\u5B57\u306E\u5927\u304D\u3055 * 1.2);
21597
- var \u7AEF\u306E\u5B57\u3068\u5370\u306E\u4F59\u767D = Math.round(\u7BB1\u306E\u9AD8\u3055 / 2);
21960
+ var \u7AEF\u306E\u5B57\u3068\u5370\u306E\u4F59\u767D = 12;
21598
21961
  var \u7AEF\u306E\u5370\u306E\u5F35\u308A\u51FA\u3057 = EDGE_HEAD_MARKER_SIZE.\u65E2\u5B9A / 2;
21599
21962
  var \u7AEF\u306E\u5B57\u3092\u623B\u3059\u8DDD\u96E2 = EDGE_HEAD_MARKER_SIZE.\u65E2\u5B9A + \u7AEF\u306E\u5B57\u3068\u5370\u306E\u4F59\u767D;
21600
21963
  var \u7AEF\u306E\u5B57\u3068\u7DDA\u306E\u9699\u9593 = \u7AEF\u306E\u5370\u306E\u5F35\u308A\u51FA\u3057 + \u7AEF\u306E\u5B57\u3068\u5370\u306E\u4F59\u767D;
@@ -21617,13 +21980,16 @@ function \u5411\u304D\u3092\u53D6\u308B(end, \u5019\u88DC) {
21617
21980
  }
21618
21981
  return null;
21619
21982
  }
21620
- function \u7AEF\u3067\u306E\u5411\u304D(d, \u7AEF) {
21983
+ function \u7DDA\u306E\u7AEF\u306E\u5411\u304D(d, \u7AEF) {
21621
21984
  const segs = extractPathSegments(d);
21622
21985
  if (segs.length === 0) return null;
21623
21986
  if (\u7AEF === "head") {
21624
21987
  const last = segs[segs.length - 1];
21625
21988
  const end2 = { x: last.x2, y: last.y2 };
21626
- const \u5019\u88DC2 = segs.slice().reverse().flatMap((s) => [s.cmd === "Q" ? s.ctrl?.[0] : s.cmd === "C" ? s.ctrl?.[1] : void 0, { x: s.x1, y: s.y1 }]);
21989
+ const \u5019\u88DC2 = segs.slice().reverse().flatMap((s) => {
21990
+ const \u5236\u5FA1\u70B9 = s.cmd === "C" ? [s.ctrl?.[1], s.ctrl?.[0]] : s.cmd === "Q" ? [s.ctrl?.[0]] : [];
21991
+ return [...\u5236\u5FA1\u70B9, { x: s.x1, y: s.y1 }];
21992
+ });
21627
21993
  return \u5411\u304D\u3092\u53D6\u308B(end2, \u5019\u88DC2);
21628
21994
  }
21629
21995
  const first = segs[0];
@@ -21634,7 +22000,7 @@ function \u7AEF\u3067\u306E\u5411\u304D(d, \u7AEF) {
21634
22000
  function \u7AEF\u306E\u5B57\u3092\u7F6E\u304F(edge, \u7AEF = "head") {
21635
22001
  const text = (\u7AEF === "head" ? edge.headLabel : edge.tailLabel)?.trim();
21636
22002
  if (!text) return null;
21637
- const \u5411\u304D = \u7AEF\u3067\u306E\u5411\u304D(edge.d, \u7AEF);
22003
+ const \u5411\u304D = \u7DDA\u306E\u7AEF\u306E\u5411\u304D(edge.d, \u7AEF);
21638
22004
  if (!\u5411\u304D) return null;
21639
22005
  const { end, ux, uy } = \u5411\u304D;
21640
22006
  const bx = end.x - ux * \u7AEF\u306E\u5B57\u3092\u623B\u3059\u8DDD\u96E2;
@@ -21962,22 +22328,23 @@ function layout(diag) {
21962
22328
  }
21963
22329
  }
21964
22330
  const positionedEdgeIds = new Set(
21965
- diag.edges.filter((e) => e.posX1 !== void 0 && e.posY1 !== void 0 && e.posX2 !== void 0 && e.posY2 !== void 0).map((e) => e.id)
22331
+ diag.edges.filter(
22332
+ (e) => e.routing === "curve" || e.routing === "metro" || e.posX1 !== void 0 && e.posY1 !== void 0 && e.posX2 !== void 0 && e.posY2 !== void 0
22333
+ ).map((e) => e.id)
21966
22334
  );
21967
22335
  bowSharedStraightPaths(edges, nodesFinal, positionedEdgeIds);
21968
22336
  pullOutgoingFirstCorner(edges, nodesFinal);
21969
- fanIncomingEndpoints(edges, nodesFinal);
22337
+ const fannedTailShifts = fanIncomingEndpoints(edges, nodesFinal);
21970
22338
  separateSharedStraightRuns(edges, nodesFinal);
21971
22339
  hopEdgeCrossings(edges);
22340
+ const labelClearanceObstacles = collectBBoxes(containedLanesFinal, nodesFinal, []).filter(
22341
+ (b) => b.kind === "node" || b.kind === "lane-label"
22342
+ );
22343
+ followLabelsToFannedTails(edges, labelClearanceObstacles, fannedTailShifts);
21972
22344
  repositionParallelLabels(edges, nodesFinal);
21973
22345
  slideLabelsAlongOwnPath(edges, nodesFinal);
21974
22346
  if (!isChainEdgeLabelDisabled()) {
21975
- edges = resolveLabelClearanceAfterPathPasses(
21976
- edges,
21977
- collectBBoxes(containedLanesFinal, nodesFinal, []).filter(
21978
- (b) => b.kind === "node" || b.kind === "lane-label"
21979
- )
21980
- );
22347
+ edges = resolveLabelClearanceAfterPathPasses(edges, labelClearanceObstacles);
21981
22348
  }
21982
22349
  const bboxes = collectBBoxes(containedLanesFinal, nodesFinal, edges);
21983
22350
  const collisions = detectCollisions(bboxes, edges);
@@ -22586,11 +22953,13 @@ function applyHeuristics(diag, laid, violations, applied) {
22586
22953
  violations.filter((v) => v.axis === "edge-node-cross").map((v) => extractEdgeId(v.detail)).filter((id) => id !== null)
22587
22954
  );
22588
22955
  if (h3Edges.size > 0) {
22589
- const originalEdges = edges;
22590
- edges = edges.map(
22591
- (e) => h3Edges.has(e.id) && e.routing !== "back-detour" ? { ...e, routing: "back-detour" } : e
22956
+ const eligibleEdges = new Set(
22957
+ edges.filter((e) => h3Edges.has(e.id) && (e.routing === void 0 || e.routing === "default")).map((e) => e.id)
22592
22958
  );
22593
- if (edges !== originalEdges) {
22959
+ if (eligibleEdges.size > 0) {
22960
+ edges = edges.map(
22961
+ (e) => eligibleEdges.has(e.id) ? { ...e, routing: "back-detour" } : e
22962
+ );
22594
22963
  applied.push("h3-edge-back-detour");
22595
22964
  mutated = true;
22596
22965
  }
@@ -22910,15 +23279,15 @@ function extractPathEndpoints(d) {
22910
23279
  if (!start || !end) return null;
22911
23280
  return { start, end };
22912
23281
  }
22913
- function isPointOnNodeBoundary(point, node, tol) {
23282
+ function isPointOnNodeBoundary(point2, node, tol) {
22914
23283
  const left = node.cx - node.w / 2;
22915
23284
  const right = node.cx + node.w / 2;
22916
23285
  const top = node.cy - node.h / 2;
22917
23286
  const bot = node.cy + node.h / 2;
22918
- if (Math.abs(point.x - left) <= tol && point.y >= top - tol && point.y <= bot + tol) return true;
22919
- if (Math.abs(point.x - right) <= tol && point.y >= top - tol && point.y <= bot + tol) return true;
22920
- if (Math.abs(point.y - top) <= tol && point.x >= left - tol && point.x <= right + tol) return true;
22921
- if (Math.abs(point.y - bot) <= tol && point.x >= left - tol && point.x <= right + tol) return true;
23287
+ if (Math.abs(point2.x - left) <= tol && point2.y >= top - tol && point2.y <= bot + tol) return true;
23288
+ if (Math.abs(point2.x - right) <= tol && point2.y >= top - tol && point2.y <= bot + tol) return true;
23289
+ if (Math.abs(point2.y - top) <= tol && point2.x >= left - tol && point2.x <= right + tol) return true;
23290
+ if (Math.abs(point2.y - bot) <= tol && point2.x >= left - tol && point2.x <= right + tol) return true;
22922
23291
  return false;
22923
23292
  }
22924
23293
  async function verifyBoundingBoxes(page, laid, options) {
@@ -24124,12 +24493,13 @@ function CdlEdgeView({
24124
24493
  const isPassThrough = isDotted && Boolean(edge.dThrough);
24125
24494
  const lineDotsD = isDotted ? shrinkPathEnd(edge.d, 12) : edge.d;
24126
24495
  const glowPathId = `cdl-edge-${edge.id}`;
24127
- const \u901A\u308A\u9053\u3092\u7E2E\u3081\u308B = EDGE_HEAD_MARKER_SIZE.\u65E2\u5B9A + \u8D70\u308B\u4E38\u306E\u534A\u5F84.\u82AF;
24496
+ const \u901A\u308A\u9053\u3092\u7E2E\u3081\u308B = EDGE_HEAD_MARKER_SIZE.\u65E2\u5B9A + \u8D70\u308B\u4E38\u306E\u5916\u5074;
24128
24497
  const glowPathD = shrinkPathStart(shrinkPathEnd(edge.d, \u901A\u308A\u9053\u3092\u7E2E\u3081\u308B), \u901A\u308A\u9053\u3092\u7E2E\u3081\u308B);
24129
24498
  return /* @__PURE__ */ jsxs(
24130
24499
  "g",
24131
24500
  {
24132
24501
  "data-cdl-edge": edge.id,
24502
+ "data-cdl-routing": edge.routing === "metro" ? "metro" : void 0,
24133
24503
  "data-cdl-from": edge.from,
24134
24504
  "data-cdl-to": edge.to,
24135
24505
  "data-cdl-active": active ? "true" : "false",
@@ -24232,7 +24602,9 @@ function SolidEdgePath({
24232
24602
  }
24233
24603
  );
24234
24604
  }
24235
- var \u8D70\u308B\u4E38\u306E\u534A\u5F84 = { \u5916: 28, \u4E2D: 18, \u82AF: 11 };
24605
+ var \u8D70\u308B\u4E38\u306E\u534A\u5F84 = { \u5916: 28, \u4E2D: 18, \u82AF: 12 };
24606
+ var \u8D70\u308B\u4E38\u306E\u7E01\u306E\u898B\u8FBC\u307F = 2;
24607
+ var \u8D70\u308B\u4E38\u306E\u5916\u5074 = \u8D70\u308B\u4E38\u306E\u534A\u5F84.\u82AF + \u8D70\u308B\u4E38\u306E\u7E01\u306E\u898B\u8FBC\u307F;
24236
24608
  function EdgeGlowCircles({ color, edgeId }) {
24237
24609
  const ref = `#cdl-edge-${edgeId}`;
24238
24610
  return /* @__PURE__ */ jsxs("g", { "data-cdl-particle": edgeId, "data-cdl-particle-path-ref": ref, children: [
@@ -25559,17 +25931,22 @@ function DynShapeNode({
25559
25931
  return renderPolygon(shape, cx, cy, stateValues, fillColor, strokeColor, activeOpacity);
25560
25932
  }
25561
25933
  })();
25934
+ const \u63C3\u3048 = \u7BB1\u3044\u3063\u3071\u3044\u306B\u63CF\u304F2(shape.kind) ? "start" : "middle";
25935
+ return renderShapeWithOutsideLabels(\u5F62, node, \u63C3\u3048);
25936
+ }
25937
+ function renderShapeWithOutsideLabels(\u5F62, node, \u63C3\u3048) {
25938
+ const cx = node.cx;
25939
+ const y = node.cy - node.h / 2;
25562
25940
  const \u540D\u524D = (node.title ?? "").trim();
25563
25941
  const \u5024 = (node.subtitle ?? "").trim();
25564
25942
  if (\u540D\u524D === "" && \u5024 === "") return \u5F62;
25565
- const \u63C3\u3048 = \u7BB1\u3044\u3063\u3071\u3044\u306B\u63CF\u304F2(shape.kind) ? "start" : "middle";
25566
25943
  return /* @__PURE__ */ jsxs("g", { children: [
25567
25944
  \u5F62,
25568
25945
  \u540D\u524D !== "" && /* @__PURE__ */ jsx(
25569
25946
  "text",
25570
25947
  {
25571
25948
  "data-cdl-role": "shape-name",
25572
- x: \u63C3\u3048 === "start" ? x : cx,
25949
+ x: \u63C3\u3048 === "start" ? node.cx - node.w / 2 : cx,
25573
25950
  y: y - NAME_GAP,
25574
25951
  textAnchor: \u63C3\u3048,
25575
25952
  fontSize: 13,
@@ -25583,7 +25960,7 @@ function DynShapeNode({
25583
25960
  {
25584
25961
  "data-cdl-role": "shape-value",
25585
25962
  x: cx,
25586
- y: y + h + VALUE_GAP,
25963
+ y: y + node.h + VALUE_GAP,
25587
25964
  textAnchor: "middle",
25588
25965
  fontSize: 12,
25589
25966
  fill: "var(--cdl-text-dim, #5a6270)",
@@ -25833,6 +26210,27 @@ function interpolateColor(template, stateValues, fallback) {
25833
26210
  if (!template) return fallback;
25834
26211
  return interpolate(template, stateValues);
25835
26212
  }
26213
+ var STATION_STROKE_ACTIVE = 4;
26214
+ var STATION_STROKE_IDLE = 3;
26215
+ var STATION_DEFAULT = "var(--cdl-tone-accent, #2d6a8f)";
26216
+ function StationNode({ node, active }) {
26217
+ const strokeWidth = active ? STATION_STROKE_ACTIVE : STATION_STROKE_IDLE;
26218
+ const radius = Math.max(0, Math.min(node.w, node.h) / 2 - strokeWidth / 2);
26219
+ const \u5F62 = /* @__PURE__ */ jsx(
26220
+ "circle",
26221
+ {
26222
+ "data-cdl-role": "node-body",
26223
+ "data-cdl-mark": "station",
26224
+ cx: node.cx,
26225
+ cy: node.cy,
26226
+ r: radius,
26227
+ fill: "var(--cdl-node-fill, #ffffff)",
26228
+ stroke: nodeAccent(node, STATION_DEFAULT),
26229
+ strokeWidth
26230
+ }
26231
+ );
26232
+ return renderShapeWithOutsideLabels(\u5F62, node, "middle");
26233
+ }
25836
26234
 
25837
26235
  // src/kinds/label-stack.ts
25838
26236
  function \u540D\u672D\u306E\u6BB5\u3092\u307B\u3069\u304F(\u771F\u306Ey, \u9593\u9694, \u4E0A\u9650, \u4E0B\u9650) {
@@ -33061,6 +33459,8 @@ function CdlNodeView({
33061
33459
  return /* @__PURE__ */ jsx(MarkStartNode, { node: resolvedNode, active });
33062
33460
  case "mark-end":
33063
33461
  return /* @__PURE__ */ jsx(MarkEndNode, { node: resolvedNode, active });
33462
+ case "station":
33463
+ return /* @__PURE__ */ jsx(StationNode, { node: resolvedNode, active });
33064
33464
  default:
33065
33465
  return /* @__PURE__ */ jsx(GenericNode, { node: resolvedNode, active, value: node.value, stateValues, \u5024\u306E\u540D\u524D });
33066
33466
  }
@@ -33293,15 +33693,15 @@ function CdlStage({
33293
33693
  }, [laid]);
33294
33694
  const edgeHeadMarkers = useMemo(() => {
33295
33695
  const referenced = /* @__PURE__ */ new Set();
33296
- const add = (tone, head, fill) => {
33696
+ const add2 = (tone, head, fill) => {
33297
33697
  for (const small of [false, true]) {
33298
33698
  const id = edgeHeadMarkerName(tone, head, small, fill);
33299
33699
  if (id !== void 0) referenced.add(id);
33300
33700
  }
33301
33701
  };
33302
33702
  for (const edge of laid.edges) {
33303
- add(edge.tone, edge.head, edge.headFill);
33304
- if (edge.tailHead !== void 0) add(edge.tone, edge.tailHead, edge.tailHeadFill);
33703
+ add2(edge.tone, edge.head, edge.headFill);
33704
+ if (edge.tailHead !== void 0) add2(edge.tone, edge.tailHead, edge.tailHeadFill);
33305
33705
  }
33306
33706
  return Object.entries(TONE).flatMap(
33307
33707
  ([tone, color]) => [