@cardenelabs/cdl 0.122.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 (64) hide show
  1. package/CHANGELOG.md +16 -1
  2. package/README.md +64 -0
  3. package/dist/index.cjs +1065 -617
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.d.cts +38 -26
  6. package/dist/index.d.ts +38 -26
  7. package/dist/index.js +1065 -618
  8. package/dist/index.js.map +1 -1
  9. package/dist/react.cjs +834 -403
  10. package/dist/react.cjs.map +1 -1
  11. package/dist/react.d.cts +1 -1
  12. package/dist/react.d.ts +1 -1
  13. package/dist/react.js +834 -403
  14. package/dist/react.js.map +1 -1
  15. package/dist/{render-U_pC-Ksh.d.cts → render-CVczwfRb.d.cts} +20 -2
  16. package/dist/{render-U_pC-Ksh.d.ts → render-CVczwfRb.d.ts} +20 -2
  17. package/package.json +9 -12
  18. package/src/builder.ts +5 -0
  19. package/src/index.ts +2 -0
  20. package/src/kinds/box-lines.ts +17 -4
  21. package/src/kinds/chart-bar.tsx +4 -0
  22. package/src/kinds/chart-gauge.tsx +20 -9
  23. package/src/kinds/chart-pie.tsx +67 -17
  24. package/src/kinds/chart-radial.tsx +7 -2
  25. package/src/kinds/chart-slope.tsx +26 -7
  26. package/src/kinds/chart-stacked-bar.tsx +8 -4
  27. package/src/kinds/chart-stat.tsx +19 -4
  28. package/src/kinds/chart-waffle.tsx +7 -2
  29. package/src/kinds/funnel.tsx +9 -2
  30. package/src/kinds/generic.tsx +5 -2
  31. package/src/kinds/mind-map.tsx +26 -10
  32. package/src/kinds/quadrant.tsx +14 -4
  33. package/src/kinds/row-align.ts +19 -7
  34. package/src/kinds/sequence-board-metrics.ts +13 -3
  35. package/src/kinds/sequence-board.tsx +4 -2
  36. package/src/kinds/shape-basement.tsx +57 -32
  37. package/src/kinds/shape-blockchain.tsx +83 -34
  38. package/src/kinds/shape-commerce.tsx +65 -24
  39. package/src/kinds/shape-finance.tsx +90 -26
  40. package/src/kinds/shape-hardware.tsx +48 -17
  41. package/src/kinds/shape-region.tsx +16 -6
  42. package/src/kinds/shape-software.tsx +30 -8
  43. package/src/kinds/text-width.ts +17 -3
  44. package/src/kinds/tree.tsx +25 -7
  45. package/src/layout/collisions.ts +21 -8
  46. package/src/layout/curve-path.ts +3 -2
  47. package/src/layout/edges.ts +70 -25
  48. package/src/layout/head-label.ts +3 -1
  49. package/src/layout/lanes.ts +3 -2
  50. package/src/layout/metro-path.ts +3 -2
  51. package/src/layout/nodes.ts +14 -6
  52. package/src/layout/predict-bbox.ts +6 -3
  53. package/src/layout/self-loop.ts +10 -4
  54. package/src/layout/spec.ts +20 -6
  55. package/src/layout/text-width.ts +25 -8
  56. package/src/layout.ts +39 -15
  57. package/src/presets.ts +69 -43
  58. package/src/render/edges.tsx +18 -3
  59. package/src/render/interactive-panel.tsx +34 -7
  60. package/src/render/nodes.tsx +97 -48
  61. package/src/render/stage.tsx +3 -0
  62. package/src/text-metrics.ts +9 -0
  63. package/src/types.ts +19 -0
  64. package/src/visual-validate.ts +67 -17
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,
@@ -219,6 +220,7 @@ export function applyRowLevelNodeShifts<
219
220
  * - viewBox は cdl が auto 計算 (右端 +80 / 下端 +80)
220
221
  */
