@cardenelabs/cdl 0.121.0 → 0.123.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.
Files changed (69) hide show
  1. package/CHANGELOG.md +51 -1
  2. package/README.md +137 -0
  3. package/SPEC.md +1 -1
  4. package/dist/index.cjs +1513 -667
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.d.cts +51 -33
  7. package/dist/index.d.ts +51 -33
  8. package/dist/index.js +1513 -668
  9. package/dist/index.js.map +1 -1
  10. package/dist/react.cjs +1271 -444
  11. package/dist/react.cjs.map +1 -1
  12. package/dist/react.d.cts +1 -1
  13. package/dist/react.d.ts +1 -1
  14. package/dist/react.js +1271 -444
  15. package/dist/react.js.map +1 -1
  16. package/dist/{render-A5AKjNuy.d.cts → render-CVczwfRb.d.cts} +40 -7
  17. package/dist/{render-A5AKjNuy.d.ts → render-CVczwfRb.d.ts} +40 -7
  18. package/package.json +9 -12
  19. package/src/builder.ts +8 -2
  20. package/src/index.ts +3 -0
  21. package/src/kinds/box-lines.ts +17 -4
  22. package/src/kinds/chart-bar.tsx +4 -0
  23. package/src/kinds/chart-gauge.tsx +20 -9
  24. package/src/kinds/chart-pie.tsx +67 -17
  25. package/src/kinds/chart-radial.tsx +7 -2
  26. package/src/kinds/chart-slope.tsx +26 -7
  27. package/src/kinds/chart-stacked-bar.tsx +8 -4
  28. package/src/kinds/chart-stat.tsx +19 -4
  29. package/src/kinds/chart-waffle.tsx +7 -2
  30. package/src/kinds/dyn-shape.tsx +22 -9
  31. package/src/kinds/funnel.tsx +9 -2
  32. package/src/kinds/generic.tsx +5 -2
  33. package/src/kinds/mind-map.tsx +26 -10
  34. package/src/kinds/quadrant.tsx +14 -4
  35. package/src/kinds/row-align.ts +19 -7
  36. package/src/kinds/sequence-board-metrics.ts +13 -3
  37. package/src/kinds/sequence-board.tsx +4 -2
  38. package/src/kinds/shape-basement.tsx +57 -32
  39. package/src/kinds/shape-blockchain.tsx +83 -34
  40. package/src/kinds/shape-commerce.tsx +65 -24
  41. package/src/kinds/shape-finance.tsx +90 -26
  42. package/src/kinds/shape-hardware.tsx +48 -17
  43. package/src/kinds/shape-region.tsx +16 -6
  44. package/src/kinds/shape-software.tsx +30 -8
  45. package/src/kinds/station.tsx +29 -0
  46. package/src/kinds/text-width.ts +17 -3
  47. package/src/kinds/tree.tsx +25 -7
  48. package/src/layout/collisions.ts +50 -28
  49. package/src/layout/curve-path.ts +126 -0
  50. package/src/layout/edges.ts +271 -58
  51. package/src/layout/head-label.ts +11 -5
  52. package/src/layout/lanes.ts +3 -2
  53. package/src/layout/metro-path.ts +185 -0
  54. package/src/layout/nodes.ts +14 -6
  55. package/src/layout/predict-bbox.ts +6 -3
  56. package/src/layout/self-loop.ts +10 -4
  57. package/src/layout/spec.ts +36 -7
  58. package/src/layout/text-width.ts +25 -8
  59. package/src/layout/tokens.ts +2 -0
  60. package/src/layout-with-validation.ts +10 -5
  61. package/src/layout.ts +55 -21
  62. package/src/presets.ts +69 -43
  63. package/src/render/edges.tsx +19 -3
  64. package/src/render/interactive-panel.tsx +34 -7
  65. package/src/render/nodes.tsx +100 -48
  66. package/src/render/stage.tsx +3 -0
  67. package/src/text-metrics.ts +9 -0
  68. package/src/types.ts +41 -4
  69. package/src/visual-validate.ts +91 -21
