@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
package/src/layout.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { CdlDiagram, LaidDiagram } from "./types";
2
2
  import { resolveLang } from "./lang";
3
+ import { resolveTextMetrics } from "./text-metrics";
3
4
  import {
4
5
  CLEARANCE_LABEL_LABEL,
5
6
  CLEARANCE_NODE_LABEL,
@@ -15,6 +16,7 @@ import {
15
16
  import {
16
17
  bowSharedStraightPaths,
17
18
  fanIncomingEndpoints,
19
+ followLabelsToFannedTails,
18
20
  hopEdgeCrossings,
19
21
  layoutEdges,
20
22
  pullOutgoingFirstCorner,
@@ -212,11 +214,13 @@ export function applyRowLevelNodeShifts<
212
214
  * - node は lane 内 stack 順で縦並び、 各 stack 間 gap 80px、 上端 80px
213
215
  * - lane label は lane 上部 30px 高
214
216
  * - contain: true の lane は内部 node 全部を 24px padding で囲む boundary
215
- * - edge routing は orthogonal L 字 (起点 side → 中継 → 終点 side)、 左右 lane 跨ぎは S 字 bezier
217
+ * - edge routing は orthogonal L 字 (起点 side → 中継 → 終点 side)。`curve` は 3 次曲線、
218
+ * `metro` は横線と傾き ±1 の斜線を円弧で結ぶ
216
219
  * - edge label は path 中点から path 進行方向の垂直方向に 50-80px、 node bbox を避ける
217
220
  * - viewBox は cdl が auto 計算 (右端 +80 / 下端 +80)
218
221
  */
219
222
  export function layout(diag: CdlDiagram): LaidDiagram {
223
+ const textMetrics = resolveTextMetrics(diag.textMetrics);
220
224
  const lanes = layoutLanes(diag);
221
225
  // 下端の名札で絵が箱の上へ出る種別は、 ここで **座標ごと** 下げる (#478)。 以降の全ての
222
226
  // 処理 (lane の伸長 / 衝突の解消 / 矢印の経路 / 外接矩形 / 図の下端) が動かした後の位置を
@@ -224,12 +228,17 @@ export function layout(diag: CdlDiagram): LaidDiagram {
224
228
  //
225
229
  // 描画側で `<g transform>` を掛ける形だった頃は、 箱ごと下へ動いて見えるのに `cy` も
226
230
  // `data-cdl-cy` も外接矩形も元のままで、 座標を読む側が実際の位置を知らなかった。
227
- const rawNodes = applyFooterShapeDrops(lanes, layoutNodes(diag, lanes));
231
+ const rawNodes = applyFooterShapeDrops(lanes, layoutNodes(diag, lanes, textMetrics));
228
232
  const expandedLanes = expandLanesForNodes(lanes, rawNodes);
229
233
  // CAR-470 SSOT ... 隣接 lane 対を跨ぐ edge label の収納幅を engine 側で先手確保。
230
234
  // 拡張後、 後続 lane は cumulative shift で連鎖 shift される。 lane.width は
231
235
  // `expandLanesForNodes` 後の値のまま (node 収納 phase と gap 拡張 phase を分離)。
232
- const gapExpandedLanes = expandLaneGapsForEdgeLabels(expandedLanes, rawNodes, diag.edges);
236
+ const gapExpandedLanes = expandLaneGapsForEdgeLabels(
237
+ expandedLanes,
238
+ rawNodes,
239
+ diag.edges,
240
+ textMetrics,
241
+ );
233
242
  const nodes = rawNodes.map((n) => {
234
243
  // posX / posY を明示した node は「user が固定した絶対座標」 なので lane 中心に引き戻さない。
235
244
  //
@@ -246,16 +255,18 @@ export function layout(diag: CdlDiagram): LaidDiagram {
246
255
  return { ...n, cx: newLane.x + newLane.width / 2 };
247
256
  });
248
257
  const containedLanesPre = recomputeContainedLanes(gapExpandedLanes, nodes);
249
- const edgesPre = layoutEdges(diag, nodes, containedLanesPre);
258
+ const edgesPre = layoutEdges(diag, nodes, containedLanesPre, textMetrics);
250
259
 
251
260
  // 衝突 → 自動再配置 (同 kind=node 同士のみ、 sequence preset の lifeline 等は据え置き)。
252
261
  // node の cy を shift した後は lane の contain bbox と edge routing も再計算が必要。
253
- const bboxesPre = collectBBoxes(containedLanesPre, nodes, edgesPre);
262
+ const bboxesPre = collectBBoxes(containedLanesPre, nodes, edgesPre, textMetrics);
254
263
  const collisionsPre = detectCollisions(bboxesPre, edgesPre);
255
264
  const nodesResolved = resolveOverlaps(nodes, collisionsPre);
256
265
  const moved = nodesResolved !== nodes;
257
266
  const containedLanes = moved ? recomputeContainedLanes(gapExpandedLanes, nodesResolved) : containedLanesPre;
258
- const edgesAfterMove = moved ? layoutEdges(diag, nodesResolved, containedLanes) : edgesPre;
267
+ const edgesAfterMove = moved
268
+ ? layoutEdges(diag, nodesResolved, containedLanes, textMetrics)
269
+ : edgesPre;
259
270
 
260
271
  // post-pass (CAR-430 = SSOT 一本化) ... edges.ts routePath() が返す init 位置を SSOT とし、
261
272
  // label 重複が生じた場合のみ chain 伝搬 shift (resolveEdgeLabelOverlapsWithChain) で解消。
@@ -291,10 +302,13 @@ export function layout(diag: CdlDiagram): LaidDiagram {
291
302
  } else {
292
303
  const propagateResult = resolveEdgeLabelOverlapsWithChainAndPropagate(
293
304
  edgesAfterMove,
294
- collectBBoxes(containedLanes, nodesResolved, []).filter(
305
+ collectBBoxes(containedLanes, nodesResolved, [], textMetrics).filter(
295
306
  (b) => b.kind === "node" || b.kind === "lane-label",
296
307
  ),
297
308
  chainShiftClearance,
309
+ undefined,
310
+ undefined,
311
+ textMetrics,
298
312
  );
299
313
  edges = propagateResult.edges;
300
314
  // node / lane-label chain 伝搬 shift を反映 (CAR-482)。
@@ -333,28 +347,33 @@ export function layout(diag: CdlDiagram): LaidDiagram {
333
347
  // 浮遊)、 label overlap は engine が最終段階で警告可能。 今後 label shift を再度保存する
334
348
  // 必要が生じたら labelX/labelY を propagateResult 側から edgePathShifts と同 map に保存し、
335
349
  // 再実行後の edges に merge する二段階 approach を検討する。
336
- edges = layoutEdges(diag, nodesFinal, containedLanesFinal);
350
+ edges = layoutEdges(diag, nodesFinal, containedLanesFinal, textMetrics);
337
351
  }
338
352
  }
339
353
 
340
354
  // 同じ 2 点を結ぶ複数の線を弓なりに分ける (dragon #941)。 label と path が確定した後に動かす =
341
355
  // 確定前に判定すると、 後から動く label の位置を見て分離を諦める。 座標を明示した edge
342
- // (canvas pivot) は「指定した直線が最終 path」 が公開契約なので対象から外す。
356
+ // (canvas pivot)、`curve`、`metro` は「指定した形が最終 path」 が公開契約なので対象から外す。
343
357
  const positionedEdgeIds = new Set(
344
358
  diag.edges
345
- .filter((e) => e.posX1 !== undefined && e.posY1 !== undefined && e.posX2 !== undefined && e.posY2 !== undefined)
359
+ .filter(
360
+ (e) =>
361
+ e.routing === "curve" ||
362
+ e.routing === "metro" ||
363
+ (e.posX1 !== undefined && e.posY1 !== undefined && e.posX2 !== undefined && e.posY2 !== undefined),
364
+ )
346
365
  .map((e) => e.id),
347
366
  );
348
- bowSharedStraightPaths(edges, nodesFinal, positionedEdgeIds);
367
+ bowSharedStraightPaths(edges, nodesFinal, positionedEdgeIds, textMetrics);
349
368
  // 弧が確定してから、 同じ節の組を結ぶ辺の label を「兄弟の線を跨がない位置」 へ置き直す (#376)。
350
369
  // 順序が逆だと、 直線を前提に置いた label を弧が貫く。 label を先に線から離すと、 今度は弧を
351
370
  // 膨らませる判定が label を障害物と見なして弧が消える。
352
371
  // 同じ辺から出る線は、 最長の 1 本を除いて最初の直線部を 40 world まで引く (#651)。
353
372
  // 経路の引き方を変えず、 確定後の最初の折れだけを動かす。
354
- pullOutgoingFirstCorner(edges, nodesFinal);
373
+ pullOutgoingFirstCorner(edges, nodesFinal, textMetrics);
355
374
  // 同じ辺に入る線の終点を辺に沿って離す (#611)。 経路を引き終えてから当てる = 終点を先に
356
375
  // 動かすと引き方の分岐が変わり、 迂回していた線が箱を貫通する (実測で 0 件が 7 件になった)。
357
- fanIncomingEndpoints(edges, nodesFinal);
376
+ const fannedTailShifts = fanIncomingEndpoints(edges, nodesFinal);
358
377
  // 同じ通り道を並んで走る区間をずらす (#610)。 弓の後に当てる = 弓で分かれた線を数え直さない。
359
378
  // 弧より先に当てる = ずらした後の形で交点を数えないと、 消えた交点に弧が残る。
360
379
  //
@@ -364,26 +383,39 @@ export function layout(diag: CdlDiagram): LaidDiagram {
364
383
  // 交わる所で上を通る側に弧を差し込む (#608)。 経路が確定してから当てる = 弓や迂回で
365
384
  // 形が変わった後の交点を見る。 札を置く前に当てるので、 札は弧を含んだ経路の上に載る。
366
385
  hopEdgeCrossings(edges);
367
- repositionParallelLabels(edges, nodesFinal);
386
+ // 終点を離す / 共有区間を分ける / 交差へ弧を入れる pass が全て終わった座標で札を見る (#1000)。
387
+ // 先に見ると、 その後に経路が動いて札が線を覆う形が戻る。
388
+ // 追従と最後の逃がし pass は、 `collectBBoxes` が含める下端の名札の箱上へのはみ出しと
389
+ // 縦列の題を同じ矩形で測る。 追従だけ箱本体で測ると、 置いた札を最後の pass が押し直す (#1002)。
390
+ const labelClearanceObstacles = collectBBoxes(
391
+ containedLanesFinal,
392
+ nodesFinal,
393
+ [],
394
+ textMetrics,
395
+ ).filter(
396
+ (b) => b.kind === "node" || b.kind === "lane-label",
397
+ );
398
+ followLabelsToFannedTails(edges, labelClearanceObstacles, fannedTailShifts, textMetrics);
399
+ repositionParallelLabels(edges, nodesFinal, textMetrics);
368
400
  // 縦にずらすだけでは逃げ場が無い札を、 自分の線に沿って動かす (#601)。 ここまでの
369
401
  // 置き直しは全て縦方向で、 線に沿って動かす経路が無かった。 いま被っている札だけを
370
402
  // 対象にするので、 落ち着いている図の座標は動かない。
371
- slideLabelsAlongOwnPath(edges, nodesFinal);
403
+ slideLabelsAlongOwnPath(edges, nodesFinal, textMetrics);
372
404
  // 経路を動かす pass が全て終わってから、 札を箱と札から離し直す (#659)。
373
405
  //
374
- // 上の chain 伝搬はここより前に走るため、 その後で `pullOutgoingFirstCorner` /
375
- // `fanIncomingEndpoints` が経路を動かすと札も一緒に動く。 動いた後を見る者が居なかった。
406
+ // 上の chain 伝搬はここより前に走るため、 その後の経路 pass が動かした最終経路との隙間を
407
+ // ここで見直す。
376
408
  // ここで動かすのは札だけで、 箱も経路も触らない = 触ると直前の pass が確定させた形を崩す。
377
409
  if (!isChainEdgeLabelDisabled()) {
378
410
  edges = resolveLabelClearanceAfterPathPasses(
379
411
  edges,
380
- collectBBoxes(containedLanesFinal, nodesFinal, []).filter(
381
- (b) => b.kind === "node" || b.kind === "lane-label",
382
- ),
412
+ labelClearanceObstacles,
413
+ undefined,
414
+ textMetrics,
383
415
  );
384
416
  }
385
417
 
386
- const bboxes = collectBBoxes(containedLanesFinal, nodesFinal, edges);
418
+ const bboxes = collectBBoxes(containedLanesFinal, nodesFinal, edges, textMetrics);
387
419
  const collisions = detectCollisions(bboxes, edges);
388
420
  const nearCollisions = detectNearCollisions(bboxes, edges);
389
421
 
@@ -426,6 +458,8 @@ export function layout(diag: CdlDiagram): LaidDiagram {
426
458
  // 描画側の固定の字の言語 (#847)。 既定 (`en`) と型の外の値は持たせない = 配置の結果の形を
427
459
  // 変更前と揃え、記法の書き損じで一部の字だけ日本語になる形を作らない
428
460
  ...(resolveLang(diag.lang) === "ja" ? { lang: "ja" as const } : {}),
461
+ // 既定は持ち越さない。既存の LaidDiagram の形を変えず、等幅の図だけ描画へ伝える
462
+ ...(textMetrics === "monospace" ? { textMetrics: "monospace" as const } : {}),
429
463
  // 種類も配色と同じく空の目印を出さない。 遮断をここに揃えると手書きの図にも効く
430
464
  ...(diag.type ? { type: diag.type } : {}),
431
465
  bboxes,
package/src/presets.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { diagram } from "./builder";
2
2
  import type { DiagramBuilder } from "./builder";
3
+ import { resolveTextMetrics } from "./text-metrics";
3
4
  import { LANE_PAD_MIN, NODE_SIZE } from "./layout/tokens";
4
5
  import { requiredRowsWidth } from "./kinds/row-align";
5
6
  import { 木の札の幅 } from "./kinds/tree";
@@ -12,7 +13,12 @@ import {
12
13
  type SequenceBoardMessage,
13
14
  type SequenceBoardMessageKind,
14
15
  } from "./kinds/sequence-board-metrics";
15
- import type { NodeKind, Tone, EdgeStyle, EdgeHead, EdgeHeadFill, RowMark, JourneyEmotion, QuadrantKey, BoundNumber, EdgeRole } from "./types";
16
+ import type { CdlTextMetrics, NodeKind, Tone, EdgeStyle, EdgeHead, EdgeHeadFill, RowMark, JourneyEmotion, QuadrantKey, BoundNumber, EdgeRole } from "./types";
17
+
18
+ type TextMetricsPreset = {
19
+ /** 図の字幅を測る規則。省略時は `"proportional"` */
20
+ textMetrics?: CdlTextMetrics;
21
+ };
16
22
 
17
23
  /**
18
24
  * 状態を読む書き方 (`{名前}`)。
@@ -175,10 +181,14 @@ function resolveStorageBoxW(width: number | undefined, presetName: string, optio
175
181
  * 指定値は下限として扱う。 「指定した幅より狭くならない」 は守り、 中身が入らない時だけ広げる。
176
182
  * 例外にしない理由は、 著者が行の実寸を事前に計算できないため (font の送り幅に依存する)。
177
183
  */
178
- function fitRowsWidth(boxW: number, rows: readonly string[] | undefined): number {
184
+ function fitRowsWidth(
185
+ boxW: number,
186
+ rows: readonly string[] | undefined,
187
+ textMetrics: CdlTextMetrics = "proportional",
188
+ ): number {
179
189
  // 呼出は `er` の実体と `classDiagram` のクラスの 2 箇所で、 どちらも `kind: "storage"` を作る。
180
190
  // 送り幅と列間は種別で違うため、 どの種別として測るかを渡す
181
- return Math.max(boxW, requiredRowsWidth(rows ? [...rows] : undefined, "storage"));
191
+ return Math.max(boxW, requiredRowsWidth(rows ? [...rows] : undefined, "storage", textMetrics));
182
192
  }
183
193
 
184
194
  /**
@@ -190,7 +200,7 @@ function fitRowsWidth(boxW: number, rows: readonly string[] | undefined): number
190
200
 
191
201
  // ─── swimlane ──────────────────────────────────────────────────
192
202
 
193
- export type SwimlanePreset = {
203
+ export type SwimlanePreset = TextMetricsPreset & {
194
204
  id: string;
195
205
  topic: string;
196
206
  /** lane label の配列。 各 lane width は固定 (default 400)、 x は auto-layout */
@@ -218,7 +228,7 @@ export type SwimlaneResult = DiagramBuilder & {
218
228
 
219
229
  export function swimlane(preset: SwimlanePreset): SwimlaneResult {
220
230
  const w = preset.laneWidth ?? 400;
221
- const b = diagram(preset.id, { topic: preset.topic, type: "swimlane" });
231
+ const b = diagram(preset.id, { topic: preset.topic, textMetrics: preset.textMetrics, type: "swimlane" });
222
232
  const labelToId = new Map<string, string>();
223
233
  const idsInOrder: string[] = [];
224
234
  for (const label of preset.lanes) {
@@ -247,7 +257,7 @@ export type FlowStepInput = {
247
257
  subtitle?: string;
248
258
  };
249
259
 
250
- export type FlowPreset = {
260
+ export type FlowPreset = TextMetricsPreset & {
251
261
  id: string;
252
262
  topic: string;
253
263
  /** lane label (1 本のみ、 全 step が縦 stack で並ぶ) */
@@ -298,7 +308,7 @@ export type FlowBuilder = {
298
308
  */
299
309
  export function flow(preset: FlowPreset): FlowBuilder {
300
310
  const w = preset.laneWidth ?? 400;
301
- const b = diagram(preset.id, { topic: preset.topic, type: "flow" });
311
+ const b = diagram(preset.id, { topic: preset.topic, textMetrics: preset.textMetrics, type: "flow" });
302
312
  b.lane("flow", { width: w, ...(preset.laneLabel ? { label: preset.laneLabel } : {}) });
303
313
  const tone: Tone = preset.defaultTone ?? "accent";
304
314
  const style: EdgeStyle = preset.defaultStyle ?? "dotted-flow";
@@ -410,7 +420,7 @@ export type SequenceActor = {
410
420
  subtitle?: string;
411
421
  };
412
422
 
413
- export type SequencePreset = {
423
+ export type SequencePreset = TextMetricsPreset & {
414
424
  id: string;
415
425
  topic: string;
416
426
  /** actor label の配列 (lifeline、 column 単位)。 呼び名を添えるなら物で書く (#578) */
@@ -452,7 +462,7 @@ export type SequenceBuilder = {
452
462
  */
453
463
  export function sequence(preset: SequencePreset): SequenceBuilder {
454
464
  const tone: Tone = preset.defaultTone ?? "accent";
455
- const b = diagram(preset.id, { topic: preset.topic, type: "sequence" });
465
+ const b = diagram(preset.id, { topic: preset.topic, textMetrics: preset.textMetrics, type: "sequence" });
456
466
 
457
467
  // 名前だけの文字列と、呼び名を添えた物の両方を受ける (#578)
458
468
  const 面々: SequenceBoardActor[] = preset.actors.map((a) =>
@@ -501,7 +511,7 @@ export function sequence(preset: SequencePreset): SequenceBuilder {
501
511
  .filter((x): x is SequenceBoardBand => x !== null);
502
512
 
503
513
  const data: SequenceBoardData = { actors: 面々, messages: 言づて, bands: 帯 };
504
- const m = sequenceBoardMetrics(data);
514
+ const m = sequenceBoardMetrics(data, resolveTextMetrics(preset.textMetrics));
505
515
  const laneId = "seq";
506
516
  b.lane(laneId, { width: m.w + LANE_PAD_MIN * 2 });
507
517
  b.node(preset.id, {
@@ -548,7 +558,7 @@ export type TopologyConnection = {
548
558
  labelOffsetY?: number;
549
559
  };
550
560
 
551
- export type TopologyPreset = {
561
+ export type TopologyPreset = TextMetricsPreset & {
552
562
  id: string;
553
563
  topic: string;
554
564
  /** group の lane width (default 360) */
@@ -592,7 +602,7 @@ export function topology(preset: TopologyPreset): TopologyBuilder {
592
602
  const groupW = preset.groupWidth ?? 460;
593
603
  const tone: Tone = preset.defaultTone ?? "accent";
594
604
  const style: EdgeStyle = preset.defaultStyle ?? "solid";
595
- const b = diagram(preset.id, { topic: preset.topic, type: "topology" });
605
+ const b = diagram(preset.id, { topic: preset.topic, textMetrics: preset.textMetrics, type: "topology" });
596
606
 
597
607
  const groupOrder: string[] = [];
598
608
  const groupStackCount = new Map<string, number>();
@@ -752,7 +762,7 @@ export type ErRelation = {
752
762
  style?: EdgeStyle;
753
763
  };
754
764
 
755
- export type ErPreset = {
765
+ export type ErPreset = TextMetricsPreset & {
756
766
  id: string;
757
767
  topic: string;
758
768
  defaultTone?: Tone;
@@ -804,7 +814,12 @@ export function er(preset: ErPreset): ErBuilder {
804
814
  const tone: Tone = preset.defaultTone ?? "info";
805
815
  const 縞を敷く = preset.rowStripe ?? true;
806
816
  // 配色の名前は素通しする。 空文字の遮断は `diagram` が持つ = ここで重ねると到達しない分岐になる
807
- const b = diagram(preset.id, { topic: preset.topic, palette: preset.palette, type: "er" });
817
+ const b = diagram(preset.id, {
818
+ topic: preset.topic,
819
+ palette: preset.palette,
820
+ textMetrics: preset.textMetrics,
821
+ type: "er",
822
+ });
808
823
 
809
824
  const entities: string[] = [];
810
825
  const phaseActivate: string[] = [];
@@ -834,7 +849,7 @@ export function er(preset: ErPreset): ErBuilder {
834
849
  const rows = 設計の形 ? 行 : (e.rows ?? []);
835
850
  // 帯は実体ごとに決める。 行の長さで箱が広がる実体があるため、 全実体を同じ帯幅で
836
851
  // 宣言すると宣言座標と実座標がずれる。
837
- const boxW = fitRowsWidth(entityW, rows);
852
+ const boxW = fitRowsWidth(entityW, rows, resolveTextMetrics(preset.textMetrics));
838
853
  b.lane(laneId, { width: boxLaneW(boxW) });
839
854
  b.node(e.id, {
840
855
  lane: laneId,
@@ -986,7 +1001,7 @@ export type FsmTransition = {
986
1001
  head?: EdgeHead;
987
1002
  };
988
1003
 
989
- export type StateMachinePreset = {
1004
+ export type StateMachinePreset = TextMetricsPreset & {
990
1005
  id: string;
991
1006
  topic: string;
992
1007
  defaultTone?: Tone;
@@ -1033,7 +1048,7 @@ export function stateMachine(preset: StateMachinePreset): StateMachineBuilder {
1033
1048
  const tone: Tone = preset.defaultTone ?? "accent";
1034
1049
  // 図の型と最後の段の札は同じ綴り (#859)。 札を別の字にすると、消費側が型の一覧で引けない
1035
1050
  const diagramType = "state";
1036
- const b = diagram(preset.id, { topic: preset.topic, type: diagramType });
1051
+ const b = diagram(preset.id, { topic: preset.topic, textMetrics: preset.textMetrics, type: diagramType });
1037
1052
 
1038
1053
  const phaseActivate: string[] = [];
1039
1054
  const stateOrder = new Map<string, number>();
@@ -1078,7 +1093,9 @@ export function stateMachine(preset: StateMachinePreset): StateMachineBuilder {
1078
1093
  const 行 = (s.actions ?? []).map((a) => (a.note ? `${a.label}: ${a.note}` : a.label));
1079
1094
  const 印 = (s.actions ?? []).map((a) => FSM_ACTION_MARK[a.when]);
1080
1095
  const laneId = 格子 && s.col !== undefined ? `col-${s.col}` : `lane-${s.id}`;
1081
- const boxW = 設計の形 ? fitRowsWidth(stateW, 行) : stateW;
1096
+ const boxW = 設計の形
1097
+ ? fitRowsWidth(stateW, 行, resolveTextMetrics(preset.textMetrics))
1098
+ : stateW;
1082
1099
  return { s, 行, 印, laneId, boxW, stack: 格子 ? (s.row ?? 0) : 0 };
1083
1100
  });
1084
1101
  const 節点の中身 = 節点.map((m) => ({
@@ -1201,7 +1218,7 @@ export type InfraConnection = {
1201
1218
  labelOffsetY?: number;
1202
1219
  };
1203
1220
 
1204
- export type InfrastructurePreset = {
1221
+ export type InfrastructurePreset = TextMetricsPreset & {
1205
1222
  id: string;
1206
1223
  topic: string;
1207
1224
  /** col あたりの lane width (default 380) */
@@ -1239,7 +1256,11 @@ export function infrastructure(preset: InfrastructurePreset): InfrastructureBuil
1239
1256
  const laneW = preset.laneWidth ?? 380;
1240
1257
  const tone: Tone = preset.defaultTone ?? "accent";
1241
1258
  const style: EdgeStyle = preset.defaultStyle ?? "solid";
1242
- const b = diagram(preset.id, { topic: preset.topic, type: "infrastructure" });
1259
+ const b = diagram(preset.id, {
1260
+ topic: preset.topic,
1261
+ textMetrics: preset.textMetrics,
1262
+ type: "infrastructure",
1263
+ });
1243
1264
 
1244
1265
  // col → laneId、 col の数で lane を auto 作成
1245
1266
  const colLaneIds = new Map<number, string>();
@@ -1452,7 +1473,7 @@ export type UmlRelation = {
1452
1473
  style?: EdgeStyle;
1453
1474
  };
1454
1475
 
1455
- export type ClassDiagramPreset = {
1476
+ export type ClassDiagramPreset = TextMetricsPreset & {
1456
1477
  id: string;
1457
1478
  topic: string;
1458
1479
  defaultTone?: Tone;
@@ -1503,7 +1524,12 @@ export function classDiagram(preset: ClassDiagramPreset): ClassDiagramBuilder {
1503
1524
  const 図の既定 = preset.defaultTone;
1504
1525
  // 縞と配色は `er()` と同じ扱い (#627)。 箱の作りが同じなので、既定も揃える
1505
1526
  const 縞を敷く = preset.rowStripe ?? true;
1506
- const b = diagram(preset.id, { topic: preset.topic, palette: preset.palette, type: "class" });
1527
+ const b = diagram(preset.id, {
1528
+ topic: preset.topic,
1529
+ palette: preset.palette,
1530
+ textMetrics: preset.textMetrics,
1531
+ type: "class",
1532
+ });
1507
1533
 
1508
1534
  const classes: string[] = [];
1509
1535
  const phaseActivate: string[] = [];
@@ -1537,7 +1563,7 @@ export function classDiagram(preset: ClassDiagramPreset): ClassDiagramBuilder {
1537
1563
  }
1538
1564
  // 帯はクラスごとに決める。 行の長さで箱が広がるクラスがあるため、 全クラスを同じ帯幅で
1539
1565
  // 宣言すると宣言座標と実座標がずれる。
1540
- const boxW = fitRowsWidth(classW, rows);
1566
+ const boxW = fitRowsWidth(classW, rows, resolveTextMetrics(preset.textMetrics));
1541
1567
  // 格子に置くかは `col` を書いたかで決める (#578)。 書かない図は従来どおり横 1 列
1542
1568
  const laneId = c.col !== undefined ? `col-${c.col}` : `lane-${c.id}`;
1543
1569
  return { c, rows, rowMarks, boxW, laneId, stack: c.col !== undefined ? (c.row ?? 0) : 0 };
@@ -1619,7 +1645,7 @@ export type TreeNode = {
1619
1645
  subtitle?: string;
1620
1646
  };
1621
1647
 
1622
- export type TreePreset = {
1648
+ export type TreePreset = TextMetricsPreset & {
1623
1649
  id: string;
1624
1650
  topic: string;
1625
1651
  /** 図の題の上に出す小見出し (#939)。 書かなければ何も出ない */
@@ -1649,7 +1675,7 @@ export type TreeBuilder = {
1649
1675
  */
1650
1676
  export function tree(preset: TreePreset): TreeBuilder {
1651
1677
  const canvasH = 480;
1652
- const b = diagram(preset.id, { topic: preset.topic, type: "tree" });
1678
+ const b = diagram(preset.id, { topic: preset.topic, textMetrics: preset.textMetrics, type: "tree" });
1653
1679
  const treeNodes: TreeNode[] = [];
1654
1680
 
1655
1681
  const api: TreeBuilder = {
@@ -1708,7 +1734,7 @@ export type JourneyStep = {
1708
1734
  opportunity?: string;
1709
1735
  };
1710
1736
 
1711
- export type UserJourneyPreset = {
1737
+ export type UserJourneyPreset = TextMetricsPreset & {
1712
1738
  id: string;
1713
1739
  topic: string;
1714
1740
  /** 図の題の上に出す小見出し (#939)。 書かなければ何も出ない */
@@ -1737,7 +1763,7 @@ export type UserJourneyBuilder = {
1737
1763
  export function userJourney(preset: UserJourneyPreset): UserJourneyBuilder {
1738
1764
  const canvasW = preset.stepWidth ? preset.stepWidth * 2 : 720;
1739
1765
  const canvasH = 480;
1740
- const b = diagram(preset.id, { topic: preset.topic, type: "journey" });
1766
+ const b = diagram(preset.id, { topic: preset.topic, textMetrics: preset.textMetrics, type: "journey" });
1741
1767
  const steps: JourneyStep[] = [];
1742
1768
 
1743
1769
  const api: UserJourneyBuilder = {
@@ -1788,7 +1814,7 @@ export type MindBranch = {
1788
1814
  subtitle?: string;
1789
1815
  };
1790
1816
 
1791
- export type MindMapPreset = {
1817
+ export type MindMapPreset = TextMetricsPreset & {
1792
1818
  id: string;
1793
1819
  topic: string;
1794
1820
  /** 図の題の上に出す小見出し (#939)。 書かなければ何も出ない */
@@ -1825,7 +1851,7 @@ export function mindMap(preset: MindMapPreset): MindMapBuilder {
1825
1851
  // 型は他の組み立て関数と同じく短い名前 (`state` / `class` / `journey` と並ぶ `mind`)。
1826
1852
  // 最後の段の札も同じ綴り (#859)
1827
1853
  const diagramType = "mind";
1828
- const b = diagram(preset.id, { topic: preset.topic, type: diagramType });
1854
+ const b = diagram(preset.id, { topic: preset.topic, textMetrics: preset.textMetrics, type: diagramType });
1829
1855
  const branches: MindBranch[] = [];
1830
1856
 
1831
1857
  const api: MindMapBuilder = {
@@ -1880,7 +1906,7 @@ export type FunnelStage = {
1880
1906
  subtitle?: string;
1881
1907
  };
1882
1908
 
1883
- export type FunnelPreset = {
1909
+ export type FunnelPreset = TextMetricsPreset & {
1884
1910
  id: string;
1885
1911
  topic: string;
1886
1912
  /** 図の題の上に出す小見出し (#939)。 書かなければ何も出ない */
@@ -1909,7 +1935,7 @@ export type FunnelBuilder = {
1909
1935
  export function funnel(preset: FunnelPreset): FunnelBuilder {
1910
1936
  const canvasW = toGridSize(preset.stageWidth ?? 560);
1911
1937
  const canvasH = 480;
1912
- const b = diagram(preset.id, { topic: preset.topic, type: "funnel" });
1938
+ const b = diagram(preset.id, { topic: preset.topic, textMetrics: preset.textMetrics, type: "funnel" });
1913
1939
  const stages: FunnelStage[] = [];
1914
1940
 
1915
1941
  const api: FunnelBuilder = {
@@ -1961,7 +1987,7 @@ export type QuadrantItem = {
1961
1987
  subtitle?: string;
1962
1988
  };
1963
1989
 
1964
- export type QuadrantPreset = {
1990
+ export type QuadrantPreset = TextMetricsPreset & {
1965
1991
  id: string;
1966
1992
  topic: string;
1967
1993
  /** 図の題の上に出す小見出し (#939)。 書かなければ何も出ない */
@@ -1998,7 +2024,7 @@ export type QuadrantBuilder = {
1998
2024
  export function quadrant(preset: QuadrantPreset): QuadrantBuilder {
1999
2025
  const canvasW = 640;
2000
2026
  const canvasH = 480;
2001
- const b = diagram(preset.id, { topic: preset.topic, type: "quadrant" });
2027
+ const b = diagram(preset.id, { topic: preset.topic, textMetrics: preset.textMetrics, type: "quadrant" });
2002
2028
  const items: QuadrantItem[] = [];
2003
2029
 
2004
2030
  const api: QuadrantBuilder = {
@@ -2071,7 +2097,7 @@ export type ChartDatum = {
2071
2097
  tone?: Tone;
2072
2098
  };
2073
2099
 
2074
- export type ChartPreset = {
2100
+ export type ChartPreset = TextMetricsPreset & {
2075
2101
  id: string;
2076
2102
  topic: string;
2077
2103
  /** 図の題の上に出す小見出し (#939)。 書かなければ何も出ない */
@@ -2115,7 +2141,7 @@ export function chart(preset: ChartPreset): ChartBuilder {
2115
2141
  ? 320
2116
2142
  : 360,
2117
2143
  );
2118
- const b = diagram(preset.id, { topic: preset.topic, type: "chart" });
2144
+ const b = diagram(preset.id, { topic: preset.topic, textMetrics: preset.textMetrics, type: "chart" });
2119
2145
  const data: ChartDatum[] = [];
2120
2146
 
2121
2147
  const kindMap = {
@@ -2186,7 +2212,7 @@ export type GanttTask = {
2186
2212
  dependsOn?: string;
2187
2213
  };
2188
2214
 
2189
- export type GanttPreset = {
2215
+ export type GanttPreset = TextMetricsPreset & {
2190
2216
  id: string;
2191
2217
  topic: string;
2192
2218
  /** 図の題の上に出す小見出し (#939)。 書かなければ何も出ない */
@@ -2229,7 +2255,7 @@ export function gantt(preset: GanttPreset): GanttBuilder {
2229
2255
  const canvasW = preset.laneWidth ? preset.laneWidth * 2 : 720;
2230
2256
  const canvasH = 360;
2231
2257
  const tone: Tone = preset.defaultTone ?? "teal";
2232
- const b = diagram(preset.id, { topic: preset.topic, type: "gantt" });
2258
+ const b = diagram(preset.id, { topic: preset.topic, textMetrics: preset.textMetrics, type: "gantt" });
2233
2259
  const tasks: GanttTask[] = [];
2234
2260
 
2235
2261
  const api: GanttBuilder = {
@@ -2319,7 +2345,7 @@ export type FlowchartEdge = {
2319
2345
  */
2320
2346
  export type FlowchartDirection = "horizontal" | "vertical";
2321
2347
 
2322
- export type FlowchartPreset = {
2348
+ export type FlowchartPreset = TextMetricsPreset & {
2323
2349
  id: string;
2324
2350
  topic: string;
2325
2351
  /** swimlane の lane (role) ラベル */
@@ -2378,7 +2404,7 @@ export function flowchart(preset: FlowchartPreset): FlowchartBuilder {
2378
2404
  const laneW = preset.laneWidth ?? 380;
2379
2405
  const tone: Tone = preset.defaultTone ?? "accent";
2380
2406
  const 縦に積む = (preset.direction ?? "horizontal") === "vertical";
2381
- const b = diagram(preset.id, { topic: preset.topic, type: "flowchart" });
2407
+ const b = diagram(preset.id, { topic: preset.topic, textMetrics: preset.textMetrics, type: "flowchart" });
2382
2408
 
2383
2409
  const laneToId = new Map<string, string>();
2384
2410
  const laneStackCount = new Map<string, number>();
@@ -2488,7 +2514,7 @@ export type NetworkLink = {
2488
2514
  tone?: Tone;
2489
2515
  };
2490
2516
 
2491
- export type NetworkPreset = {
2517
+ export type NetworkPreset = TextMetricsPreset & {
2492
2518
  id: string;
2493
2519
  topic: string;
2494
2520
  defaultTone?: Tone;
@@ -2534,7 +2560,7 @@ const NET_KIND_LABEL: Record<NetworkDeviceKind, string> = {
2534
2560
  export function network(preset: NetworkPreset): NetworkBuilder {
2535
2561
  const laneW = preset.laneWidth ?? 320;
2536
2562
  const tone: Tone = preset.defaultTone ?? "info";
2537
- const b = diagram(preset.id, { topic: preset.topic, type: "network" });
2563
+ const b = diagram(preset.id, { topic: preset.topic, textMetrics: preset.textMetrics, type: "network" });
2538
2564
 
2539
2565
  const colLaneIds = new Map<number, string>();
2540
2566
  const phaseActivate: string[] = [];
@@ -2609,7 +2635,7 @@ export type FsmTransitionExt = {
2609
2635
  tone?: Tone;
2610
2636
  };
2611
2637
 
2612
- export type StateMachine2Preset = {
2638
+ export type StateMachine2Preset = TextMetricsPreset & {
2613
2639
  id: string;
2614
2640
  topic: string;
2615
2641
  defaultTone?: Tone;
@@ -2653,7 +2679,7 @@ export function stateMachine2(preset: StateMachine2Preset): StateMachine2Builder
2653
2679
  const tone: Tone = preset.defaultTone ?? "accent";
2654
2680
  // 入れ子を持つ状態の図も型は `state`。 最後の段の札も同じ綴りで、1 つ目の図との見分けは題が持つ (#859)
2655
2681
  const diagramType = "state";
2656
- const b = diagram(preset.id, { topic: preset.topic, type: diagramType });
2682
+ const b = diagram(preset.id, { topic: preset.topic, textMetrics: preset.textMetrics, type: diagramType });
2657
2683
 
2658
2684
  const stateOrder = new Map<string, number>();
2659
2685
  const phaseActivate: string[] = [];
@@ -2,7 +2,13 @@ import { 等幅の字 } from "./mono-text";
2
2
  import type { JSX } from "react";
3
3
  import { computeLabelBBoxWorld, hasRenderedLabel, LABEL_PILL_PAD_X } from "../layout/spec";
4
4
  import { 端の字の大きさ, 端の字を置く, type 線の端 } from "../layout/head-label";
5
- import { EDGE_HEAD_MARKER_SIZE, type EdgeRole, type LaidEdge, type Tone } from "../types";
5
+ import {
6
+ EDGE_HEAD_MARKER_SIZE,
7
+ type CdlTextMetrics,
8
+ type EdgeRole,
9
+ type LaidEdge,
10
+ type Tone,
11
+ } from "../types";
6
12
  import { edgeHeadMarkerName } from "./edge-head";
7
13
  import { toneColor } from "./tone";
8
14
 
@@ -119,6 +125,7 @@ export function CdlEdgeView({
119
125
  return (
120
126
  <g
121
127
  data-cdl-edge={edge.id}
128
+ data-cdl-routing={edge.routing === "metro" ? "metro" : undefined}
122
129
  data-cdl-from={edge.from}
123
130
  data-cdl-to={edge.to}
124
131
  data-cdl-active={active ? "true" : "false"}
@@ -130,6 +137,7 @@ export function CdlEdgeView({
130
137
  data-cdl-edge-label={edge.label}
131
138
  data-cdl-edge-sub={edge.sub ?? ""}
132
139
  data-cdl-tone={edge.tone}
140
+ data-cdl-edge-role={edge.role}
133
141
  >
134
142
  {isDotted ? (
135
143
  <g>
@@ -348,9 +356,11 @@ export function CdlEdgeGlowView({
348
356
  export function CdlEdgeLabelView({
349
357
  edge,
350
358
  active,
359
+ textMetrics = "proportional",
351
360
  }: {
352
361
  edge: LaidEdge;
353
362
  active: boolean;
363
+ textMetrics?: CdlTextMetrics;
354
364
  }): JSX.Element | null {
355
365
  // routing v9 = 空 label / 空 sub の edge は DOM 描画自体を skip、
356
366
  // collisions.ts 側 (bbox 生成 skip、 CAR-430 で SSOT 統一) と対称に render 層でも noop。
@@ -362,7 +372,7 @@ export function CdlEdgeLabelView({
362
372
  // bbox 計算 drift を解消)。 pill h / w / padding / boxLeftX / boxTopY は本 helper 経由で取得、
363
373
  // <g transform="translate(labelX, labelY)"> 内 rect は相対座標なので world x/y を helper から
364
374
  // 引き算して rect x/y に渡す。
365
- const bbox = computeLabelBBoxWorld(edge);
375
+ const bbox = computeLabelBBoxWorld(edge, textMetrics);
366
376
  const boxW = bbox.w;
367
377
  const boxH = bbox.h;
368
378
  const boxLeftX = bbox.x - edge.labelX;
@@ -382,6 +392,8 @@ export function CdlEdgeLabelView({
382
392
  data-cdl-edge-label-for={edge.id}
383
393
  data-cdl-edge-label-text={edge.label}
384
394
  data-cdl-edge-sub-text={edge.sub ?? ""}
395
+ data-cdl-tone={edge.tone}
396
+ data-cdl-edge-role={edge.role}
385
397
  >
386
398
  {/* 下地は既定で敷く。 丸い下地は箱と同じ形なので、 罫の細い図では外せるようにした (#618) */}
387
399
  {edge.labelPlate !== false && (
@@ -440,12 +452,14 @@ export function CdlEdgeEndLabelView({
440
452
  edge,
441
453
  active,
442
454
  端,
455
+ textMetrics = "proportional",
443
456
  }: {
444
457
  edge: LaidEdge;
445
458
  active: boolean;
446
459
  端: 線の端;
460
+ textMetrics?: CdlTextMetrics;
447
461
  }): JSX.Element | null {
448
- const 置き場所 = 端の字を置く(edge, 端);
462
+ const 置き場所 = 端の字を置く(edge, 端, textMetrics);
449
463
  if (!置き場所) return null;
450
464
  const color = edgeColor(edge);
451
465
  return (
@@ -453,6 +467,8 @@ export function CdlEdgeEndLabelView({
453
467
  data-cdl-role="edge-end-label"
454
468
  data-cdl-edge-end-label-for={edge.id}
455
469
  data-cdl-edge-end={端}
470
+ data-cdl-tone={edge.tone}
471
+ data-cdl-edge-role={edge.role}
456
472
  x={置き場所.x}
457
473
  y={置き場所.y}
458
474
  fontSize={端の字の大きさ}