@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
|
@@ -3,12 +3,13 @@ import {
|
|
|
3
3
|
shapeRegion,
|
|
4
4
|
labelPlan,
|
|
5
5
|
fitArt,
|
|
6
|
+
textWidth,
|
|
6
7
|
打ち消しの倍率,
|
|
7
8
|
type 打ち消す字,
|
|
8
9
|
切って収まる下限,
|
|
9
10
|
帯で切る,
|
|
10
11
|
} from "./shape-region";
|
|
11
|
-
import type { LaidNode } from "../types";
|
|
12
|
+
import type { CdlTextMetrics, LaidNode } from "../types";
|
|
12
13
|
import { 字引きを作る, type CdlLang } from "../lang";
|
|
13
14
|
|
|
14
15
|
/**
|
|
@@ -26,6 +27,7 @@ import { 字引きを作る, type CdlLang } from "../lang";
|
|
|
26
27
|
type ShapeProps = {
|
|
27
28
|
node: LaidNode;
|
|
28
29
|
active: boolean;
|
|
30
|
+
textMetrics?: CdlTextMetrics;
|
|
29
31
|
/** 描画側の固定の字の言語 (#847)。 渡さない呼び方は英語で描く */
|
|
30
32
|
lang?: CdlLang;
|
|
31
33
|
};
|
|
@@ -81,7 +83,13 @@ function 字の比(c: string): number {
|
|
|
81
83
|
}
|
|
82
84
|
|
|
83
85
|
/** 字の並びの幅の見積り。 字ごとの比を足して、 字間を字数分足す。 */
|
|
84
|
-
function 字幅(
|
|
86
|
+
function 字幅(
|
|
87
|
+
文字列: string,
|
|
88
|
+
大きさ: number,
|
|
89
|
+
字間: number,
|
|
90
|
+
textMetrics: CdlTextMetrics = "proportional",
|
|
91
|
+
): number {
|
|
92
|
+
if (textMetrics === "monospace") return textWidth(文字列, 大きさ, 字間, textMetrics);
|
|
85
93
|
let 幅 = 0;
|
|
86
94
|
let 字数 = 0;
|
|
87
95
|
for (const c of 文字列) {
|
|
@@ -100,28 +108,55 @@ function 字幅(文字列: string, 大きさ: number, 字間: number): number {
|
|
|
100
108
|
*
|
|
101
109
|
* 字間も同じ割合で詰める = 大きさだけ縮めても字間が残ると幅が比例して縮まない。
|
|
102
110
|
*/
|
|
103
|
-
function 幅に収める(
|
|
104
|
-
|
|
111
|
+
function 幅に収める(
|
|
112
|
+
文字列: string,
|
|
113
|
+
大きさ: number,
|
|
114
|
+
字間: number,
|
|
115
|
+
箱幅: number,
|
|
116
|
+
題か = false,
|
|
117
|
+
textMetrics: CdlTextMetrics = "proportional",
|
|
118
|
+
) {
|
|
119
|
+
const 測る = (t: string, s: number, ls: number): number => 字幅(t, s, ls, textMetrics);
|
|
120
|
+
const 幅 = 測る(文字列, 大きさ, 字間);
|
|
105
121
|
// 縮めは読める下限で止める (#953)。 下限が無いと長い名前が点にしか見えない大きさになる。
|
|
106
122
|
// 止めた行は箱に入らないので、入るところまで切って `…` を付ける (#433 の決まりを保つ)。
|
|
107
123
|
//
|
|
108
124
|
// **下限を当てるのは題の行だけ** (#957)。 補いの行に当てると、収まっている行を
|
|
109
125
|
// 大きくしてから切ることになる = 書いた字を落として得るのが 1px に満たない
|
|
110
|
-
const 下限 = 題か ? 切って収まる下限(文字列, 大きさ, 字間, 箱幅,
|
|
126
|
+
const 下限 = 題か ? 切って収まる下限(文字列, 大きさ, 字間, 箱幅, 測る) : 0;
|
|
111
127
|
const 倍率 = 幅 > 0 && 箱幅 > 0 ? Math.min(1, Math.max(箱幅 / 幅, 下限)) : 1;
|
|
112
128
|
const size = 大きさ * 倍率;
|
|
113
129
|
const spacing = 字間 * 倍率;
|
|
114
|
-
return { size, spacing, text: 帯で切る(文字列, size, 箱幅, (u, f) =>
|
|
130
|
+
return { size, spacing, text: 帯で切る(文字列, size, 箱幅, (u, f) => 測る(u, f, spacing)) };
|
|
115
131
|
}
|
|
116
132
|
|
|
117
133
|
/**
|
|
118
134
|
* 名前の塊を描く。 縦の位置と大きさは `labelPlan` が決め (箱の下端から積み上げる、 #433)、
|
|
119
135
|
* 横は `幅に収める` が箱の幅に合わせて縮める。
|
|
120
136
|
*/
|
|
121
|
-
function bottomLabel(
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
137
|
+
function bottomLabel(
|
|
138
|
+
node: LaidNode,
|
|
139
|
+
cx: number,
|
|
140
|
+
plan: ReturnType<typeof labelPlan>,
|
|
141
|
+
textMetrics: CdlTextMetrics = "proportional",
|
|
142
|
+
) {
|
|
143
|
+
const 肩 = 幅に収める(
|
|
144
|
+
node.eyebrow ?? "",
|
|
145
|
+
plan.肩書?.size ?? 0,
|
|
146
|
+
LABEL_EYEBROW_SPACING,
|
|
147
|
+
node.w,
|
|
148
|
+
false,
|
|
149
|
+
textMetrics,
|
|
150
|
+
);
|
|
151
|
+
const 名 = 幅に収める(node.title ?? "", plan.名前.size, 0, node.w, true, textMetrics);
|
|
152
|
+
const 説 = 幅に収める(
|
|
153
|
+
node.subtitle ?? "",
|
|
154
|
+
plan.説明?.size ?? 0,
|
|
155
|
+
0,
|
|
156
|
+
node.w,
|
|
157
|
+
false,
|
|
158
|
+
textMetrics,
|
|
159
|
+
);
|
|
125
160
|
return (
|
|
126
161
|
<>
|
|
127
162
|
{node.eyebrow && plan.肩書 && (
|
|
@@ -174,7 +209,7 @@ const BANK_BLD_W_MIN = 120;
|
|
|
174
209
|
const BANK_BLD_W_MAX = 280;
|
|
175
210
|
|
|
176
211
|
/** shape-bank = 銀行 (Greek 神殿 pediment + $ + 4 columns + base、 中央配置) */
|
|
177
|
-
export function ShapeBankNode({ node, active }: ShapeProps): JSX.Element {
|
|
212
|
+
export function ShapeBankNode({ node, active, textMetrics = "proportional" }: ShapeProps): JSX.Element {
|
|
178
213
|
const frame = commonFrame(active);
|
|
179
214
|
const r = shapeRegion(node);
|
|
180
215
|
const 線 = frame.strokeWidth / 2;
|
|
@@ -231,7 +266,7 @@ export function ShapeBankNode({ node, active }: ShapeProps): JSX.Element {
|
|
|
231
266
|
return (
|
|
232
267
|
<g>
|
|
233
268
|
{fitArt(r, 広がり, 絵)}
|
|
234
|
-
{bottomLabel(node, r.cx, r.label)}
|
|
269
|
+
{bottomLabel(node, r.cx, r.label, textMetrics)}
|
|
235
270
|
</g>
|
|
236
271
|
);
|
|
237
272
|
}
|
|
@@ -252,7 +287,12 @@ const TRUST_BLD_W_MIN = 140;
|
|
|
252
287
|
const TRUST_BLD_W_MAX = 280;
|
|
253
288
|
|
|
254
289
|
/** shape-trust-bank = 信託銀行 (王冠 + 神殿 3 columns、 中央柱に余計装飾なし) */
|
|
255
|
-
export function ShapeTrustBankNode({
|
|
290
|
+
export function ShapeTrustBankNode({
|
|
291
|
+
node,
|
|
292
|
+
active,
|
|
293
|
+
lang,
|
|
294
|
+
textMetrics = "proportional",
|
|
295
|
+
}: ShapeProps): JSX.Element {
|
|
256
296
|
const 字引 = 字引きを作る(lang);
|
|
257
297
|
const frame = commonFrame(active);
|
|
258
298
|
const r = shapeRegion(node);
|
|
@@ -316,13 +356,18 @@ export function ShapeTrustBankNode({ node, active, lang }: ShapeProps): JSX.Elem
|
|
|
316
356
|
return (
|
|
317
357
|
<g>
|
|
318
358
|
{fitArt(r, 広がり, 絵)}
|
|
319
|
-
{bottomLabel(node, r.cx, r.label)}
|
|
359
|
+
{bottomLabel(node, r.cx, r.label, textMetrics)}
|
|
320
360
|
</g>
|
|
321
361
|
);
|
|
322
362
|
}
|
|
323
363
|
|
|
324
364
|
/** shape-payment-provider = P2-A base (中央 provider box + PAY header + processing spinner + PROCESSING label) */
|
|
325
|
-
export function ShapePaymentProviderNode({
|
|
365
|
+
export function ShapePaymentProviderNode({
|
|
366
|
+
node,
|
|
367
|
+
active,
|
|
368
|
+
lang,
|
|
369
|
+
textMetrics = "proportional",
|
|
370
|
+
}: ShapeProps): JSX.Element {
|
|
326
371
|
const 字引 = 字引きを作る(lang);
|
|
327
372
|
const frame = commonFrame(active);
|
|
328
373
|
const r = shapeRegion(node);
|
|
@@ -355,13 +400,17 @@ export function ShapePaymentProviderNode({ node, active, lang }: ShapeProps): JS
|
|
|
355
400
|
</g>
|
|
356
401
|
{/* PROCESSING label */}
|
|
357
402
|
<text x={cx} y={y + boxH - 16} fontSize={11} fontWeight={700} letterSpacing={2} textAnchor="middle" fill="#a4b5f2">{字引("paymentProvider.status")}</text>
|
|
358
|
-
{bottomLabel(node, r.cx, r.label)}
|
|
403
|
+
{bottomLabel(node, r.cx, r.label, textMetrics)}
|
|
359
404
|
</g>
|
|
360
405
|
);
|
|
361
406
|
}
|
|
362
407
|
|
|
363
408
|
/** shape-credit-card = クレジットカード (シンプル、 chip + NFC + 4 digit block + brand) */
|
|
364
|
-
export function ShapeCreditCardNode({
|
|
409
|
+
export function ShapeCreditCardNode({
|
|
410
|
+
node,
|
|
411
|
+
active,
|
|
412
|
+
textMetrics = "proportional",
|
|
413
|
+
}: ShapeProps): JSX.Element {
|
|
365
414
|
const frame = commonFrame(active);
|
|
366
415
|
const r = shapeRegion(node);
|
|
367
416
|
const cardW = Math.min(r.availW - 30, 260);
|
|
@@ -394,7 +443,7 @@ export function ShapeCreditCardNode({ node, active }: ShapeProps): JSX.Element {
|
|
|
394
443
|
{/* brand mark */}
|
|
395
444
|
<circle cx={x + cardW - 48} cy={y + cardH - 24} r={15} fill="#c9302c" opacity={0.9} stroke="none" />
|
|
396
445
|
<circle cx={x + cardW - 30} cy={y + cardH - 24} r={15} fill="#e5c07b" opacity={0.9} stroke="none" />
|
|
397
|
-
{bottomLabel(node, r.cx, r.label)}
|
|
446
|
+
{bottomLabel(node, r.cx, r.label, textMetrics)}
|
|
398
447
|
</g>
|
|
399
448
|
);
|
|
400
449
|
}
|
|
@@ -447,7 +496,12 @@ const BROKERAGE_W_MAX = 140;
|
|
|
447
496
|
const BROKERAGE_H_MIN = BROKERAGE_TICK_H + BROKERAGE_LOGO_H + BROKERAGE_BASE_H;
|
|
448
497
|
|
|
449
498
|
/** shape-brokerage = B2-A 大和証券風 (縦長本社 + 上部電光 + $ SECURITIES ロゴ帯 + window grid + entrance 回転扉) */
|
|
450
|
-
export function ShapeBrokerageNode({
|
|
499
|
+
export function ShapeBrokerageNode({
|
|
500
|
+
node,
|
|
501
|
+
active,
|
|
502
|
+
lang,
|
|
503
|
+
textMetrics = "proportional",
|
|
504
|
+
}: ShapeProps): JSX.Element {
|
|
451
505
|
const 字引 = 字引きを作る(lang);
|
|
452
506
|
const frame = commonFrame(active);
|
|
453
507
|
const r = shapeRegion(node);
|
|
@@ -494,7 +548,7 @@ export function ShapeBrokerageNode({ node, active, lang }: ShapeProps): JSX.Elem
|
|
|
494
548
|
},
|
|
495
549
|
];
|
|
496
550
|
const 絵 = (倍率: number): JSX.Element => {
|
|
497
|
-
const 倍 = 打ち消しの倍率(倍率,
|
|
551
|
+
const 倍 = 打ち消しの倍率(倍率, 打ち消す字ら, textMetrics);
|
|
498
552
|
return (
|
|
499
553
|
<g>
|
|
500
554
|
{/* drop shadow */}
|
|
@@ -538,7 +592,7 @@ export function ShapeBrokerageNode({ node, active, lang }: ShapeProps): JSX.Elem
|
|
|
538
592
|
return (
|
|
539
593
|
<g>
|
|
540
594
|
{fitArt(r, 広がり, 絵)}
|
|
541
|
-
{bottomLabel(node, r.cx, r.label)}
|
|
595
|
+
{bottomLabel(node, r.cx, r.label, textMetrics)}
|
|
542
596
|
</g>
|
|
543
597
|
);
|
|
544
598
|
}
|
|
@@ -549,7 +603,12 @@ const EXCHANGE_SUB_SIZE = 11;
|
|
|
549
603
|
/** 中央の六角形の外接半径。 看板の字 (指定 11、 字間 1 で幅 83.7) が入る大きさから決めた (#888)。 */
|
|
550
604
|
const EXCHANGE_HEX_R = 52;
|
|
551
605
|
|
|
552
|
-
export function ShapeExchangeNode({
|
|
606
|
+
export function ShapeExchangeNode({
|
|
607
|
+
node,
|
|
608
|
+
active,
|
|
609
|
+
lang,
|
|
610
|
+
textMetrics = "proportional",
|
|
611
|
+
}: ShapeProps): JSX.Element {
|
|
553
612
|
const 字引 = 字引きを作る(lang);
|
|
554
613
|
const frame = commonFrame(active);
|
|
555
614
|
const r = shapeRegion(node);
|
|
@@ -620,7 +679,7 @@ export function ShapeExchangeNode({ node, active, lang }: ShapeProps): JSX.Eleme
|
|
|
620
679
|
},
|
|
621
680
|
];
|
|
622
681
|
const 絵 = (倍率: number): JSX.Element => {
|
|
623
|
-
const 倍 = 打ち消しの倍率(倍率,
|
|
682
|
+
const 倍 = 打ち消しの倍率(倍率, 打ち消す字ら, textMetrics);
|
|
624
683
|
return (
|
|
625
684
|
<g>
|
|
626
685
|
{/* 8 dashed radial lines */}
|
|
@@ -654,7 +713,7 @@ export function ShapeExchangeNode({ node, active, lang }: ShapeProps): JSX.Eleme
|
|
|
654
713
|
return (
|
|
655
714
|
<g>
|
|
656
715
|
{fitArt(r, 広がり, 絵)}
|
|
657
|
-
{bottomLabel(node, r.cx, r.label)}
|
|
716
|
+
{bottomLabel(node, r.cx, r.label, textMetrics)}
|
|
658
717
|
</g>
|
|
659
718
|
);
|
|
660
719
|
}
|
|
@@ -700,7 +759,12 @@ const ATM_W_MAX = ATM_PANEL_W;
|
|
|
700
759
|
const ATM_H_MIN = ATM_SIGN_H + ATM_SIGN_GAP + 30 + ATM_FLOOR_RESERVE;
|
|
701
760
|
|
|
702
761
|
/** shape-atm = A3-C1 (支店内 ATM 単体、 上部 ATM CORNER 天井看板 + 中央 ATM 大 + 直下床帯) */
|
|
703
|
-
export function ShapeAtmNode({
|
|
762
|
+
export function ShapeAtmNode({
|
|
763
|
+
node,
|
|
764
|
+
active,
|
|
765
|
+
lang,
|
|
766
|
+
textMetrics = "proportional",
|
|
767
|
+
}: ShapeProps): JSX.Element {
|
|
704
768
|
const 字引 = 字引きを作る(lang);
|
|
705
769
|
const frame = commonFrame(active);
|
|
706
770
|
const r = shapeRegion(node);
|
|
@@ -800,7 +864,7 @@ export function ShapeAtmNode({ node, active, lang }: ShapeProps): JSX.Element {
|
|
|
800
864
|
return (
|
|
801
865
|
<g>
|
|
802
866
|
{fitArt(r, 広がり, 絵)}
|
|
803
|
-
{bottomLabel(node, r.cx, r.label)}
|
|
867
|
+
{bottomLabel(node, r.cx, r.label, textMetrics)}
|
|
804
868
|
</g>
|
|
805
869
|
);
|
|
806
870
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { JSX } from "react";
|
|
2
2
|
import { shapeRegion, labelPlan, fitArt, textWidth, 切って収まる下限, 帯で切る, type ArtExtent } from "./shape-region";
|
|
3
|
-
import type { LaidNode } from "../types";
|
|
3
|
+
import type { CdlTextMetrics, LaidNode } from "../types";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Shape-driven hardware 6 kinds (CAR-1111 Phase 2-C)。
|
|
@@ -45,6 +45,10 @@ type ShapeProps = {
|
|
|
45
45
|
active: boolean;
|
|
46
46
|
};
|
|
47
47
|
|
|
48
|
+
type TextMeasuredShapeProps = ShapeProps & {
|
|
49
|
+
textMetrics?: CdlTextMetrics;
|
|
50
|
+
};
|
|
51
|
+
|
|
48
52
|
const commonFrame = (active: boolean) => ({
|
|
49
53
|
fill: "var(--cdl-node-fill, #ffffff)",
|
|
50
54
|
stroke: active ? "var(--cdl-tone-accent, #2d6a8f)" : "var(--cdl-divider, #d4d4d8)",
|
|
@@ -104,18 +108,26 @@ const LABEL_EYEBROW_SPACING = 2;
|
|
|
104
108
|
* 字幅の見積りは共有の `textWidth` (実測を階級で丸め上げた表) が持つ。 字間も同じ割合で
|
|
105
109
|
* 詰める = 大きさだけ縮めても字間が残ると幅が比例して縮まない。
|
|
106
110
|
*/
|
|
107
|
-
function 幅に収める(
|
|
108
|
-
|
|
111
|
+
function 幅に収める(
|
|
112
|
+
文字列: string,
|
|
113
|
+
大きさ: number,
|
|
114
|
+
字間: number,
|
|
115
|
+
箱幅: number,
|
|
116
|
+
題か = false,
|
|
117
|
+
textMetrics: CdlTextMetrics = "proportional",
|
|
118
|
+
) {
|
|
119
|
+
const 測る = (t: string, s: number, ls: number): number => textWidth(t, s, ls, textMetrics);
|
|
120
|
+
const 幅 = 測る(文字列, 大きさ, 字間);
|
|
109
121
|
// 縮めは読める下限で止める (#953)。 下限が無いと長い名前が点にしか見えない大きさになる。
|
|
110
122
|
// 止めた行は箱に入らないので、入るところまで切って `…` を付ける (#433 の決まりを保つ)。
|
|
111
123
|
//
|
|
112
124
|
// **下限を当てるのは題の行だけ** (#957)。 補いの行に当てると、収まっている行を
|
|
113
125
|
// 大きくしてから切ることになる = 書いた字を落として得るのが 1px に満たない
|
|
114
|
-
const 下限 = 題か ? 切って収まる下限(文字列, 大きさ, 字間, 箱幅,
|
|
126
|
+
const 下限 = 題か ? 切って収まる下限(文字列, 大きさ, 字間, 箱幅, 測る) : 0;
|
|
115
127
|
const 倍率 = 幅 > 0 && 箱幅 > 0 ? Math.min(1, Math.max(箱幅 / 幅, 下限)) : 1;
|
|
116
128
|
const size = 大きさ * 倍率;
|
|
117
129
|
const spacing = 字間 * 倍率;
|
|
118
|
-
return { size, spacing, text: 帯で切る(文字列, size, 箱幅, (u, f) =>
|
|
130
|
+
return { size, spacing, text: 帯で切る(文字列, size, 箱幅, (u, f) => 測る(u, f, spacing)) };
|
|
119
131
|
}
|
|
120
132
|
|
|
121
133
|
/**
|
|
@@ -124,10 +136,29 @@ function 幅に収める(文字列: string, 大きさ: number, 字間: number,
|
|
|
124
136
|
*
|
|
125
137
|
* `externalLabel` と違い、 **箱の中に必ず収まる**。 字の大きさも箱の高さに追随する。
|
|
126
138
|
*/
|
|
127
|
-
function bottomLabel(
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
139
|
+
function bottomLabel(
|
|
140
|
+
node: LaidNode,
|
|
141
|
+
cx: number,
|
|
142
|
+
plan: ReturnType<typeof labelPlan>,
|
|
143
|
+
textMetrics: CdlTextMetrics = "proportional",
|
|
144
|
+
) {
|
|
145
|
+
const 肩 = 幅に収める(
|
|
146
|
+
node.eyebrow ?? "",
|
|
147
|
+
plan.肩書?.size ?? 0,
|
|
148
|
+
LABEL_EYEBROW_SPACING,
|
|
149
|
+
node.w,
|
|
150
|
+
false,
|
|
151
|
+
textMetrics,
|
|
152
|
+
);
|
|
153
|
+
const 名 = 幅に収める(node.title ?? "", plan.名前.size, 0, node.w, true, textMetrics);
|
|
154
|
+
const 説 = 幅に収める(
|
|
155
|
+
node.subtitle ?? "",
|
|
156
|
+
plan.説明?.size ?? 0,
|
|
157
|
+
0,
|
|
158
|
+
node.w,
|
|
159
|
+
false,
|
|
160
|
+
textMetrics,
|
|
161
|
+
);
|
|
131
162
|
return (
|
|
132
163
|
<>
|
|
133
164
|
{node.eyebrow && plan.肩書 && (
|
|
@@ -171,7 +202,7 @@ const RACK_H =
|
|
|
171
202
|
const RACK_W_MIN = 120;
|
|
172
203
|
|
|
173
204
|
/** shape-server-rack = 19 inch rack (top vent + 4 U rack mount server + power indicator) */
|
|
174
|
-
export function ShapeServerRackNode({ node, active }:
|
|
205
|
+
export function ShapeServerRackNode({ node, active, textMetrics = "proportional" }: TextMeasuredShapeProps): JSX.Element {
|
|
175
206
|
const r = shapeRegion(node);
|
|
176
207
|
const frame = commonFrame(active);
|
|
177
208
|
const 線 = frame.strokeWidth / 2;
|
|
@@ -260,7 +291,7 @@ export function ShapeServerRackNode({ node, active }: ShapeProps): JSX.Element {
|
|
|
260
291
|
})}
|
|
261
292
|
</g>
|
|
262
293
|
))}
|
|
263
|
-
{bottomLabel(node, r.cx, r.label)}
|
|
294
|
+
{bottomLabel(node, r.cx, r.label, textMetrics)}
|
|
264
295
|
</g>
|
|
265
296
|
);
|
|
266
297
|
}
|
|
@@ -281,7 +312,7 @@ const WIFI_ARC_R = [10, 16, 22];
|
|
|
281
312
|
const WIFI_BASE_Y = 5;
|
|
282
313
|
|
|
283
314
|
/** shape-network-node = 六角 hub (network signal cone icon + 6-arm radial + node dots) */
|
|
284
|
-
export function ShapeNetworkNodeNode({ node, active }:
|
|
315
|
+
export function ShapeNetworkNodeNode({ node, active, textMetrics = "proportional" }: TextMeasuredShapeProps): JSX.Element {
|
|
285
316
|
const r = shapeRegion(node);
|
|
286
317
|
const cx = r.cx;
|
|
287
318
|
const cy = r.cy;
|
|
@@ -358,7 +389,7 @@ export function ShapeNetworkNodeNode({ node, active }: ShapeProps): JSX.Element
|
|
|
358
389
|
<circle cx={cx} cy={cy + WIFI_BASE_Y + 3} r={2.5} fill="var(--cdl-tone-accent, #2d6a8f)" stroke="none" />
|
|
359
390
|
</g>
|
|
360
391
|
))}
|
|
361
|
-
{bottomLabel(node, r.cx, r.label)}
|
|
392
|
+
{bottomLabel(node, r.cx, r.label, textMetrics)}
|
|
362
393
|
</g>
|
|
363
394
|
);
|
|
364
395
|
}
|
|
@@ -378,7 +409,7 @@ const PHONE_H = 240;
|
|
|
378
409
|
const PHONE_W = 110;
|
|
379
410
|
|
|
380
411
|
/** shape-mobile-device = スマホ (rounded rect + speaker slit + home button) */
|
|
381
|
-
export function ShapeMobileDeviceNode({ node, active }:
|
|
412
|
+
export function ShapeMobileDeviceNode({ node, active, textMetrics = "proportional" }: TextMeasuredShapeProps): JSX.Element {
|
|
382
413
|
const r = shapeRegion(node);
|
|
383
414
|
const frame = commonFrame(active);
|
|
384
415
|
const 線 = frame.strokeWidth / 2;
|
|
@@ -436,7 +467,7 @@ export function ShapeMobileDeviceNode({ node, active }: ShapeProps): JSX.Element
|
|
|
436
467
|
/>
|
|
437
468
|
</g>
|
|
438
469
|
))}
|
|
439
|
-
{bottomLabel(node, r.cx, r.label)}
|
|
470
|
+
{bottomLabel(node, r.cx, r.label, textMetrics)}
|
|
440
471
|
</g>
|
|
441
472
|
);
|
|
442
473
|
}
|
|
@@ -692,7 +723,7 @@ const SAT_MAST_LEN = 22;
|
|
|
692
723
|
const SAT_MAST_TIP_R = 4;
|
|
693
724
|
|
|
694
725
|
/** shape-satellite = 人工衛星 (body + 左右 solar panel + dish antenna + orbit arc) */
|
|
695
|
-
export function ShapeSatelliteNode({ node, active }:
|
|
726
|
+
export function ShapeSatelliteNode({ node, active, textMetrics = "proportional" }: TextMeasuredShapeProps): JSX.Element {
|
|
696
727
|
const r = shapeRegion(node);
|
|
697
728
|
const cx = r.cx;
|
|
698
729
|
const cy = r.cy;
|
|
@@ -864,7 +895,7 @@ export function ShapeSatelliteNode({ node, active }: ShapeProps): JSX.Element {
|
|
|
864
895
|
</circle>
|
|
865
896
|
</g>
|
|
866
897
|
))}
|
|
867
|
-
{bottomLabel(node, r.cx, r.label)}
|
|
898
|
+
{bottomLabel(node, r.cx, r.label, textMetrics)}
|
|
868
899
|
</g>
|
|
869
900
|
);
|
|
870
901
|
}
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* では名前だけの時と同じ倍率 0.308 まで戻り、 3px の線を約 0.92px で描ける。
|
|
21
21
|
*/
|
|
22
22
|
import type { JSX } from "react";
|
|
23
|
-
import type { LaidNode, NodeKind } from "../types";
|
|
23
|
+
import type { CdlTextMetrics, LaidNode, NodeKind } from "../types";
|
|
24
24
|
import { MONO_ADVANCE_RATIO, displayWidth } from "./row-align";
|
|
25
25
|
import { textWidth } from "./text-width";
|
|
26
26
|
import { 幅で切る } from "./box-lines";
|
|
@@ -56,10 +56,11 @@ export function 幅の倍率(
|
|
|
56
56
|
size: number,
|
|
57
57
|
letterSpacing: number,
|
|
58
58
|
幅: number,
|
|
59
|
+
textMetrics: CdlTextMetrics = "proportional",
|
|
59
60
|
): number {
|
|
60
61
|
if (!(幅 > 0) || !(size > 0)) return 1;
|
|
61
62
|
// 階級表を二重に持つと、 片方を直した時にもう片方だけ古くなる。
|
|
62
|
-
const 実幅 = textWidth(text, size, letterSpacing);
|
|
63
|
+
const 実幅 = textWidth(text, size, letterSpacing, textMetrics);
|
|
63
64
|
return 実幅 > 幅 ? 幅 / 実幅 : 1;
|
|
64
65
|
}
|
|
65
66
|
|
|
@@ -684,10 +685,15 @@ const 帯の高さに対する上限 = 0.8;
|
|
|
684
685
|
* 「収まっていない物を収まっていると判定」 する向きに外れる (`row-align.ts` の
|
|
685
686
|
* `rowColumnWidth` と同じ考え方)。
|
|
686
687
|
*/
|
|
687
|
-
export function 絵の中の字の幅(
|
|
688
|
+
export function 絵の中の字の幅(
|
|
689
|
+
text: string,
|
|
690
|
+
size: number,
|
|
691
|
+
letterSpacing = 0,
|
|
692
|
+
textMetrics: CdlTextMetrics = "proportional",
|
|
693
|
+
): number {
|
|
688
694
|
return Math.max(
|
|
689
695
|
displayWidth(text) * size * MONO_ADVANCE_RATIO,
|
|
690
|
-
textWidth(text, size, letterSpacing),
|
|
696
|
+
textWidth(text, size, letterSpacing, textMetrics),
|
|
691
697
|
);
|
|
692
698
|
}
|
|
693
699
|
|
|
@@ -715,7 +721,11 @@ export function 絵の中の字の幅(text: string, size: number, letterSpacing
|
|
|
715
721
|
* 帯の値が有限の正の値でない絵は、収まるかの判定ができない。
|
|
716
722
|
* `fitArt` が判定できない入力で手を触れないのと同じ向きに倒す。
|
|
717
723
|
*/
|
|
718
|
-
export function 打ち消しの倍率(
|
|
724
|
+
export function 打ち消しの倍率(
|
|
725
|
+
倍率: number,
|
|
726
|
+
字ら: readonly 打ち消す字[],
|
|
727
|
+
textMetrics: CdlTextMetrics = "proportional",
|
|
728
|
+
): number {
|
|
719
729
|
const 戻し = Number.isFinite(倍率) && 倍率 > 0 ? 1 / 倍率 : 1;
|
|
720
730
|
let 上限 = Number.POSITIVE_INFINITY;
|
|
721
731
|
for (const 字 of 字ら) {
|
|
@@ -724,7 +734,7 @@ export function 打ち消しの倍率(倍率: number, 字ら: readonly 打ち消
|
|
|
724
734
|
if (Number.isFinite(高さ) && 高さ > 0) {
|
|
725
735
|
上限 = Math.min(上限, (高さ * 帯の高さに対する上限) / 字.size);
|
|
726
736
|
}
|
|
727
|
-
const 実幅 = 絵の中の字の幅(字.text, 字.size, 字.letterSpacing ?? 0);
|
|
737
|
+
const 実幅 = 絵の中の字の幅(字.text, 字.size, 字.letterSpacing ?? 0, textMetrics);
|
|
728
738
|
if (Number.isFinite(幅) && 幅 > 0 && 実幅 > 0) {
|
|
729
739
|
上限 = Math.min(上限, 幅 / 実幅);
|
|
730
740
|
}
|
|
@@ -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 { 字引きを作る, type CdlLang } from "../lang";
|
|
4
4
|
import {
|
|
5
5
|
textWidth,
|
|
@@ -50,6 +50,10 @@ type ShapeProps = {
|
|
|
50
50
|
lang?: CdlLang;
|
|
51
51
|
};
|
|
52
52
|
|
|
53
|
+
type TextMeasuredShapeProps = ShapeProps & {
|
|
54
|
+
textMetrics?: CdlTextMetrics;
|
|
55
|
+
};
|
|
56
|
+
|
|
53
57
|
const commonFrame = (active: boolean) => ({
|
|
54
58
|
fill: "var(--cdl-node-fill, #ffffff)",
|
|
55
59
|
stroke: active ? "var(--cdl-tone-accent, #2d6a8f)" : "var(--cdl-divider, #d4d4d8)",
|
|
@@ -345,7 +349,12 @@ export function ShapeTerminalNode({ node, active, lang }: ShapeProps): JSX.Eleme
|
|
|
345
349
|
}
|
|
346
350
|
|
|
347
351
|
/** shape-code-block = code editor (tab + gutter + 4 syntax-colored stub lines) */
|
|
348
|
-
export function ShapeCodeBlockNode({
|
|
352
|
+
export function ShapeCodeBlockNode({
|
|
353
|
+
node,
|
|
354
|
+
active,
|
|
355
|
+
lang,
|
|
356
|
+
textMetrics = "proportional",
|
|
357
|
+
}: TextMeasuredShapeProps): JSX.Element {
|
|
349
358
|
const 字引 = 字引きを作る(lang);
|
|
350
359
|
const frame = commonFrame(active);
|
|
351
360
|
const 線 = frame.strokeWidth / 2;
|
|
@@ -364,7 +373,10 @@ export function ShapeCodeBlockNode({ node, active, lang }: ShapeProps): JSX.Elem
|
|
|
364
373
|
* 固定のままだと、控えより長い値を渡した図で字が札からはみ出す。 本体の幅を上限に置くのは、
|
|
365
374
|
* 札が本体より広くなると `path` で描く角丸が本体の右端を越えるため。
|
|
366
375
|
*/
|
|
367
|
-
const タブ幅 = Math.min(
|
|
376
|
+
const タブ幅 = Math.min(
|
|
377
|
+
Math.max(100, textWidth(札, 札の字, 0, textMetrics) + 24),
|
|
378
|
+
Math.max(0, w),
|
|
379
|
+
);
|
|
368
380
|
const タブ高 = 24;
|
|
369
381
|
const 角 = 10;
|
|
370
382
|
const 溝 = 32;
|
|
@@ -422,7 +434,7 @@ export function ShapeCodeBlockNode({ node, active, lang }: ShapeProps): JSX.Elem
|
|
|
422
434
|
{ text: 札, size: 札の字, 帯: { 幅: タブ幅, 高さ: タブ高 } },
|
|
423
435
|
{ text: "1", size: 行番号の字, 帯: { 幅: 溝 - 持ち上げ, 高さ: 行間 } },
|
|
424
436
|
];
|
|
425
|
-
const 倍返し = 打ち消しの倍率(倍,
|
|
437
|
+
const 倍返し = 打ち消しの倍率(倍, 打ち消す字ら, textMetrics);
|
|
426
438
|
const tabW = タブ幅 * 倍;
|
|
427
439
|
const tabH = タブ高 * 倍;
|
|
428
440
|
const 丸 = 角 * 倍;
|
|
@@ -456,7 +468,12 @@ export function ShapeCodeBlockNode({ node, active, lang }: ShapeProps): JSX.Elem
|
|
|
456
468
|
data-cdl-counter-scaled="1"
|
|
457
469
|
x={x + tabW / 2}
|
|
458
470
|
y={y + tabH / 2 + 4 * 倍}
|
|
459
|
-
fontSize={
|
|
471
|
+
fontSize={
|
|
472
|
+
札の字 *
|
|
473
|
+
倍 *
|
|
474
|
+
倍返し *
|
|
475
|
+
幅の倍率(札, 札の字 * 倍 * 倍返し, 0, tabW, textMetrics)
|
|
476
|
+
}
|
|
460
477
|
fontFamily={lineFont}
|
|
461
478
|
textAnchor="middle"
|
|
462
479
|
fill="#e6edf3"
|
|
@@ -538,7 +555,12 @@ export function ShapeCodeBlockNode({ node, active, lang }: ShapeProps): JSX.Elem
|
|
|
538
555
|
}
|
|
539
556
|
|
|
540
557
|
/** shape-kanban-card = kanban ticket (top color bar + 3 stub line) */
|
|
541
|
-
export function ShapeKanbanCardNode({
|
|
558
|
+
export function ShapeKanbanCardNode({
|
|
559
|
+
node,
|
|
560
|
+
active,
|
|
561
|
+
lang,
|
|
562
|
+
textMetrics = "proportional",
|
|
563
|
+
}: TextMeasuredShapeProps): JSX.Element {
|
|
542
564
|
const 字引 = 字引きを作る(lang);
|
|
543
565
|
const frame = commonFrame(active);
|
|
544
566
|
const 線 = frame.strokeWidth / 2;
|
|
@@ -628,7 +650,7 @@ export function ShapeKanbanCardNode({ node, active, lang }: ShapeProps): JSX.Ele
|
|
|
628
650
|
* = 書いてもいない語が図の一部として読める状態だった (見本帳で 3 件)。
|
|
629
651
|
*/
|
|
630
652
|
const 状態 = 書いた値か控え(node.eyebrow, 字引("kanbanCard.status"));
|
|
631
|
-
const 状態の倍率 = 幅の倍率(状態, PILL字, PILL字間, PILL
|
|
653
|
+
const 状態の倍率 = 幅の倍率(状態, PILL字, PILL字間, PILL字幅, textMetrics);
|
|
632
654
|
const 説明の字 = 名前.size * 説明字比;
|
|
633
655
|
// 札の内側の幅。 左右の余白は札と同じ倍率で縮むので、倍率を掛けてから引く
|
|
634
656
|
const 使える幅 = Math.max(0, w - 左右余白 * 倍 * 2);
|
|
@@ -707,7 +729,7 @@ export function ShapeKanbanCardNode({ node, active, lang }: ShapeProps): JSX.Ele
|
|
|
707
729
|
<text
|
|
708
730
|
x={x + w / 2}
|
|
709
731
|
y={名前.y + 名前.size * 説明基準比}
|
|
710
|
-
fontSize={説明の字 * 幅の倍率(説明, 説明の字, 0,
|
|
732
|
+
fontSize={説明の字 * 幅の倍率(説明, 説明の字, 0, 使える幅, textMetrics)}
|
|
711
733
|
textAnchor="middle"
|
|
712
734
|
fill="var(--cdl-text-dim, #5a6270)"
|
|
713
735
|
>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { JSX } from "react";
|
|
2
|
+
|
|
3
|
+
import type { LaidNode } from "../types";
|
|
4
|
+
import { renderShapeWithOutsideLabels } from "./dyn-shape";
|
|
5
|
+
import { nodeAccent } from "./node-tone";
|
|
6
|
+
|
|
7
|
+
const STATION_STROKE_ACTIVE = 4;
|
|
8
|
+
const STATION_STROKE_IDLE = 3;
|
|
9
|
+
const STATION_DEFAULT = "var(--cdl-tone-accent, #2d6a8f)";
|
|
10
|
+
|
|
11
|
+
/** 路線図の線路上に置く駅。名前と説明は丸を隠さないよう外側に置く。 */
|
|
12
|
+
export function StationNode({ node, active }: { node: LaidNode; active: boolean }): JSX.Element {
|
|
13
|
+
const strokeWidth = active ? STATION_STROKE_ACTIVE : STATION_STROKE_IDLE;
|
|
14
|
+
// 線は箱の辺で止まるため、枠線の外縁を箱の辺へ合わせると線路と丸の間が空かない。
|
|
15
|
+
const radius = Math.max(0, Math.min(node.w, node.h) / 2 - strokeWidth / 2);
|
|
16
|
+
const 形 = (
|
|
17
|
+
<circle
|
|
18
|
+
data-cdl-role="node-body"
|
|
19
|
+
data-cdl-mark="station"
|
|
20
|
+
cx={node.cx}
|
|
21
|
+
cy={node.cy}
|
|
22
|
+
r={radius}
|
|
23
|
+
fill="var(--cdl-node-fill, #ffffff)"
|
|
24
|
+
stroke={nodeAccent(node, STATION_DEFAULT)}
|
|
25
|
+
strokeWidth={strokeWidth}
|
|
26
|
+
/>
|
|
27
|
+
);
|
|
28
|
+
return renderShapeWithOutsideLabels(形, node, "middle");
|
|
29
|
+
}
|
package/src/kinds/text-width.ts
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* (`row-align.ts`) の両方が使うが、 前者から取ると React まで引き込まれる。 測定の検査で
|
|
6
6
|
* 読込みの時間が計測に乗り、 図の走査時間の予算 (100ms) を超えた (実測 = 226ms)。
|
|
7
7
|
*/
|
|
8
|
+
import type { CdlTextMetrics } from "../types";
|
|
9
|
+
import { measureAdvanceWidth } from "../layout/text-width";
|
|
8
10
|
|
|
9
11
|
/**
|
|
10
12
|
* 字 1 つが字の大きさに対して占める横幅。
|
|
@@ -40,8 +42,8 @@
|
|
|
40
42
|
* ASCII の内側は分類を残す。 ここは `W` 1.038 と `;` 0.343 で 3 倍開いており、 最大に寄せると
|
|
41
43
|
* 普通の名前が 1.5 倍の見積りになって過剰に縮む。
|
|
42
44
|
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
+
* `textMetrics: "monospace"` の図は JetBrains Mono の実測値を持つ `fontFamily: "mono"` の
|
|
46
|
+
* 経路で測る。 比例幅の図はこの階級表を使い、従来の見積りを保つ。
|
|
45
47
|
*/
|
|
46
48
|
const WIDE_RATIO = 1.04;
|
|
47
49
|
const NARROW_RATIO = 0.37;
|
|
@@ -58,7 +60,19 @@ const NARROW_CHARS = "ijlI.,:;'! ";
|
|
|
58
60
|
* `字送り` (`letter-spacing`) は字ごとに加わるため字数を掛ける。 最後の 1 字ぶんは実装に
|
|
59
61
|
* よって付かないが、 広い側に外す方を採る。
|
|
60
62
|
*/
|
|
61
|
-
export function textWidth(
|
|
63
|
+
export function textWidth(
|
|
64
|
+
s: string,
|
|
65
|
+
size: number,
|
|
66
|
+
字送り = 0,
|
|
67
|
+
textMetrics: CdlTextMetrics = "proportional",
|
|
68
|
+
): number {
|
|
69
|
+
if (textMetrics === "monospace") {
|
|
70
|
+
return measureAdvanceWidth(s, {
|
|
71
|
+
fontSize: size,
|
|
72
|
+
letterSpacing: 字送り,
|
|
73
|
+
fontFamily: "mono",
|
|
74
|
+
});
|
|
75
|
+
}
|
|
62
76
|
let 幅 = 0;
|
|
63
77
|
let 字数 = 0;
|
|
64
78
|
for (const c of s) {
|