221
222
  export function layout(diag: CdlDiagram): LaidDiagram {
223
+ const textMetrics = resolveTextMetrics(diag.textMetrics);
222
224
  const lanes = layoutLanes(diag);
223
225
  // 下端の名札で絵が箱の上へ出る種別は、 ここで **座標ごと** 下げる (#478)。 以降の全ての
224
226
  // 処理 (lane の伸長 / 衝突の解消 / 矢印の経路 / 外接矩形 / 図の下端) が動かした後の位置を
@@ -226,12 +228,17 @@ export function layout(diag: CdlDiagram): LaidDiagram {
226
228
  //
227
229
  // 描画側で `<g transform>` を掛ける形だった頃は、 箱ごと下へ動いて見えるのに `cy` も
228
230
  // `data-cdl-cy` も外接矩形も元のままで、 座標を読む側が実際の位置を知らなかった。
229
- const rawNodes = applyFooterShapeDrops(lanes, layoutNodes(diag, lanes));
231
+ const rawNodes = applyFooterShapeDrops(lanes, layoutNodes(diag, lanes, textMetrics));
230
232
  const expandedLanes = expandLanesForNodes(lanes, rawNodes);
231
233
  // CAR-470 SSOT ... 隣接 lane 対を跨ぐ edge label の収納幅を engine 側で先手確保。
232
234
  // 拡張後、 後続 lane は cumulative shift で連鎖 shift される。 lane.width は
233
235
  // `expandLanesForNodes` 後の値のまま (node 収納 phase と gap 拡張 phase を分離)。
234
- const gapExpandedLanes = expandLaneGapsForEdgeLabels(expandedLanes, rawNodes, diag.edges);
236
+ const gapExpandedLanes = expandLaneGapsForEdgeLabels(
237
+ expandedLanes,
238
+ rawNodes,
239
+ diag.edges,
240
+ textMetrics,
241
+ );
235
242
  const nodes = rawNodes.map((n) => {
236
243
  // posX / posY を明示した node は「user が固定した絶対座標」 なので lane 中心に引き戻さない。
237
244
  //
@@ -248,16 +255,18 @@ export function layout(diag: CdlDiagram): LaidDiagram {
248
255
  return { ...n, cx: newLane.x + newLane.width / 2 };
249
256
  });
250
257
  const containedLanesPre = recomputeContainedLanes(gapExpandedLanes, nodes);
251
- const edgesPre = layoutEdges(diag, nodes, containedLanesPre);
258
+ const edgesPre = layoutEdges(diag, nodes, containedLanesPre, textMetrics);
252
259
 
253
260
  // 衝突 → 自動再配置 (同 kind=node 同士のみ、 sequence preset の lifeline 等は据え置き)。
254
261
  // node の cy を shift した後は lane の contain bbox と edge routing も再計算が必要。
255
- const bboxesPre = collectBBoxes(containedLanesPre, nodes, edgesPre);
262
+ const bboxesPre = collectBBoxes(containedLanesPre, nodes, edgesPre, textMetrics);
256
263
  const collisionsPre = detectCollisions(bboxesPre, edgesPre);
257
264
  const nodesResolved = resolveOverlaps(nodes, collisionsPre);
258
265
  const moved = nodesResolved !== nodes;
259
266
  const containedLanes = moved ? recomputeContainedLanes(gapExpandedLanes, nodesResolved) : containedLanesPre;
260
- const edgesAfterMove = moved ? layoutEdges(diag, nodesResolved, containedLanes) : edgesPre;
267
+ const edgesAfterMove = moved
268
+ ? layoutEdges(diag, nodesResolved, containedLanes, textMetrics)
269
+ : edgesPre;
261
270
 
262
271
  // post-pass (CAR-430 = SSOT 一本化) ... edges.ts routePath() が返す init 位置を SSOT とし、
263
272
  // label 重複が生じた場合のみ chain 伝搬 shift (resolveEdgeLabelOverlapsWithChain) で解消。
@@ -293,10 +302,13 @@ export function layout(diag: CdlDiagram): LaidDiagram {
293
302
  } else {
294
303
  const propagateResult = resolveEdgeLabelOverlapsWithChainAndPropagate(
295
304
  edgesAfterMove,
296
- collectBBoxes(containedLanes, nodesResolved, []).filter(
305
+ collectBBoxes(containedLanes, nodesResolved, [], textMetrics).filter(
297
306
  (b) => b.kind === "node" || b.kind === "lane-label",
298
307
  ),
299
308
  chainShiftClearance,
309
+ undefined,
310
+ undefined,
311
+ textMetrics,
300
312
  );
301
313
  edges = propagateResult.edges;
302
314
  // node / lane-label chain 伝搬 shift を反映 (CAR-482)。
@@ -335,7 +347,7 @@ export function layout(diag: CdlDiagram): LaidDiagram {
335
347
  // 浮遊)、 label overlap は engine が最終段階で警告可能。 今後 label shift を再度保存する
336
348
  // 必要が生じたら labelX/labelY を propagateResult 側から edgePathShifts と同 map に保存し、
337
349
  // 再実行後の edges に merge する二段階 approach を検討する。
338
- edges = layoutEdges(diag, nodesFinal, containedLanesFinal);
350
+ edges = layoutEdges(diag, nodesFinal, containedLanesFinal, textMetrics);
339
351
  }
340
352
  }
341
353
 
@@ -352,13 +364,13 @@ export function layout(diag: CdlDiagram): LaidDiagram {
352
364
  )
353
365
  .map((e) => e.id),
354
366
  );
355
- bowSharedStraightPaths(edges, nodesFinal, positionedEdgeIds);
367
+ bowSharedStraightPaths(edges, nodesFinal, positionedEdgeIds, textMetrics);
356
368
  // 弧が確定してから、 同じ節の組を結ぶ辺の label を「兄弟の線を跨がない位置」 へ置き直す (#376)。
357
369
  // 順序が逆だと、 直線を前提に置いた label を弧が貫く。 label を先に線から離すと、 今度は弧を
358
370
  // 膨らませる判定が label を障害物と見なして弧が消える。
359
371
  // 同じ辺から出る線は、 最長の 1 本を除いて最初の直線部を 40 world まで引く (#651)。
360
372
  // 経路の引き方を変えず、 確定後の最初の折れだけを動かす。
361
- pullOutgoingFirstCorner(edges, nodesFinal);
373
+ pullOutgoingFirstCorner(edges, nodesFinal, textMetrics);
362
374
  // 同じ辺に入る線の終点を辺に沿って離す (#611)。 経路を引き終えてから当てる = 終点を先に
363
375
  // 動かすと引き方の分岐が変わり、 迂回していた線が箱を貫通する (実測で 0 件が 7 件になった)。
364
376
  const fannedTailShifts = fanIncomingEndpoints(edges, nodesFinal);
@@ -375,25 +387,35 @@ export function layout(diag: CdlDiagram): LaidDiagram {
375
387
  // 先に見ると、 その後に経路が動いて札が線を覆う形が戻る。
376
388
  // 追従と最後の逃がし pass は、 `collectBBoxes` が含める下端の名札の箱上へのはみ出しと
377
389
  // 縦列の題を同じ矩形で測る。 追従だけ箱本体で測ると、 置いた札を最後の pass が押し直す (#1002)。
378
- const labelClearanceObstacles = collectBBoxes(containedLanesFinal, nodesFinal, []).filter(
390
+ const labelClearanceObstacles = collectBBoxes(
391
+ containedLanesFinal,
392
+ nodesFinal,
393
+ [],
394
+ textMetrics,
395
+ ).filter(
379
396
  (b) => b.kind === "node" || b.kind === "lane-label",
380
397
  );
381
- followLabelsToFannedTails(edges, labelClearanceObstacles, fannedTailShifts);
382
- repositionParallelLabels(edges, nodesFinal);
398
+ followLabelsToFannedTails(edges, labelClearanceObstacles, fannedTailShifts, textMetrics);
399
+ repositionParallelLabels(edges, nodesFinal, textMetrics);
383
400
  // 縦にずらすだけでは逃げ場が無い札を、 自分の線に沿って動かす (#601)。 ここまでの
384
401
  // 置き直しは全て縦方向で、 線に沿って動かす経路が無かった。 いま被っている札だけを
385
402
  // 対象にするので、 落ち着いている図の座標は動かない。
386
- slideLabelsAlongOwnPath(edges, nodesFinal);
403
+ slideLabelsAlongOwnPath(edges, nodesFinal, textMetrics);
387
404
  // 経路を動かす pass が全て終わってから、 札を箱と札から離し直す (#659)。
388
405
  //
389
406
  // 上の chain 伝搬はここより前に走るため、 その後の経路 pass が動かした最終経路との隙間を
390
407
  // ここで見直す。
391
408
  // ここで動かすのは札だけで、 箱も経路も触らない = 触ると直前の pass が確定させた形を崩す。
392
409
  if (!isChainEdgeLabelDisabled()) {
393
- edges = resolveLabelClearanceAfterPathPasses(edges, labelClearanceObstacles);
410
+ edges = resolveLabelClearanceAfterPathPasses(
411
+ edges,
412
+ labelClearanceObstacles,
413
+ undefined,
414
+ textMetrics,
415
+ );
394
416
  }
395
417
 
396
- const bboxes = collectBBoxes(containedLanesFinal, nodesFinal, edges);
418
+ const bboxes = collectBBoxes(containedLanesFinal, nodesFinal, edges, textMetrics);
397
419
  const collisions = detectCollisions(bboxes, edges);
398
420
  const nearCollisions = detectNearCollisions(bboxes, edges);
399
421
 
@@ -436,6 +458,8 @@ export function layout(diag: CdlDiagram): LaidDiagram {
436
458
  // 描画側の固定の字の言語 (#847)。 既定 (`en`) と型の外の値は持たせない = 配置の結果の形を
437
459
  // 変更前と揃え、記法の書き損じで一部の字だけ日本語になる形を作らない
438
460
  ...(resolveLang(diag.lang) === "ja" ? { lang: "ja" as const } : {}),
461
+ // 既定は持ち越さない。既存の LaidDiagram の形を変えず、等幅の図だけ描画へ伝える
462
+ ...(textMetrics === "monospace" ? { textMetrics: "monospace" as const } : {}),
439
463
  // 種類も配色と同じく空の目印を出さない。 遮断をここに揃えると手書きの図にも効く
440
464
  ...(diag.type ? { type: diag.type } : {}),
441
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
 
@@ -131,6 +137,7 @@ export function CdlEdgeView({
131
137
  data-cdl-edge-label={edge.label}
132
138
  data-cdl-edge-sub={edge.sub ?? ""}
133
139
  data-cdl-tone={edge.tone}
140
+ data-cdl-edge-role={edge.role}
134
141
  >
135
142
  {isDotted ? (
136
143
  <g>
@@ -349,9 +356,11 @@ export function CdlEdgeGlowView({
349
356
  export function CdlEdgeLabelView({
350
357
  edge,
351
358
  active,
359
+ textMetrics = "proportional",
352
360
  }: {
353
361
  edge: LaidEdge;
354
362
  active: boolean;
363
+ textMetrics?: CdlTextMetrics;
355
364
  }): JSX.Element | null {
356
365
  // routing v9 = 空 label / 空 sub の edge は DOM 描画自体を skip、
357
366
  // collisions.ts 側 (bbox 生成 skip、 CAR-430 で SSOT 統一) と対称に render 層でも noop。
@@ -363,7 +372,7 @@ export function CdlEdgeLabelView({
363
372
  // bbox 計算 drift を解消)。 pill h / w / padding / boxLeftX / boxTopY は本 helper 経由で取得、
364
373
  // <g transform="translate(labelX, labelY)"> 内 rect は相対座標なので world x/y を helper から
365
374
  // 引き算して rect x/y に渡す。
366
- const bbox = computeLabelBBoxWorld(edge);
375
+ const bbox = computeLabelBBoxWorld(edge, textMetrics);
367
376
  const boxW = bbox.w;
368
377
  const boxH = bbox.h;
369
378
  const boxLeftX = bbox.x - edge.labelX;
@@ -383,6 +392,8 @@ export function CdlEdgeLabelView({
383
392
  data-cdl-edge-label-for={edge.id}
384
393
  data-cdl-edge-label-text={edge.label}
385
394
  data-cdl-edge-sub-text={edge.sub ?? ""}
395
+ data-cdl-tone={edge.tone}
396
+ data-cdl-edge-role={edge.role}
386
397
  >
387
398
  {/* 下地は既定で敷く。 丸い下地は箱と同じ形なので、 罫の細い図では外せるようにした (#618) */}
388
399
  {edge.labelPlate !== false && (
@@ -441,12 +452,14 @@ export function CdlEdgeEndLabelView({
441
452
  edge,
442
453
  active,
443
454
  端,
455
+ textMetrics = "proportional",
444
456
  }: {
445
457
  edge: LaidEdge;
446
458
  active: boolean;
447
459
  端: 線の端;
460
+ textMetrics?: CdlTextMetrics;
448
461
  }): JSX.Element | null {
449
- const 置き場所 = 端の字を置く(edge, 端);
462
+ const 置き場所 = 端の字を置く(edge, 端, textMetrics);
450
463
  if (!置き場所) return null;
451
464
  const color = edgeColor(edge);
452
465
  return (
@@ -454,6 +467,8 @@ export function CdlEdgeEndLabelView({
454
467
  data-cdl-role="edge-end-label"
455
468
  data-cdl-edge-end-label-for={edge.id}
456
469
  data-cdl-edge-end={端}
470
+ data-cdl-tone={edge.tone}
471
+ data-cdl-edge-role={edge.role}
457
472
  x={置き場所.x}
458
473
  y={置き場所.y}
459
474
  fontSize={端の字の大きさ}