@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.
- package/CHANGELOG.md +51 -1
- package/README.md +137 -0
- package/SPEC.md +1 -1
- package/dist/index.cjs +1513 -667
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +51 -33
- package/dist/index.d.ts +51 -33
- package/dist/index.js +1513 -668
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +1271 -444
- 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 +1271 -444
- package/dist/react.js.map +1 -1
- package/dist/{render-A5AKjNuy.d.cts → render-CVczwfRb.d.cts} +40 -7
- package/dist/{render-A5AKjNuy.d.ts → render-CVczwfRb.d.ts} +40 -7
- package/package.json +9 -12
- package/src/builder.ts +8 -2
- package/src/index.ts +3 -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/dyn-shape.tsx +22 -9
- 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/station.tsx +29 -0
- package/src/kinds/text-width.ts +17 -3
- package/src/kinds/tree.tsx +25 -7
- package/src/layout/collisions.ts +50 -28
- package/src/layout/curve-path.ts +126 -0
- package/src/layout/edges.ts +271 -58
- package/src/layout/head-label.ts +11 -5
- package/src/layout/lanes.ts +3 -2
- package/src/layout/metro-path.ts +185 -0
- 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 +36 -7
- package/src/layout/text-width.ts +25 -8
- package/src/layout/tokens.ts +2 -0
- package/src/layout-with-validation.ts +10 -5
- package/src/layout.ts +55 -21
- package/src/presets.ts +69 -43
- package/src/render/edges.tsx +19 -3
- package/src/render/interactive-panel.tsx +34 -7
- package/src/render/nodes.tsx +100 -48
- package/src/render/stage.tsx +3 -0
- package/src/text-metrics.ts +9 -0
- package/src/types.ts +41 -4
- package/src/visual-validate.ts +91 -21
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
|
|
|
@@ -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 {
|
|
4
4
|
ASCENT,
|
|
5
5
|
DESCENT,
|
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
type ShapeProps = {
|
|
44
44
|
node: LaidNode;
|
|
45
45
|
active: boolean;
|
|
46
|
+
textMetrics?: CdlTextMetrics;
|
|
46
47
|
};
|
|
47
48
|
|
|
48
49
|
// basement は shape 全体が body なので、 純白ではなく accent 青系薄 tint (#dde8f0) で
|
|
@@ -81,34 +82,44 @@ function labelBlock(
|
|
|
81
82
|
pos: { x: number; y: number },
|
|
82
83
|
size: number,
|
|
83
84
|
band: LabelBand,
|
|
85
|
+
textMetrics: CdlTextMetrics = "proportional",
|
|
84
86
|
): { k: number; x: number; baseline: number; 上端: number; 下端: number } {
|
|
85
87
|
const 上 = node.eyebrow ? size + EYEBROW_GAP + EYEBROW_SIZE * ASCENT : size * ASCENT;
|
|
86
88
|
const 下 = node.subtitle ? size + SUB_GAP + SUB_SIZE * DESCENT : size * DESCENT;
|
|
87
89
|
// 3 行のうち一番広い行が横の制約になる
|
|
88
90
|
const 幅 = Math.max(
|
|
89
|
-
textWidth(node.title ?? "", size),
|
|
90
|
-
node.eyebrow ? textWidth(node.eyebrow, EYEBROW_SIZE, EYEBROW_TRACKING) : 0,
|
|
91
|
-
node.subtitle ? textWidth(node.subtitle, SUB_SIZE) : 0,
|
|
91
|
+
textWidth(node.title ?? "", size, 0, textMetrics),
|
|
92
|
+
node.eyebrow ? textWidth(node.eyebrow, EYEBROW_SIZE, EYEBROW_TRACKING, textMetrics) : 0,
|
|
93
|
+
node.subtitle ? textWidth(node.subtitle, SUB_SIZE, 0, textMetrics) : 0,
|
|
92
94
|
);
|
|
93
95
|
// 横で縮める倍率を読める下限で止める (#953)。 切って収まらない幅では下限を当てない。
|
|
94
96
|
//
|
|
95
97
|
// **題が一番広い行のときだけ当てる** (#957)。 塊の倍率は一番広い行で決まるので、
|
|
96
98
|
// 補いの行が幅を決めている箱で下限を当てると、題を 0.6px 上げるために補いの行から
|
|
97
99
|
// 字を落とすことになる = 払うのと得るのが別の行になる
|
|
98
|
-
const 題幅 = textWidth(node.title ?? "", size);
|
|
100
|
+
const 題幅 = textWidth(node.title ?? "", size, 0, textMetrics);
|
|
101
|
+
const 測る = (t: string, s: number, ls: number): number => textWidth(t, s, ls, textMetrics);
|
|
99
102
|
const 下限 =
|
|
100
|
-
題幅 >= 幅 ? 切って収まる下限(node.title ?? "", size, 0, band.w,
|
|
103
|
+
題幅 >= 幅 ? 切って収まる下限(node.title ?? "", size, 0, band.w, 測る) : 0;
|
|
101
104
|
const { k, x, y: baseline } = fitLabel(band, { 上, 下, 幅 }, pos, 下限);
|
|
102
105
|
return { k, x, baseline, 上端: baseline - 上 * k, 下端: baseline + 下 * k };
|
|
103
106
|
}
|
|
104
107
|
|
|
105
|
-
function commonLabel(
|
|
106
|
-
|
|
108
|
+
function commonLabel(
|
|
109
|
+
node: LaidNode,
|
|
110
|
+
pos: { x: number; y: number },
|
|
111
|
+
size: number,
|
|
112
|
+
band: LabelBand,
|
|
113
|
+
textMetrics: CdlTextMetrics = "proportional",
|
|
114
|
+
) {
|
|
115
|
+
const 塊 = labelBlock(node, pos, size, band, textMetrics);
|
|
107
116
|
const k = 塊.k;
|
|
108
117
|
const x = 塊.x;
|
|
109
118
|
// 下限で止めた行は帯に入らないので、入るところまで切って `…` を付ける (#953)
|
|
110
119
|
const 切る = (t: string, 大きさ: number, 字間 = 0) =>
|
|
111
|
-
帯で切る(t, 大きさ * k, band.w, (u, f) =>
|
|
120
|
+
帯で切る(t, 大きさ * k, band.w, (u, f) =>
|
|
121
|
+
textWidth(u, f, 字間 * k, textMetrics),
|
|
122
|
+
);
|
|
112
123
|
return (
|
|
113
124
|
<>
|
|
114
125
|
{node.eyebrow && (
|
|
@@ -171,7 +182,7 @@ const RULE_PAD_RATIO = 0.14;
|
|
|
171
182
|
const RULE_SHRINK_RATIO = 0.055;
|
|
172
183
|
|
|
173
184
|
/** shape-file = 縦長 A4 型 file (right-top folded corner、 body に fold 内側 corner 統合) + drop shadow + text lines */
|
|
174
|
-
export function ShapeFileNode({ node, active }: ShapeProps): JSX.Element {
|
|
185
|
+
export function ShapeFileNode({ node, active, textMetrics = "proportional" }: ShapeProps): JSX.Element {
|
|
175
186
|
const 帯 = bodyBand(node, { 影ry: SHADOW_RY });
|
|
176
187
|
const 紙h = 帯.h;
|
|
177
188
|
const 紙w = Math.min(帯.w, 紙h * 0.72);
|
|
@@ -197,7 +208,7 @@ export function ShapeFileNode({ node, active }: ShapeProps): JSX.Element {
|
|
|
197
208
|
const 名前帯 = { x, y, w: 紙w, h: 紙h };
|
|
198
209
|
const 名前位置 = { x: 帯.cx, y: node.cy + node.h * 0.12 };
|
|
199
210
|
// 名前の塊を先に測る。 3 本目が紙頭から固定 90 の位置に出ていたので、 塊に食い込む線は引かない
|
|
200
|
-
const 名前 = labelBlock(node, 名前位置, 16,
|
|
211
|
+
const 名前 = labelBlock(node, 名前位置, 16, 名前帯, textMetrics);
|
|
201
212
|
const 間隔 = Math.min(RULE_GAP_MAX, 紙h * RULE_GAP_RATIO);
|
|
202
213
|
const 線余白 = Math.min(RULE_PAD_MAX, 紙w * RULE_PAD_RATIO);
|
|
203
214
|
const textLines = [0, 1, 2]
|
|
@@ -231,7 +242,7 @@ export function ShapeFileNode({ node, active }: ShapeProps): JSX.Element {
|
|
|
231
242
|
);
|
|
232
243
|
})}
|
|
233
244
|
{/* label = 中央 (text lines の下、 title 大、 subtitle 小、 A4 下半分) */}
|
|
234
|
-
{commonLabel(node, 名前位置, 16,
|
|
245
|
+
{commonLabel(node, 名前位置, 16, 名前帯, textMetrics)}
|
|
235
246
|
</g>
|
|
236
247
|
);
|
|
237
248
|
}
|
|
@@ -241,7 +252,7 @@ const TAB_H_MAX = 22;
|
|
|
241
252
|
const TAB_H_RATIO = 0.3;
|
|
242
253
|
|
|
243
254
|
/** shape-folder = 左上 tab を body 左端に完全張り付け (tab 左 0、 tab 右 35%、 slope 42%) + drop shadow */
|
|
244
|
-
export function ShapeFolderNode({ node, active }: ShapeProps): JSX.Element {
|
|
255
|
+
export function ShapeFolderNode({ node, active, textMetrics = "proportional" }: ShapeProps): JSX.Element {
|
|
245
256
|
const 帯 = bodyBand(node, { 影ry: SHADOW_RY });
|
|
246
257
|
const x = 帯.x;
|
|
247
258
|
const y = 帯.y;
|
|
@@ -266,7 +277,7 @@ export function ShapeFolderNode({ node, active }: ShapeProps): JSX.Element {
|
|
|
266
277
|
const 境界y = y + tabH + Math.min(8, (帯.h - tabH) * 0.2);
|
|
267
278
|
const 端 = Math.min(6, 帯.w * 0.1);
|
|
268
279
|
const 名前位置 = { x: 帯.cx, y: node.cy + 18 };
|
|
269
|
-
const 名前 = labelBlock(node, 名前位置, 16,
|
|
280
|
+
const 名前 = labelBlock(node, 名前位置, 16, 帯, textMetrics);
|
|
270
281
|
return (
|
|
271
282
|
<g>
|
|
272
283
|
{dropShadow(帯.cx, 帯.底, 帯.w * 0.44)}
|
|
@@ -284,7 +295,7 @@ export function ShapeFolderNode({ node, active }: ShapeProps): JSX.Element {
|
|
|
284
295
|
opacity={0.35}
|
|
285
296
|
/>
|
|
286
297
|
)}
|
|
287
|
-
{commonLabel(node, 名前位置, 16,
|
|
298
|
+
{commonLabel(node, 名前位置, 16, 帯, textMetrics)}
|
|
288
299
|
</g>
|
|
289
300
|
);
|
|
290
301
|
}
|
|
@@ -297,7 +308,7 @@ const CLOUD_VB_H = 512;
|
|
|
297
308
|
const CLOUD_W_RATIO = 0.9;
|
|
298
309
|
|
|
299
310
|
/** shape-cloud = Font Awesome cloud icon path を scale + translate + drop shadow */
|
|
300
|
-
export function ShapeCloudNode({ node, active }: ShapeProps): JSX.Element {
|
|
311
|
+
export function ShapeCloudNode({ node, active, textMetrics = "proportional" }: ShapeProps): JSX.Element {
|
|
301
312
|
const 帯 = bodyBand(node, { 影ry: SHADOW_RY });
|
|
302
313
|
const frame = commonFrame(active);
|
|
303
314
|
// Font Awesome "cloud" icon の path (viewBox 640 x 512) を帯に scale + center。
|
|
@@ -328,7 +339,7 @@ export function ShapeCloudNode({ node, active }: ShapeProps): JSX.Element {
|
|
|
328
339
|
/>
|
|
329
340
|
</g>
|
|
330
341
|
{/* label = cloud 内側の中央 */}
|
|
331
|
-
{commonLabel(node, { x: 帯.cx, y: node.cy + node.h * 0.05 }, 16,
|
|
342
|
+
{commonLabel(node, { x: 帯.cx, y: node.cy + node.h * 0.05 }, 16, 帯, textMetrics)}
|
|
332
343
|
</g>
|
|
333
344
|
);
|
|
334
345
|
}
|
|
@@ -341,7 +352,7 @@ const CYLINDER_RY_RATIO = 0.1;
|
|
|
341
352
|
const CYLINDER_DISKS = [0.3, 0.55, 0.8];
|
|
342
353
|
|
|
343
354
|
/** shape-cylinder = 3D 円柱 (上面 ellipse + 側面 + 底面 arc 下半分のみ) + drop shadow */
|
|
344
|
-
export function ShapeCylinderNode({ node, active }: ShapeProps): JSX.Element {
|
|
355
|
+
export function ShapeCylinderNode({ node, active, textMetrics = "proportional" }: ShapeProps): JSX.Element {
|
|
345
356
|
const 帯 = bodyBand(node, { 影ry: CYLINDER_SHADOW_RY });
|
|
346
357
|
const x = 帯.x;
|
|
347
358
|
const y = 帯.y;
|
|
@@ -361,7 +372,7 @@ export function ShapeCylinderNode({ node, active }: ShapeProps): JSX.Element {
|
|
|
361
372
|
const 蓋底 = y + ry * 2;
|
|
362
373
|
const 名前位置 = { x: 帯.cx, y: node.cy + 6 };
|
|
363
374
|
const 名前帯 = { x: 帯.x, y: 蓋底, w: 帯.w, h: Math.max(0, 帯.底 - 蓋底) };
|
|
364
|
-
const 名前 = labelBlock(node, 名前位置, 15,
|
|
375
|
+
const 名前 = labelBlock(node, 名前位置, 15, 名前帯, textMetrics);
|
|
365
376
|
return (
|
|
366
377
|
<g>
|
|
367
378
|
{dropShadow(帯.cx, 帯.底, rx * 0.85, CYLINDER_SHADOW_RY)}
|
|
@@ -386,7 +397,7 @@ export function ShapeCylinderNode({ node, active }: ShapeProps): JSX.Element {
|
|
|
386
397
|
opacity={0.3}
|
|
387
398
|
/>
|
|
388
399
|
))}
|
|
389
|
-
{commonLabel(node, 名前位置, 15,
|
|
400
|
+
{commonLabel(node, 名前位置, 15, 名前帯, textMetrics)}
|
|
390
401
|
</g>
|
|
391
402
|
);
|
|
392
403
|
}
|
|
@@ -395,7 +406,7 @@ export function ShapeCylinderNode({ node, active }: ShapeProps): JSX.Element {
|
|
|
395
406
|
const HEX_DOT_R = 2.5;
|
|
396
407
|
|
|
397
408
|
/** shape-hexagon = 六角形 (component / service 表現、 accent 頂点) + drop shadow */
|
|
398
|
-
export function ShapeHexagonNode({ node, active }: ShapeProps): JSX.Element {
|
|
409
|
+
export function ShapeHexagonNode({ node, active, textMetrics = "proportional" }: ShapeProps): JSX.Element {
|
|
399
410
|
// 点は多角形の外へ出るので、 枠より点の方が広ければ点に合わせて帯を縮める
|
|
400
411
|
const 帯 = bodyBand(node, { 影ry: SHADOW_RY, 外: Math.max(HEX_DOT_R, FRAME_HALF) });
|
|
401
412
|
const cx = 帯.cx;
|
|
@@ -426,13 +437,14 @@ export function ShapeHexagonNode({ node, active }: ShapeProps): JSX.Element {
|
|
|
426
437
|
{ x: cx, y: node.cy + 6 },
|
|
427
438
|
16,
|
|
428
439
|
{ x: 帯.x + cut, y: 帯.y, w: Math.max(0, 帯.w - cut * 2), h: 帯.h },
|
|
440
|
+
textMetrics,
|
|
429
441
|
)}
|
|
430
442
|
</g>
|
|
431
443
|
);
|
|
432
444
|
}
|
|
433
445
|
|
|
434
446
|
/** shape-diamond = ひし形 (decision / 判定) + drop shadow + inner border */
|
|
435
|
-
export function ShapeDiamondNode({ node, active }: ShapeProps): JSX.Element {
|
|
447
|
+
export function ShapeDiamondNode({ node, active, textMetrics = "proportional" }: ShapeProps): JSX.Element {
|
|
436
448
|
const 帯 = bodyBand(node, { 影ry: SHADOW_RY });
|
|
437
449
|
const cx = 帯.cx;
|
|
438
450
|
const cy = 帯.cy;
|
|
@@ -475,6 +487,7 @@ export function ShapeDiamondNode({ node, active }: ShapeProps): JSX.Element {
|
|
|
475
487
|
w: (帯.w * innerScale) / 2,
|
|
476
488
|
h: (帯.h * innerScale) / 2,
|
|
477
489
|
},
|
|
490
|
+
textMetrics,
|
|
478
491
|
)}
|
|
479
492
|
</g>
|
|
480
493
|
);
|
|
@@ -485,7 +498,7 @@ const STACK_OFFSET_MAX = 12;
|
|
|
485
498
|
const STACK_OFFSET_RATIO = 0.15;
|
|
486
499
|
|
|
487
500
|
/** shape-stack = 重ね rect 3 枚 (layer / history) + drop shadow */
|
|
488
|
-
export function ShapeStackNode({ node, active }: ShapeProps): JSX.Element {
|
|
501
|
+
export function ShapeStackNode({ node, active, textMetrics = "proportional" }: ShapeProps): JSX.Element {
|
|
489
502
|
const 帯 = bodyBand(node, { 影ry: SHADOW_RY });
|
|
490
503
|
const x = 帯.x;
|
|
491
504
|
const y = 帯.y;
|
|
@@ -539,6 +552,7 @@ export function ShapeStackNode({ node, active }: ShapeProps): JSX.Element {
|
|
|
539
552
|
{ x: 手前cx, y: node.cy + ずれ + 6 },
|
|
540
553
|
15,
|
|
541
554
|
{ x, y: y + ずれ * 2, w: cardW, h: cardH },
|
|
555
|
+
textMetrics,
|
|
542
556
|
)}
|
|
543
557
|
</g>
|
|
544
558
|
);
|
|
@@ -564,21 +578,32 @@ const PERSON_SHADOW_RX = 55;
|
|
|
564
578
|
* 動かさず **字の大きさだけ横に入る倍率まで下げる** = 各行の張り出しは縮むだけなので、
|
|
565
579
|
* `labelPlan` が保証した縦の収まりは崩れない。
|
|
566
580
|
*/
|
|
567
|
-
function planLabel(
|
|
581
|
+
function planLabel(
|
|
582
|
+
node: LaidNode,
|
|
583
|
+
cx: number,
|
|
584
|
+
幅上限: number,
|
|
585
|
+
plan: ReturnType<typeof labelPlan>,
|
|
586
|
+
textMetrics: CdlTextMetrics = "proportional",
|
|
587
|
+
) {
|
|
568
588
|
const 幅 = Math.max(
|
|
569
|
-
textWidth(node.title ?? "", plan.名前.size),
|
|
570
|
-
node.eyebrow && plan.肩書
|
|
571
|
-
|
|
589
|
+
textWidth(node.title ?? "", plan.名前.size, 0, textMetrics),
|
|
590
|
+
node.eyebrow && plan.肩書
|
|
591
|
+
? textWidth(node.eyebrow, plan.肩書.size, EYEBROW_TRACKING, textMetrics)
|
|
592
|
+
: 0,
|
|
593
|
+
node.subtitle && plan.説明 ? textWidth(node.subtitle, plan.説明.size, 0, textMetrics) : 0,
|
|
572
594
|
);
|
|
573
595
|
// 縮めは読める下限で止める (#953)。 下限が無いと長い名前が点にしか見えない大きさになる。
|
|
574
596
|
// 題が一番広い行のときだけ当てる (#957、 上の `labelBlock` と同じ理由)
|
|
575
|
-
const 題幅 = textWidth(node.title ?? "", plan.名前.size);
|
|
597
|
+
const 題幅 = textWidth(node.title ?? "", plan.名前.size, 0, textMetrics);
|
|
598
|
+
const 測る = (t: string, s: number, ls: number): number => textWidth(t, s, ls, textMetrics);
|
|
576
599
|
const 下限 =
|
|
577
|
-
題幅 >= 幅 ? 切って収まる下限(node.title ?? "", plan.名前.size, 0, 幅上限,
|
|
600
|
+
題幅 >= 幅 ? 切って収まる下限(node.title ?? "", plan.名前.size, 0, 幅上限, 測る) : 0;
|
|
578
601
|
const k = 幅 > 0 && 幅上限 > 0 ? Math.min(1, Math.max(幅上限 / 幅, 下限)) : 1;
|
|
579
602
|
// 下限で止めた行は箱に入らないので、入るところまで切って `…` を付ける (#953)
|
|
580
603
|
const 切る = (t: string, 大きさ: number, 字間 = 0) =>
|
|
581
|
-
帯で切る(t, 大きさ * k, 幅上限, (u, f) =>
|
|
604
|
+
帯で切る(t, 大きさ * k, 幅上限, (u, f) =>
|
|
605
|
+
textWidth(u, f, 字間 * k, textMetrics),
|
|
606
|
+
);
|
|
582
607
|
return (
|
|
583
608
|
<>
|
|
584
609
|
{node.eyebrow && plan.肩書 && (
|
|
@@ -628,7 +653,7 @@ function planLabel(node: LaidNode, cx: number, 幅上限: number, plan: ReturnTy
|
|
|
628
653
|
* `shapeRegion` で取り、 入らなければ `fitArt` が比を保ったまま縮める。 名前は箱の下端から
|
|
629
654
|
* 積み上げる (`labelPlan`) = 固定位置に置くと箱の下 176.2 まで伸びていた。
|
|
630
655
|
*/
|
|
631
|
-
export function ShapePersonNode({ node, active }: ShapeProps): JSX.Element {
|
|
656
|
+
export function ShapePersonNode({ node, active, textMetrics = "proportional" }: ShapeProps): JSX.Element {
|
|
632
657
|
const r = shapeRegion(node);
|
|
633
658
|
const cx = r.cx;
|
|
634
659
|
const strokeColor = active ? "var(--cdl-tone-accent, #2d6a8f)" : "var(--cdl-divider, #d4d4d8)";
|
|
@@ -698,7 +723,7 @@ export function ShapePersonNode({ node, active }: ShapeProps): JSX.Element {
|
|
|
698
723
|
/>
|
|
699
724
|
</g>,
|
|
700
725
|
)}
|
|
701
|
-
{planLabel(node, r.cx, Math.max(0, node.w), r.label)}
|
|
726
|
+
{planLabel(node, r.cx, Math.max(0, node.w), r.label, textMetrics)}
|
|
702
727
|
</g>
|
|
703
728
|
);
|
|
704
729
|
}
|