@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.
- package/CHANGELOG.md +28 -1
- package/README.md +70 -1
- package/dist/index.cjs +1125 -618
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +38 -26
- package/dist/index.d.ts +38 -26
- package/dist/index.js +1125 -619
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +894 -404
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js +894 -404
- package/dist/react.js.map +1 -1
- package/dist/{render-U_pC-Ksh.d.cts → render-U6omSNH2.d.cts} +23 -4
- package/dist/{render-U_pC-Ksh.d.ts → render-U6omSNH2.d.ts} +23 -4
- package/package.json +9 -12
- package/src/builder.ts +5 -0
- package/src/index.ts +2 -0
- package/src/kinds/box-lines.ts +17 -4
- package/src/kinds/chart-bar.tsx +4 -0
- package/src/kinds/chart-gauge.tsx +20 -9
- package/src/kinds/chart-pie.tsx +67 -17
- package/src/kinds/chart-radial.tsx +7 -2
- package/src/kinds/chart-slope.tsx +26 -7
- package/src/kinds/chart-stacked-bar.tsx +8 -4
- package/src/kinds/chart-stat.tsx +19 -4
- package/src/kinds/chart-waffle.tsx +7 -2
- package/src/kinds/funnel.tsx +9 -2
- package/src/kinds/generic.tsx +5 -2
- package/src/kinds/mind-map.tsx +26 -10
- package/src/kinds/quadrant.tsx +14 -4
- package/src/kinds/row-align.ts +19 -7
- package/src/kinds/sequence-board-metrics.ts +13 -3
- package/src/kinds/sequence-board.tsx +4 -2
- package/src/kinds/shape-basement.tsx +57 -32
- package/src/kinds/shape-blockchain.tsx +83 -34
- package/src/kinds/shape-commerce.tsx +65 -24
- package/src/kinds/shape-finance.tsx +90 -26
- package/src/kinds/shape-hardware.tsx +48 -17
- package/src/kinds/shape-region.tsx +16 -6
- package/src/kinds/shape-software.tsx +30 -8
- package/src/kinds/text-width.ts +17 -3
- package/src/kinds/tree.tsx +25 -7
- package/src/layout/collisions.ts +21 -8
- package/src/layout/curve-path.ts +3 -2
- package/src/layout/edges.ts +173 -29
- package/src/layout/head-label.ts +3 -1
- package/src/layout/lanes.ts +3 -2
- package/src/layout/metro-path.ts +3 -2
- package/src/layout/nodes.ts +14 -6
- package/src/layout/predict-bbox.ts +6 -3
- package/src/layout/self-loop.ts +10 -4
- package/src/layout/spec.ts +20 -6
- package/src/layout/text-width.ts +25 -8
- package/src/layout.ts +39 -15
- package/src/presets.ts +69 -43
- package/src/render/edges.tsx +18 -3
- package/src/render/interactive-panel.tsx +34 -7
- package/src/render/nodes.tsx +97 -48
- package/src/render/stage.tsx +3 -0
- package/src/text-metrics.ts +9 -0
- package/src/types.ts +22 -2
- 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 名前の級 = 収める級(
|
|
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 収める級(
|
|
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 (
|
|
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.名前,
|
|
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,
|
|
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;
|
package/src/kinds/chart-stat.tsx
CHANGED
|
@@ -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 数値の級 = 収める級(
|
|
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 収める級(
|
|
303
|
+
export function 収める級(
|
|
304
|
+
字: string,
|
|
305
|
+
使える幅: number,
|
|
306
|
+
初期: number,
|
|
307
|
+
下限: number,
|
|
308
|
+
textMetrics: CdlTextMetrics = "proportional",
|
|
309
|
+
): number {
|
|
295
310
|
let 級 = 初期;
|
|
296
|
-
while (級 > 下限 && textWidth(字,
|
|
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(
|
|
121
|
+
Math.max(
|
|
122
|
+
textWidth(d.label ?? "", 名前の級, 0, textMetrics),
|
|
123
|
+
textWidth(数値の文字[i] ?? "", 数値の級, 0, textMetrics),
|
|
124
|
+
),
|
|
120
125
|
),
|
|
121
126
|
);
|
|
122
127
|
|
package/src/kinds/funnel.tsx
CHANGED
|
@@ -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
|
-
補足の枠字 *
|
|
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;
|
package/src/kinds/generic.tsx
CHANGED
|
@@ -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) =>
|
|
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;
|
package/src/kinds/mind-map.tsx
CHANGED
|
@@ -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 収めた名前 = 補足あり
|
|
216
|
+
const 収めた名前 = 補足あり
|
|
217
|
+
? undefined
|
|
218
|
+
: 幅に収める(名前.text, 名前.fontSize, 使える, 測る);
|
|
212
219
|
// 積む側は箱の幅から余白を引いて使える幅を出すので、枠が無い時は同じ幅になる
|
|
213
220
|
// 箱の幅を渡す。 助っ人の引数を増やすと木の側にも要らない欄が付く
|
|
214
|
-
const 行 = 箱に積む(
|
|
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 描く横の端(
|
|
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
|
}
|
package/src/kinds/quadrant.tsx
CHANGED
|
@@ -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}`, {
|
|
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, {
|
|
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 (
|
package/src/kinds/row-align.ts
CHANGED
|
@@ -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(
|
|
118
|
-
|
|
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(
|
|
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(
|
|
104
|
+
export function sequenceBoardMetrics(
|
|
105
|
+
data: SequenceBoardData,
|
|
106
|
+
textMetrics: CdlTextMetrics = "proportional",
|
|
107
|
+
): SequenceBoardMetrics {
|
|
104
108
|
const 名幅 = data.actors.map((a) =>
|
|
105
|
-
Math.max(
|
|
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(
|
|
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
|
|