@cardenelabs/cdl 0.122.0 → 0.124.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 +28 -1
  2. package/README.md +70 -1
  3. package/dist/index.cjs +1125 -618
  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 +1125 -619
  8. package/dist/index.js.map +1 -1
  9. package/dist/react.cjs +894 -404
  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 +894 -404
  14. package/dist/react.js.map +1 -1
  15. package/dist/{render-U_pC-Ksh.d.cts → render-U6omSNH2.d.cts} +23 -4
  16. package/dist/{render-U_pC-Ksh.d.ts → render-U6omSNH2.d.ts} +23 -4
  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 +173 -29
  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 +22 -2
  64. package/src/visual-validate.ts +67 -17
@@ -1,5 +1,5 @@
1
1
  import { type JSX } from "react";
2
- import type { ChartSlopeForm, LaidNode, Tone } from "../types";
2
+ import type { CdlTextMetrics, ChartSlopeForm, LaidNode, Tone } from "../types";
3
3
  import { TONE, chartSeriesColor } from "../render/tone";
4
4
  import { resolveChartData } from "../render/payload-binding";
5
5
  import { textWidth } from "./text-width";
@@ -99,6 +99,7 @@ export function ChartSlopeNode({
99
99
  stateValues = {},
100
100
  drawing = false,
101
101
  progress = 1,
102
+ textMetrics = "proportional",
102
103
  }: {
103
104
  node: LaidNode;
104
105
  active: boolean;
@@ -106,6 +107,7 @@ export function ChartSlopeNode({
106
107
  /** 段の `draw` にこの node が載っている間だけ true */
107
108
  drawing?: boolean;
108
109
  progress?: number;
110
+ textMetrics?: CdlTextMetrics;
109
111
  }): JSX.Element {
110
112
  const x0 = node.cx - node.w / 2;
111
113
  const y0 = node.cy - node.h / 2;
@@ -193,8 +195,8 @@ export function ChartSlopeNode({
193
195
  const 右の字 = 系列たち.map((s) =>
194
196
  形 === "delta" ? 増減表記(s.前, s.今) : 実値表記(s.今),
195
197
  );
196
- const 左値幅 = Math.max(0, ...左の字.map((t) => textWidth(t, 値の級)));
197
- const 右値幅 = Math.max(0, ...右の字.map((t) => textWidth(t, 値の級)));
198
+ const 左値幅 = Math.max(0, ...左の字.map((t) => textWidth(t, 値の級, 0, textMetrics)));
199
+ const 右値幅 = Math.max(0, ...右の字.map((t) => textWidth(t, 値の級, 0, textMetrics)));
198
200
 
199
201
  /** 名札に回せる横幅。 線の幅を先に取り分けてから、残りを左右へ半分ずつ配る */
200
202
  const 名前に使える = Math.max(
@@ -206,10 +208,16 @@ export function ChartSlopeNode({
206
208
  (右値幅 + 軸から値まで + 値から名前まで)) /
207
209
  2,
208
210
  );
209
- const 名前の級 = 収める級(系列たち.map((s) => s.名前), 名前に使える, 名前の上限級, 名前の下限級);
211
+ const 名前の級 = 収める級(
212
+ 系列たち.map((s) => s.名前),
213
+ 名前に使える,
214
+ 名前の上限級,
215
+ 名前の下限級,
216
+ textMetrics,
217
+ );
210
218
  const 名前幅 = Math.min(
211
219
  名前に使える,
212
- Math.max(0, ...系列たち.map((s) => textWidth(s.名前, 名前の級))),
220
+ Math.max(0, ...系列たち.map((s) => textWidth(s.名前, 名前の級, 0, textMetrics))),
213
221
  );
214
222
 
215
223
  const 左の余白 = 名前幅 + 値から名前まで + 左値幅 + 軸から値まで;
@@ -427,9 +435,20 @@ function 印の色(tone: Tone | undefined, idx: number): string {
427
435
  * 最長の名前が枠に収まるまで 1 ずつ下げ、下限で止める。 下限でも収まらない図では
428
436
  * 名札が枠から出るが、線と軸は箱の内側に残る。
429
437
  */
430
- function 収める級(名前たち: readonly string[], 幅: number, 上限: number, 下限: number): number {
438
+ function 収める級(
439
+ 名前たち: readonly string[],
440
+ 幅: number,
441
+ 上限: number,
442
+ 下限: number,
443
+ textMetrics: CdlTextMetrics = "proportional",
444
+ ): number {
431
445
  if (名前たち.length === 0) return 上限;
432
446
  let 級 = 上限;
433
- while (級 > 下限 && Math.max(...名前たち.map((n) => textWidth(n, 級))) > 幅) 級 -= 1;
447
+ while (
448
+ 級 > 下限 &&
449
+ Math.max(...名前たち.map((n) => textWidth(n, 級, 0, textMetrics))) > 幅
450
+ ) {
451
+ 級 -= 1;
452
+ }
434
453
  return 級;
435
454
  }
@@ -1,5 +1,5 @@
1
1
  import { useId, type JSX } from "react";
2
- import type { LaidNode, Tone } from "../types";
2
+ import type { CdlTextMetrics, LaidNode, Tone } from "../types";
3
3
  import { TONE, chartSeriesColor } from "../render/tone";
4
4
  import { resolveChartData, type ResolvedChartDatum } from "../render/payload-binding";
5
5
  import { textWidth } from "./text-width";
@@ -51,6 +51,7 @@ export function ChartStackedBarNode({
51
51
  stateValues = {},
52
52
  drawing = false,
53
53
  progress = 1,
54
+ textMetrics = "proportional",
54
55
  }: {
55
56
  node: LaidNode;
56
57
  active: boolean;
@@ -59,6 +60,7 @@ export function ChartStackedBarNode({
59
60
  drawing?: boolean;
60
61
  /** phase の進み (0→1)。 `drawing` が true の時だけ読む */
61
62
  progress?: number;
63
+ textMetrics?: CdlTextMetrics;
62
64
  }): JSX.Element {
63
65
  const x0 = node.cx - node.w / 2;
64
66
  const y0 = node.cy - node.h / 2;
@@ -72,7 +74,8 @@ export function ChartStackedBarNode({
72
74
  const 時点の級 = 13;
73
75
  /** 時点の名前を置く左の幅 */
74
76
  const 時点の幅 =
75
- Math.max(0, ...帯たち.map((b) => textWidth(b.名前, 時点の級))) + (帯たち.length > 1 ? 16 : 0);
77
+ Math.max(0, ...帯たち.map((b) => textWidth(b.名前, 時点の級, 0, textMetrics))) +
78
+ (帯たち.length > 1 ? 16 : 0);
76
79
  const 帯の左 = PAD + 時点の幅;
77
80
  const 帯の幅 = Math.max(40, node.w - 帯の左 - PAD);
78
81
  const 帯の高さ = 44;
@@ -196,7 +199,7 @@ export function ChartStackedBarNode({
196
199
  })}
197
200
 
198
201
  {/* 一覧。 区画に字が入らなかった件もここで名前と色が引ける */}
199
- {一覧の並び(data, 帯の左, node.w - PAD, 名前の級).map(({ d, idx, x, 行 }) => {
202
+ {一覧の並び(data, 帯の左, node.w - PAD, 名前の級, textMetrics).map(({ d, idx, x, 行 }) => {
200
203
  const y =
201
204
  上端 +
202
205
  帯たち.length * (帯の高さ + 帯の間) -
@@ -284,12 +287,13 @@ export function 一覧の並び(
284
287
  左: number,
285
288
  右端: number,
286
289
  級: number,
290
+ textMetrics: CdlTextMetrics = "proportional",
287
291
  ): Array<{ d: ResolvedChartDatum; idx: number; x: number; 行: number }> {
288
292
  const out: Array<{ d: ResolvedChartDatum; idx: number; x: number; 行: number }> = [];
289
293
  let x = 左;
290
294
  let 行 = 0;
291
295
  data.forEach((d, idx) => {
292
- const 幅 = 印から名前まで + textWidth(d.label ?? "", 級);
296
+ const 幅 = 印から名前まで + textWidth(d.label ?? "", 級, 0, textMetrics);
293
297
  // 行の先頭では折り返さない = 折り返しても同じ位置に戻るだけで、空の行が増える
294
298
  if (x > 左 && x + 幅 > 右端) {
295
299
  行 += 1;
@@ -1,5 +1,5 @@
1
1
  import { type JSX } from "react";
2
- import type { LaidNode, Tone } from "../types";
2
+ import type { CdlTextMetrics, LaidNode, Tone } from "../types";
3
3
  import { TONE, chartSeriesColor } from "../render/tone";
4
4
  import { resolveChartData } from "../render/payload-binding";
5
5
  import { textWidth } from "./text-width";
@@ -35,10 +35,12 @@ export function ChartStatNode({
35
35
  node,
36
36
  active,
37
37
  stateValues = {},
38
+ textMetrics = "proportional",
38
39
  }: {
39
40
  node: LaidNode;
40
41
  active: boolean;
41
42
  stateValues?: Record<string, string>;
43
+ textMetrics?: CdlTextMetrics;
42
44
  }): JSX.Element {
43
45
  const x0 = node.cx - node.w / 2;
44
46
  const y0 = node.cy - node.h / 2;
@@ -87,7 +89,13 @@ export function ChartStatNode({
87
89
  const 色 = 印の色(d.tone, idx);
88
90
  const 数値 = 実値表記(d.value);
89
91
  // 数値は区画に収まるまで小さくする。 収まらないまま描くと隣の区画へはみ出す
90
- const 数値の級 = 収める級(数値, 枠.w - 区画.余白 * 2, 区画.数値の級, 区画.名前の級 + 1);
92
+ const 数値の級 = 収める級(
93
+ 数値,
94
+ 枠.w - 区画.余白 * 2,
95
+ 区画.数値の級,
96
+ 区画.名前の級 + 1,
97
+ textMetrics,
98
+ );
91
99
  return (
92
100
  <g
93
101
  key={`stat-${idx}`}
@@ -147,6 +155,7 @@ export function ChartStatNode({
147
155
  枠.w - 区画.余白 * 2,
148
156
  区画.前の級,
149
157
  8,
158
+ textMetrics,
150
159
  )}
151
160
  textAnchor="middle"
152
161
  fill="var(--cdl-text-dim, #5a6270)"
@@ -291,9 +300,15 @@ function 区画の割付(
291
300
  * 中身 (数値が主役) が崩れるため。 下限でも入らない場合は入らないまま描く = 勝手に字を
292
301
  * 落とすと数値が別の値に読める。
293
302
  */
294
- export function 収める級(字: string, 使える幅: number, 初期: number, 下限: number): number {
303
+ export function 収める級(
304
+ 字: string,
305
+ 使える幅: number,
306
+ 初期: number,
307
+ 下限: number,
308
+ textMetrics: CdlTextMetrics = "proportional",
309
+ ): number {
295
310
  let 級 = 初期;
296
- while (級 > 下限 && textWidth(字, 級) > 使える幅) 級 -= 1;
311
+ while (級 > 下限 && textWidth(字, 級, 0, textMetrics) > 使える幅) 級 -= 1;
297
312
  return 級;
298
313
  }
299
314
 
@@ -1,5 +1,5 @@
1
1
  import { type JSX } from "react";
2
- import type { LaidNode, Tone } from "../types";
2
+ import type { CdlTextMetrics, LaidNode, Tone } from "../types";
3
3
  import { TONE, chartSeriesColor } from "../render/tone";
4
4
  import { resolveChartData } from "../render/payload-binding";
5
5
  import { textWidth } from "./text-width";
@@ -84,6 +84,7 @@ export function ChartWaffleNode({
84
84
  stateValues = {},
85
85
  drawing = false,
86
86
  progress = 1,
87
+ textMetrics = "proportional",
87
88
  }: {
88
89
  node: LaidNode;
89
90
  active: boolean;
@@ -92,6 +93,7 @@ export function ChartWaffleNode({
92
93
  drawing?: boolean;
93
94
  /** phase の進み (0→1)。 `drawing` が true の時だけ読む */
94
95
  progress?: number;
96
+ textMetrics?: CdlTextMetrics;
95
97
  }): JSX.Element {
96
98
  const x0 = node.cx - node.w / 2;
97
99
  const y0 = node.cy - node.h / 2;
@@ -116,7 +118,10 @@ export function ChartWaffleNode({
116
118
  Math.max(
117
119
  0,
118
120
  ...data.map((d, i) =>
119
- Math.max(textWidth(d.label ?? "", 名前の級), textWidth(数値の文字[i] ?? "", 数値の級)),
121
+ Math.max(
122
+ textWidth(d.label ?? "", 名前の級, 0, textMetrics),
123
+ textWidth(数値の文字[i] ?? "", 数値の級, 0, textMetrics),
124
+ ),
120
125
  ),
121
126
  );
122
127
 
@@ -1,5 +1,5 @@
1
1
  import { useId, type JSX } from "react";
2
- import type { LaidNode, Tone } from "../types";
2
+ import type { CdlTextMetrics, LaidNode, Tone } from "../types";
3
3
  import { TONE } from "../render/tone";
4
4
  import { resolveFunnelData } from "../render/payload-binding";
5
5
  import { 収める倍率 } from "../layout/text-width";
@@ -18,6 +18,7 @@ export function FunnelNode({
18
18
  stateValues = {},
19
19
  drawing = false,
20
20
  progress = 1,
21
+ textMetrics = "proportional",
21
22
  }: {
22
23
  node: LaidNode;
23
24
  active: boolean;
@@ -26,6 +27,7 @@ export function FunnelNode({
26
27
  drawing?: boolean;
27
28
  /** phase の進み (0→1)。 `drawing` が true の時だけ読む */
28
29
  progress?: number;
30
+ textMetrics?: CdlTextMetrics;
29
31
  }): JSX.Element {
30
32
  const x0 = node.cx - node.w / 2;
31
33
  const y0 = node.cy - node.h / 2;
@@ -109,7 +111,12 @@ export function FunnelNode({
109
111
  const 補足の枠字 = 補足の字 * 縦倍;
110
112
  const 収まる幅 = Math.max(0, bottomW - 16);
111
113
  const 補足の実字 =
112
- 補足の枠字 * 収める倍率(補足, { fontSize: 補足の枠字, fontWeight: 500 }, 収まる幅);
114
+ 補足の枠字 *
115
+ 収める倍率(
116
+ 補足,
117
+ { fontSize: 補足の枠字, fontWeight: 500, textMetrics },
118
+ 収まる幅,
119
+ );
113
120
  // 2 行の塊を段の中央に積む。 縦の位置は枠の字 (縮める前) で決める = 幅で縮めた分だけ
114
121
  // 行が上下に動くと、段ごとに行の高さが揃わない
115
122
  const 塊の上 = y + stageH / 2 - (名前の実字 * 1.2 + 補足の枠字 * 1.25) / 2;
@@ -1,6 +1,6 @@
1
1
  import { 等幅の字 } from "../render/mono-text";
2
2
  import type { JSX } from "react";
3
- import type { LaidNode, NodeKind } from "../types";
3
+ import type { CdlTextMetrics, LaidNode, NodeKind } from "../types";
4
4
  import { toneColor } from "./node-tone";
5
5
  import { interpolate } from "../render/utils";
6
6
  import type { 値の名前の表 } from "../input-option";
@@ -237,6 +237,7 @@ export function GenericNode({
237
237
  value,
238
238
  stateValues,
239
239
  値の名前,
240
+ textMetrics = "proportional",
240
241
  }: {
241
242
  node: LaidNode;
242
243
  active: boolean;
@@ -244,6 +245,7 @@ export function GenericNode({
244
245
  stateValues: Record<string, string>;
245
246
  /** 入力欄の値ごとの名前 (#855)。 値と行の差し込みに使う */
246
247
  値の名前?: 値の名前の表;
248
+ textMetrics?: CdlTextMetrics;
247
249
  }): JSX.Element {
248
250
  const base = STYLES[node.kind] ?? FALLBACK;
249
251
  // `tone` 指定があれば kind の既定色を置き換える。 色を決める式は `node-tone.ts` に集約。
@@ -458,7 +460,8 @@ export function GenericNode({
458
460
  // 左列は比例の書体で描く (`font-family` を指定していないので継承する)。 字数に一定を
459
461
  // 掛ける形では実物に届かず、 右列が左列の字に重なる (`#425`)。 字ごとの幅で見積もる。
460
462
  const leftMaxWidth = splits.reduce(
461
- (m, s) => Math.max(m, rowColumnWidth(s.left, GENERIC_ROW_LEFT_SIZE_PX, false)),
463
+ (m, s) =>
464
+ Math.max(m, rowColumnWidth(s.left, GENERIC_ROW_LEFT_SIZE_PX, false, textMetrics)),
462
465
  0,
463
466
  );
464
467
  const rightColX = ROW_PAD_X_PX + leftMaxWidth + ROW_COL_GAP_PX;
@@ -1,5 +1,5 @@
1
1
  import type { JSX } from "react";
2
- import { 使える幅, 幅に収める, 箱に積む, type 積む行 } from "./box-lines";
2
+ import { 使える幅, 幅に収める, 箱に積む, 箱の字を測る, type 積む行 } from "./box-lines";
3
3
  /*
4
4
  名前を描く幅の見積り。 **切る側と同じ物差しを使う** (#966)。
5
5
 
@@ -8,7 +8,7 @@ import { 使える幅, 幅に収める, 箱に積む, type 積む行 } from "./b
8
8
  置いたつもりの名前が描く側で切られた (実測 = `Features` が `Featur…` になった)。
9
9
  */
10
10
  import { textWidth as 名前の幅 } from "./text-width";
11
- import type { LaidNode, Tone, MindBranchNode, MindBranchPayload } from "../types";
11
+ import type { CdlTextMetrics, LaidNode, Tone, MindBranchNode, MindBranchPayload } from "../types";
12
12
  import { TONE } from "../render/tone";
13
13
  import { resolveMindData } from "../render/payload-binding";
14
14
  import { 進みを畳む, 伸ばすdash, 段の伸び } from "./draw-progress";
@@ -93,6 +93,7 @@ const MIND_PAD = 40;
93
93
  export function 放射の中身(
94
94
  node: LaidNode,
95
95
  stateValues: Record<string, string> = {},
96
+ textMetrics: CdlTextMetrics = "proportional",
96
97
  ): { nodes: LayoutNode[]; edges: LayoutEdge[] } | undefined {
97
98
  // 名前は状態を読める (#467)。 中心と枝の両方を解決層に通す
98
99
  const mindData = node.mindData ? resolveMindData(node.mindData, stateValues) : undefined;
@@ -103,6 +104,7 @@ export function 放射の中身(
103
104
  node.h / 2,
104
105
  node.w - MIND_PAD * 2,
105
106
  node.h - MIND_PAD * 2,
107
+ textMetrics,
106
108
  );
107
109
  }
108
110
 
@@ -124,6 +126,7 @@ export function MindMapNode({
124
126
  stateValues = {},
125
127
  drawing = false,
126
128
  progress = 1,
129
+ textMetrics = "proportional",
127
130
  }: {
128
131
  node: LaidNode;
129
132
  active: boolean;
@@ -132,11 +135,13 @@ export function MindMapNode({
132
135
  drawing?: boolean;
133
136
  /** phase の進み (0→1)。 `drawing` が true の時だけ読む */
134
137
  progress?: number;
138
+ textMetrics?: CdlTextMetrics;
135
139
  }): JSX.Element {
136
140
  const x0 = node.cx - node.w / 2;
137
141
  const y0 = node.cy - node.h / 2;
138
- const layout = 放射の中身(node, stateValues);
142
+ const layout = 放射の中身(node, stateValues, textMetrics);
139
143
  if (!layout) return <g transform={`translate(${x0} ${y0})`} />;
144
+ const 測る = 箱の字を測る(textMetrics);
140
145
 
141
146
  /**
142
147
  * 枝がどこまで伸びたか (0..1、 cdl#520)。 描く指定が無ければ常に 1 = 全部出ている。
@@ -208,10 +213,17 @@ export function MindMapNode({
208
213
  const 文字の寄せ = 枠なし ? (外へ ? "start" : "end") : "middle";
209
214
  const 描画域の端 = 外へ ? node.w - MIND_PAD : MIND_PAD;
210
215
  const 使える = 枠なし ? Math.abs(描画域の端 - 文字のx) : 使える幅(n.w);
211
- const 収めた名前 = 補足あり ? undefined : 幅に収める(名前.text, 名前.fontSize, 使える);
216
+ const 収めた名前 = 補足あり
217
+ ? undefined
218
+ : 幅に収める(名前.text, 名前.fontSize, 使える, 測る);
212
219
  // 積む側は箱の幅から余白を引いて使える幅を出すので、枠が無い時は同じ幅になる
213
220
  // 箱の幅を渡す。 助っ人の引数を増やすと木の側にも要らない欄が付く
214
- const 行 = 箱に積む(材料, 枠なし ? 使える + (n.w - 使える幅(n.w)) : n.w, n.h);
221
+ const 行 = 箱に積む(
222
+ 材料,
223
+ 枠なし ? 使える + (n.w - 使える幅(n.w)) : n.w,
224
+ n.h,
225
+ 測る,
226
+ );
215
227
  const 文字の色 = n.isRoot
216
228
  ? "var(--cdl-text-on-tone, #ffffff)"
217
229
  : "var(--cdl-chip-text, #1a1f2a)";
@@ -309,11 +321,14 @@ type LayoutEdge = {
309
321
  * 名前が描画域に入らない時、描く側は字を縮めてから切る。 ここで出すのは縮める前の幅 =
310
322
  * 実際に描く幅の上限になる。 縮むのは名前が溢れる図だけで、その時も箱の幅で測るより近い。
311
323
  */
312
- export function 描く横の端(n: LayoutNode): [number, number] {
324
+ export function 描く横の端(
325
+ n: LayoutNode,
326
+ textMetrics: CdlTextMetrics = "proportional",
327
+ ): [number, number] {
313
328
  if (n.isRoot || !n.isLeaf) return [n.x - n.w / 2, n.x + n.w / 2];
314
329
  const 起点 = n.x + (n.side === "right" ? -n.w / 2 : n.w / 2);
315
330
  const 名前 = 放射の行(n)[0]!;
316
- const 幅 = 名前の幅(名前.text, 名前.fontSize);
331
+ const 幅 = 名前の幅(名前.text, 名前.fontSize, 0, textMetrics);
317
332
  return n.side === "right" ? [起点, 起点 + 幅] : [起点 - 幅, 起点];
318
333
  }
319
334
 
@@ -323,6 +338,7 @@ export function computeMindMapLayout(
323
338
  cy: number,
324
339
  canvasW: number,
325
340
  canvasH: number,
341
+ textMetrics: CdlTextMetrics = "proportional",
326
342
  ): { nodes: LayoutNode[]; edges: LayoutEdge[] } {
327
343
  const nodes: LayoutNode[] = [];
328
344
  const edges: LayoutEdge[] = [];
@@ -422,7 +438,7 @@ export function computeMindMapLayout(
422
438
  /** 枝の名前を描く幅。 字の級は `放射の行` から取る (#538、 書き写すと 2 経路で別の値になる) */
423
439
  const 枝の名前の幅 = (title: string): number => {
424
440
  const 名前 = 放射の行({ title, isRoot: false })[0]!;
425
- return 名前の幅(名前.text, 名前.fontSize);
441
+ return 名前の幅(名前.text, 名前.fontSize, 0, textMetrics);
426
442
  };
427
443
 
428
444
  /** 片側の横の割付。 枝の離れと、その側の sub box の幅と、深さごとの追い出し量を持つ */
@@ -655,7 +671,7 @@ export function computeMindMapLayout(
655
671
  // 何もはみ出していない図まで縮む (実測 = 枝 1 本の図が 0.83 倍になり、
656
672
  // 箱の高さが 30.8 → 25.6 に縮んで補足の行が落ちた)。
657
673
  // すぐ下の寄せも同じ `描く横の端` で測るので、縮めると寄せの基準も揃う。
658
- const [左, 右] = 描く横の端(n);
674
+ const [左, 右] = 描く横の端(n, textMetrics);
659
675
  maxExtentX = Math.max(maxExtentX, Math.abs(左 - cx), Math.abs(右 - cx));
660
676
  maxExtentY = Math.max(maxExtentY, Math.abs(n.y - cy) + n.h / 2);
661
677
  }
@@ -683,7 +699,7 @@ export function computeMindMapLayout(
683
699
  let 左端 = Number.POSITIVE_INFINITY;
684
700
  let 右端 = Number.NEGATIVE_INFINITY;
685
701
  for (const n of nodes) {
686
- const [l, r] = 描く横の端(n);
702
+ const [l, r] = 描く横の端(n, textMetrics);
687
703
  左端 = Math.min(左端, l);
688
704
  右端 = Math.max(右端, r);
689
705
  }
@@ -1,5 +1,5 @@
1
1
  import type { JSX } from "react";
2
- import type { LaidNode } from "../types";
2
+ import type { CdlTextMetrics, LaidNode } from "../types";
3
3
  import { TONE, toneFill } from "../render/tone";
4
4
  import { measureTextWidth, 収める倍率 } from "../layout/text-width";
5
5
  import { resolveQuadrantData } from "../render/payload-binding";
@@ -23,10 +23,12 @@ export function QuadrantNode({
23
23
  node,
24
24
  active,
25
25
  stateValues = {},
26
+ textMetrics = "proportional",
26
27
  }: {
27
28
  node: LaidNode;
28
29
  active: boolean;
29
30
  stateValues?: Record<string, string>;
31
+ textMetrics?: CdlTextMetrics;
30
32
  }): JSX.Element {
31
33
  const x0 = node.cx - node.w / 2;
32
34
  const y0 = node.cy - node.h / 2;
@@ -56,7 +58,11 @@ export function QuadrantNode({
56
58
  const axisLabelPad = (text: string): number =>
57
59
  Math.max(
58
60
  PAD_MIN,
59
- measureTextWidth(`← ${text}`, { fontSize: AXIS_LABEL_FONT, fontWeight: 700 }) + 12,
61
+ measureTextWidth(`← ${text}`, {
62
+ fontSize: AXIS_LABEL_FONT,
63
+ fontWeight: 700,
64
+ textMetrics,
65
+ }) + 12,
60
66
  );
61
67
  // 名前が長くても図そのものは残す。 上限を置かないと、 名前が箱の幅を超えた時に
62
68
  // 描く場所が無くなる (実測 = 幅 640 に 50 文字の名前を左右へ置くと canvasW が -143 になった)。
@@ -294,7 +300,11 @@ export function QuadrantNode({
294
300
  ITEM_MIN_W,
295
301
  Math.max(
296
302
  ...行ら.map((r) =>
297
- measureTextWidth(r.text, { fontSize: r.size, fontWeight: r.weight }),
303
+ measureTextWidth(r.text, {
304
+ fontSize: r.size,
305
+ fontWeight: r.weight,
306
+ textMetrics,
307
+ }),
298
308
  ),
299
309
  ) +
300
310
  ITEM_PAD_X * 2,
@@ -333,7 +343,7 @@ export function QuadrantNode({
333
343
  SUB_FONT *
334
344
  収める倍率(
335
345
  補足,
336
- { fontSize: SUB_FONT, fontWeight: 500 },
346
+ { fontSize: SUB_FONT, fontWeight: 500, textMetrics },
337
347
  Math.max(0, 幅 - ITEM_PAD_X * 2),
338
348
  );
339
349
  return (
@@ -1,3 +1,4 @@
1
+ import type { CdlTextMetrics } from "../types";
1
2
  import { textWidth } from "./text-width";
2
3
 
3
4
  /**
@@ -114,8 +115,15 @@ export function splitRow(row: string): { left: string; right: string; split: boo
114
115
  * 等幅と比例を 1 つの関数で分けるのは、 同じ `generic` の行でも左が比例 (18) / 右が等幅 (21)
115
116
  * だから。 呼ぶ側が列ごとに選ぶ。
116
117
  */
117
- export function rowColumnWidth(text: string, size: number, mono: boolean): number {
118
- return mono ? displayWidth(text) * size * MONO_ADVANCE_RATIO : textWidth(text, size);
118
+ export function rowColumnWidth(
119
+ text: string,
120
+ size: number,
121
+ mono: boolean,
122
+ textMetrics: CdlTextMetrics = "proportional",
123
+ ): number {
124
+ return mono
125
+ ? displayWidth(text) * size * MONO_ADVANCE_RATIO
126
+ : textWidth(text, size, 0, textMetrics);
119
127
  }
120
128
 
121
129
  /**
@@ -179,7 +187,11 @@ export function 桁で省く(text: string, 上限: number): string {
179
187
  * 上に丸めるのは、 送り幅が小数のため幅が小数になり、 図の座標が読みにくくなるから。 下に
180
188
  * 丸めると 1 未満だけ足りない箱ができる。
181
189
  */
182
- export function requiredRowsWidth(rows: string[] | undefined, kind?: string): number {
190
+ export function requiredRowsWidth(
191
+ rows: string[] | undefined,
192
+ kind?: string,
193
+ textMetrics: CdlTextMetrics = "proportional",
194
+ ): number {
183
195
  if (!rows || rows.length === 0) return 0;
184
196
  const splits = rows.map(splitRow);
185
197
  const 一番広い = (取る: (s: { left: string; right: string }) => number) =>
@@ -188,17 +200,17 @@ export function requiredRowsWidth(rows: string[] | undefined, kind?: string): nu
188
200
  /** `storage` = 左右とも等幅の 26。 列の間に縦線を引くため間隔が広い。 */
189
201
  const storage =
190
202
  ROW_PAD_X_PX +
191
- 一番広い((s) => rowColumnWidth(s.left, 26, true)) +
203
+ 一番広い((s) => rowColumnWidth(s.left, 26, true, textMetrics)) +
192
204
  STORAGE_COL_GAP_PX +
193
- 一番広い((s) => rowColumnWidth(s.right, 26, true)) +
205
+ 一番広い((s) => rowColumnWidth(s.right, 26, true, textMetrics)) +
194
206
  ROW_RIGHT_PAD_PX;
195
207
 
196
208
  /** `generic` = 左が比例の 18、 右が等幅の 21。 */
197
209
  const generic =
198
210
  ROW_PAD_X_PX +
199
- 一番広い((s) => rowColumnWidth(s.left, GENERIC_ROW_LEFT_SIZE_PX, false)) +
211
+ 一番広い((s) => rowColumnWidth(s.left, GENERIC_ROW_LEFT_SIZE_PX, false, textMetrics)) +
200
212
  ROW_COL_GAP_PX +
201
- 一番広い((s) => rowColumnWidth(s.right, GENERIC_ROW_RIGHT_SIZE_PX, true)) +
213
+ 一番広い((s) => rowColumnWidth(s.right, GENERIC_ROW_RIGHT_SIZE_PX, true, textMetrics)) +
202
214
  ROW_RIGHT_PAD_PX;
203
215
 
204
216
  // **種別が分からない時は広い方を返す**。 どちらで描かれるか決められないので、 狭い方を返すと
@@ -8,6 +8,7 @@
8
8
  * React を引き込まない。
9
9
  */
10
10
  import { textWidth } from "./text-width";
11
+ import type { CdlTextMetrics } from "../types";
11
12
 
12
13
  /** 画布の縁 */
13
14
  export const SEQ_EDGE = 16;
@@ -100,12 +101,21 @@ export type SequenceBoardMetrics = {
100
101
  * 幅は **語の桁 + 列 × 面の数**。 語を線の脇ではなく左の桁に置くので、線の長さが語の長さに
101
102
  * 引きずられない = 面の数だけで横幅が決まる。
102
103
  */
103
- export function sequenceBoardMetrics(data: SequenceBoardData): SequenceBoardMetrics {
104
+ export function sequenceBoardMetrics(
105
+ data: SequenceBoardData,
106
+ textMetrics: CdlTextMetrics = "proportional",
107
+ ): SequenceBoardMetrics {
104
108
  const 名幅 = data.actors.map((a) =>
105
- Math.max(textWidth(a.name, SEQ_NAME), a.subtitle ? textWidth(a.subtitle, SEQ_SUB) : 0),
109
+ Math.max(
110
+ textWidth(a.name, SEQ_NAME, 0, textMetrics),
111
+ a.subtitle ? textWidth(a.subtitle, SEQ_SUB, 0, textMetrics) : 0,
112
+ ),
106
113
  );
107
114
  const colW = g4(Math.max(SEQ_COL_MIN, Math.max(0, ...名幅) + SEQ_COL_PAD));
108
- const 語幅 = Math.max(0, ...data.messages.map((m) => textWidth(m.label, SEQ_LABEL)));
115
+ const 語幅 = Math.max(
116
+ 0,
117
+ ...data.messages.map((m) => textWidth(m.label, SEQ_LABEL, 0, textMetrics)),
118
+ );
109
119
  const gutter = g4(SEQ_EDGE + SEQ_GUTTER_PAD + 語幅 + SEQ_GUTTER_GAP);
110
120
  const headY = SEQ_EDGE + SEQ_HEAD;
111
121
  const rowY = (i: number): number => headY + SEQ_ROW_PAD + i * SEQ_ROW;
@@ -1,6 +1,6 @@
1
1
  import { 等幅の字 } from "../render/mono-text";
2
2
  import type { JSX } from "react";
3
- import type { LaidNode } from "../types";
3
+ import type { CdlTextMetrics, LaidNode } from "../types";
4
4
  import { interpolate } from "../render/utils";
5
5
  import {
6
6
  SEQ_BAND_PAD,
@@ -45,13 +45,15 @@ import {
45
45
  export function SequenceBoardNode({
46
46
  node,
47
47
  stateValues,
48
+ textMetrics = "proportional",
48
49
  }: {
49
50
  node: LaidNode;
50
51
  stateValues?: Record<string, string>;
52
+ textMetrics?: CdlTextMetrics;
51
53
  }): JSX.Element {
52
54
  const data: SequenceBoardData | undefined = node.sequenceData;
53
55
  if (!data) return <g data-cdl-role="sequence-board" />;
54
- const m = sequenceBoardMetrics(data);
56
+ const m = sequenceBoardMetrics(data, textMetrics);
55
57
  const x = node.cx - node.w / 2;
56
58
  const y = node.cy - node.h / 2;
57
59