@@ -77,7 +77,14 @@
77
77
  * test が無いと、 次に種別を足した時に同じ状態へ戻る。
78
78
  * ───────────────────────────────────────────────────────────
79
79
  */
80
- import { EDGE_HEADS, type CdlDiagram, type LaidDiagram, type LaidNode, type NodeKind } from "./types";
80
+ import {
81
+ EDGE_HEADS,
82
+ type CdlDiagram,
83
+ type CdlTextMetrics,
84
+ type LaidDiagram,
85
+ type LaidNode,
86
+ type NodeKind,
87
+ } from "./types";
81
88
  import { CLEARANCE_LANE_LABEL, DIST_LABEL_PATH_MAX } from "./layout/clearance-constants";
82
89
  import { layout } from "./layout";
83
90
  import {
@@ -92,7 +99,7 @@ import {
92
99
  } from "./layout/collisions";
93
100
  import { measureTextWidth } from "./layout/text-width";
94
101
  import { droppedLineIsHidden, labelPlan } from "./kinds/shape-region";
95
- import { 箱に積む, type 積む行 } from "./kinds/box-lines";
102
+ import { 箱に積む, 箱の字を測る, type 積む行 } from "./kinds/box-lines";
96
103
  import { 木の中身, 木の行 } from "./kinds/tree";
97
104
  import { 放射の中身, 放射の行 } from "./kinds/mind-map";
98
105
  import { templateRefIds } from "./render/utils";
@@ -143,6 +150,7 @@ import {
143
150
  TITLE_ROW_MIN_GAP,
144
151
  rendersTitleText,
145
152
  fitsOwnTitleWidth,
153
+ skipsNodeTitleWidthCheck,
146
154
  drawsTitleOutsideBox,
147
155
  titleScalesToFit,
148
156
  MIN_TITLE_FONT_SIZE,
@@ -586,7 +594,12 @@ function emptyCounts(): Record<VisualAxis, number> {
586
594
  }
587
595
 
588
596
  /** 箱に行を積む種別の、 中の 1 箱。 */
589
- type 積む箱 = { 行: 積む行[]; w: number; h: number };
597
+ type 積む箱 = {
598
+ 行: 積む行[];
599
+ w: number;
600
+ h: number;
601
+ 測る: (text: string, fontSize: number) => number;
602
+ };
590
603
 
591
604
  /**
592
605
  * 木と放射の中の箱を、 **描く時と同じ手順で** 集める (`#538`)。
@@ -600,10 +613,23 @@ type 積む箱 = { 行: 積む行[]; w: number; h: number };
600
613
  *
601
614
  * 行を積まない種別では空を返す。
602
615
  */
603
- function 積む箱を集める(n: LaidNode): 積む箱[] {
604
- const 箱 = (b: { w: number; h: number }, 行: 積む行[]): 積む箱 => ({ 行, w: b.w, h: b.h });
605
- if (n.kind === "tree-hierarchy") return 木の中身(n).nodes.map((b) => 箱(b, 木の行(b)));
606
- if (n.kind === "mind-map") return (放射の中身(n)?.nodes ?? []).map((b) => 箱(b, 放射の行(b)));
616
+ function 積む箱を集める(
617
+ n: LaidNode,
618
+ textMetrics: CdlTextMetrics = "proportional",
619
+ ): 積む箱[] {
620
+ const 箱 = (
621
+ b: { w: number; h: number },
622
+ 行: 積む行[],
623
+ 測る: 積む箱["測る"],
624
+ ): 積む箱 => ({ 行, w: b.w, h: b.h, 測る });
625
+ if (n.kind === "tree-hierarchy") {
626
+ const 測る = 箱の字を測る(textMetrics);
627
+ return 木の中身(n, {}, textMetrics).nodes.map((b) => 箱(b, 木の行(b), 測る));
628
+ }
629
+ if (n.kind === "mind-map") {
630
+ const 測る = 箱の字を測る(textMetrics);
631
+ return (放射の中身(n, {}, textMetrics)?.nodes ?? []).map((b) => 箱(b, 放射の行(b), 測る));
632
+ }
607
633
  return [];
608
634
  }
609
635
 
@@ -1971,6 +1997,7 @@ function runAxes(
1971
1997
  /** 軸 23 が基準にする器。 入口で確かめ済 (`器を確かめる`)。 高さ `undefined` は上限なし。 */
1972
1998
  器: { 幅: number; 高さ: number | undefined },
1973
1999
  ): VisualValidationReport {
2000
+ const textMetrics = laid.textMetrics ?? "proportional";
1974
2001
  // ───────────────────────────────────────────────────────────
1975
2002
  // Axis 1: node-visibility (bbox 面積 / 最小寸法)
1976
2003
  //
@@ -1980,6 +2007,8 @@ function runAxes(
1980
2007
  const LIFELINE_W_THRESHOLD = 8;
1981
2008
  for (const n of laid.nodes) {
1982
2009
  if (n.w < LIFELINE_W_THRESHOLD) continue;
2010
+ // 駅は文字を収める箱ではなく線路上の印なので、箱向けの最小寸法を要求しない。
2011
+ if (n.kind === "station") continue;
1983
2012
  if (n.w < MIN_NODE_W || n.h < MIN_NODE_H) {
1984
2013
  push(
1985
2014
  "node-visibility",
@@ -1991,7 +2020,7 @@ function runAxes(
1991
2020
  // ───────────────────────────────────────────────────────────
1992
2021
  // Axis 2: edge-label-overlap (edge label ↔ node の AABB collision)
1993
2022
  // ───────────────────────────────────────────────────────────
1994
- const bboxes = collectBBoxes(laid.lanes, laid.nodes, laid.edges);
2023
+ const bboxes = collectBBoxes(laid.lanes, laid.nodes, laid.edges, textMetrics);
1995
2024
  const collisions = detectCollisions(bboxes, laid.edges);
1996
2025
  for (const c of collisions) {
1997
2026
  const involvesEdgeLabel = c.a.kind === "edge-label" || c.b.kind === "edge-label";
@@ -2016,6 +2045,8 @@ function runAxes(
2016
2045
  // 自分で幅に合わせて切る種別も同じ。 字数から幅を出す下の近似は字の大きさ 22 を前提に
2017
2046
  // しており、 それより小さい字で描く種別に当てると偽陽性になる (#469)
2018
2047
  if (fitsOwnTitleWidth(n.kind)) continue;
2048
+ // 箱の外に出す駅名は小さな丸の幅で切らないため、箱幅を上限にする前提が成り立たない。
2049
+ if (skipsNodeTitleWidthCheck(n.kind)) continue;
2019
2050
  // 常に非表示の node の文字は画面に出ないので、 切れようがない。
2020
2051
  if (!isRenderedNode(n)) continue;
2021
2052
  const titleTrimmed = n.title?.trim() ?? "";
@@ -2036,7 +2067,11 @@ function runAxes(
2036
2067
  // 名前と図表の題) は内側の余白に縛られないので、足すと収まっている題まで挙がる。
2037
2068
  const 余白 = drawsTitleOutsideBox(n.kind) ? 0 : TITLE_BOX_PADDING;
2038
2069
  const 使える幅 = n.w - 余白;
2039
- const 実幅 = measureTextWidth(titleTrimmed, { fontSize: titleFontSize(n.kind), bound: "drawn" });
2070
+ const 実幅 = measureTextWidth(titleTrimmed, {
2071
+ fontSize: titleFontSize(n.kind),
2072
+ bound: "drawn",
2073
+ textMetrics,
2074
+ });
2040
2075
  if (titleScalesToFit(n.kind, n.h)) {
2041
2076
  // 収まるまで縮める種別は、題がはみ出すことが起きない。 代わりに字が読めない大きさまで
2042
2077
  // 落ちるので、**読める下限を割るか** を見る (#951)。 下限まで縮めても収まらない時
@@ -2152,7 +2187,15 @@ function runAxes(
2152
2187
  : null;
2153
2188
  const 相手 = 動かす === n.a ? n.b : n.a;
2154
2189
  const reason =
2155
- 動かす !== null ? (札が動けない理由(動かす.id, 相手, laid.edges, 札の障害物) ?? undefined) : undefined;
2190
+ 動かす !== null
2191
+ ? (札が動けない理由(
2192
+ 動かす.id,
2193
+ 相手,
2194
+ laid.edges,
2195
+ 札の障害物,
2196
+ textMetrics,
2197
+ ) ?? undefined)
2198
+ : undefined;
2156
2199
  push(
2157
2200
  "clearance",
2158
2201
  `${n.a.kind}:${n.a.id} ↔ ${n.b.kind}:${n.b.id} gap=${n.gap.toFixed(1)}px (need ${n.required}px)`,
@@ -2189,7 +2232,10 @@ function runAxes(
2189
2232
  if (!hasRenderedLabel(e)) continue;
2190
2233
  const segs = extractPathSegments(e.d);
2191
2234
  if (segs.length === 0) continue;
2192
- const minDist = measurePathLabelGapSegs(flattenPathSegments(segs), computeLabelBBoxWorld(e));
2235
+ const minDist = measurePathLabelGapSegs(
2236
+ flattenPathSegments(segs),
2237
+ computeLabelBBoxWorld(e, textMetrics),
2238
+ );
2193
2239
  if (!Number.isFinite(minDist)) continue;
2194
2240
  if (minDist > PROXIMITY_ERROR_THRESHOLD) {
2195
2241
  push(
@@ -2259,9 +2305,16 @@ function runAxes(
2259
2305
  // ───────────────────────────────────────────────────────────
2260
2306
  for (const e of laid.edges) {
2261
2307
  if (!hasRenderedLabel(e)) continue;
2262
- const mainW = measureTextWidth(e.label ?? "", { fontSize: EDGE_LABEL_TEXT.main.fontSize });
2308
+ const mainW = measureTextWidth(e.label ?? "", {
2309
+ fontSize: EDGE_LABEL_TEXT.main.fontSize,
2310
+ textMetrics,
2311
+ });
2263
2312
  const subW = e.sub
2264
- ? measureTextWidth(e.sub, { fontSize: EDGE_LABEL_TEXT.sub.fontSize, fontFamily: "mono" })
2313
+ ? measureTextWidth(e.sub, {
2314
+ fontSize: EDGE_LABEL_TEXT.sub.fontSize,
2315
+ fontFamily: "mono",
2316
+ textMetrics,
2317
+ })
2265
2318
  : 0;
2266
2319
  const expectedBoxW = Math.max(mainW, subW) + LABEL_PILL_PAD_X * 2;
2267
2320
  const bbox = bboxes.find((b) => b.kind === "edge-label" && b.id === e.id);
@@ -2494,7 +2547,9 @@ function runAxes(
2494
2547
  // 共有 plan が行を落とさない高さでは、 固有 layout がその行を描かない種別でも報告されない。
2495
2548
  // 本軸は箱が狭くて落ちた行だけを扱い、 種別が元から対応しない欄は別の検査範囲とする。
2496
2549
  for (const n of laid.nodes) {
2497
- const 落とした = labelPlan(n).落とした.filter((line) => droppedLineIsHidden(n.kind, line));
2550
+ const 落とした = labelPlan(n).落とした.filter((line) =>
2551
+ droppedLineIsHidden(n.kind, line),
2552
+ );
2498
2553
  if (落とした.length === 0) continue;
2499
2554
  push(
2500
2555
  "shape-label-dropped",
@@ -2526,7 +2581,7 @@ function runAxes(
2526
2581
  // なので、 対象から外すと変えた時に落ちても見えない。
2527
2582
  // ───────────────────────────────────────────────────────────
2528
2583
  for (const n of laid.nodes) {
2529
- const 箱 = 積む箱を集める(n);
2584
+ const 箱 = 積む箱を集める(n, textMetrics);
2530
2585
  if (箱.length === 0) continue;
2531
2586
  /** 補足を書いた箱の数。 書いていない箱は元から描かれないので数えない。 */
2532
2587
  let 対象 = 0;
@@ -2536,7 +2591,7 @@ function runAxes(
2536
2591
  const 宣言 = b.行.slice(1).filter((r) => r.text.trim() !== "");
2537
2592
  if (宣言.length === 0) continue;
2538
2593
  対象 += 1;
2539
- const 残った = new Set(箱に積む(b.行, b.w, b.h).map((r) => r.役割));
2594
+ const 残った = new Set(箱に積む(b.行, b.w, b.h, b.測る).map((r) => r.役割));
2540
2595
  if (宣言.every((r) => 残った.has(r.役割))) continue;
2541
2596
  落ちた.push({ title: b.行[0]?.text ?? "", h: b.h });
2542
2597
  }
@@ -3111,6 +3166,13 @@ function runAxes(
3111
3166
  for (let i = 0; i + 1 < sorted.length; i++) {
3112
3167
  const upper = sorted[i]!;
3113
3168
  const lower = sorted[i + 1]!;
3169
+ // 横の範囲が重ならない箱は同じ lane に横並びで置かれた組なので、縦の隙間を測らない。
3170
+ if (
3171
+ upper.cx + upper.w / 2 <= lower.cx - lower.w / 2 ||
3172
+ lower.cx + lower.w / 2 <= upper.cx - upper.w / 2
3173
+ ) {
3174
+ continue;
3175
+ }
3114
3176
  const upperBottom = upper.cy + upper.h / 2;
3115
3177
  const lowerTop = lower.cy - lower.h / 2;
3116
3178
  const gap = lowerTop - upperBottom;
@@ -4214,17 +4276,25 @@ function runAxes(
4214
4276
  y1: number;
4215
4277
  x2: number;
4216
4278
  y2: number;
4217
- cmd: "L" | "Q" | "C";
4279
+ cmd: "L" | "Q" | "C" | "A";
4218
4280
  ctrl?: ReadonlyArray<{ x: number; y: number }>;
4219
4281
  }>,
4220
4282
  peakY: number,
4221
4283
  ): Array<{ minX: number; maxX: number }> => {
4222
4284
  const out: Array<{ minX: number; maxX: number }> = [];
4285
+ let 円弧の始点: { x: number; y: number } | null = null;
4223
4286
  for (const s of segs) {
4224
- if (Math.abs(s.y1 - s.y2) > 1) continue;
4225
- if (Math.abs(s.y1 - peakY) > 1) continue;
4287
+ if (s.cmd === "A") {
4288
+ // 飛び越しの弧は同じ高さの通り道の上の小さな山で、山の前後は 1 本の通り道になる。
4289
+ 円弧の始点 ??= { x: s.x1, y: s.y1 };
4290
+ continue;
4291
+ }
4292
+ const 始点 = 円弧の始点 ?? { x: s.x1, y: s.y1 };
4293
+ 円弧の始点 = null;
4294
+ if (Math.abs(始点.y - s.y2) > 1) continue;
4295
+ if (Math.abs(始点.y - peakY) > 1) continue;
4226
4296
  if (s.cmd !== "L" && !(s.ctrl ?? []).every((c) => Math.abs(c.y - peakY) <= 1)) continue;
4227
- out.push({ minX: Math.min(s.x1, s.x2), maxX: Math.max(s.x1, s.x2) });
4297
+ out.push({ minX: Math.min(始点.x, s.x2), maxX: Math.max(始点.x, s.x2) });
4228
4298
  }
4229
4299
  return out;
4230
4300
  };
@@ -4382,7 +4452,7 @@ function runAxes(
4382
4452
  const allowedLanes = new Set<string>();
4383
4453
  if (fromNode) allowedLanes.add(fromNode.lane);
4384
4454
  if (toNode) allowedLanes.add(toNode.lane);
4385
- const box = computeLabelBBoxWorld(e);
4455
+ const box = computeLabelBBoxWorld(e, textMetrics);
4386
4456
  for (const lane of laid.lanes) {
4387
4457
  if (allowedLanes.has(lane.id)) continue;
4388
4458
  const side = rectCrossesLaneBorder(box, lane, rangeByLane.get(lane.id));