@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
|
@@ -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
|
fitArt,
|
|
5
5
|
labelPlan,
|
|
@@ -27,6 +27,7 @@ import { 字引きを作る, type CdlLang, type CdlTextKey } from "../lang";
|
|
|
27
27
|
type ShapeProps = {
|
|
28
28
|
node: LaidNode;
|
|
29
29
|
active: boolean;
|
|
30
|
+
textMetrics?: CdlTextMetrics;
|
|
30
31
|
/** 描画側の固定の字の言語 (#847)。 渡さない呼び方は英語で描く */
|
|
31
32
|
lang?: CdlLang;
|
|
32
33
|
};
|
|
@@ -136,7 +137,12 @@ function blockchainNodeExtent(strokeWidth: number): ArtExtent {
|
|
|
136
137
|
/**
|
|
137
138
|
* 名前の塊を描く。 位置と大きさは `labelPlan` が決める (箱の下端から積み上げる、 #433)。
|
|
138
139
|
*/
|
|
139
|
-
function bottomLabel(
|
|
140
|
+
function bottomLabel(
|
|
141
|
+
node: LaidNode,
|
|
142
|
+
cx: number,
|
|
143
|
+
plan: ReturnType<typeof labelPlan>,
|
|
144
|
+
textMetrics: CdlTextMetrics = "proportional",
|
|
145
|
+
) {
|
|
140
146
|
// 縮めは読める下限で止める (#953)。 止めた行は箱に入らないので、入るところまで切る。
|
|
141
147
|
// 下限を当てるのは題の行だけ (#957) = 補いの行に当てると、収まっている行を大きくしてから
|
|
142
148
|
// 切ることになる
|
|
@@ -144,7 +150,8 @@ function bottomLabel(node: LaidNode, cx: number, plan: ReturnType<typeof labelPl
|
|
|
144
150
|
// **縮める時と切る時で同じ測り方を使う** (#959)。 絵の中の字は等幅の字送りと字形の
|
|
145
151
|
// 大きい方を採る (`絵の中の字の幅`) ので、`幅の倍率` の `textWidth` で倍率を出すと
|
|
146
152
|
// 「収まる」 と言った行を切る側が「はみ出す」 と判定する (実測 = 幅 140 の箱で 9 種)
|
|
147
|
-
const 測る = (u: string, f: number, ls: number) =>
|
|
153
|
+
const 測る = (u: string, f: number, ls: number) =>
|
|
154
|
+
字の幅(u, f, textMetrics) + ls * [...u].length;
|
|
148
155
|
const 素の幅 = 測る(t, 大きさ, 字間);
|
|
149
156
|
const 収まる = 素の幅 > node.w && node.w > 0 ? node.w / 素の幅 : 1;
|
|
150
157
|
const 下限 = 題か ? 切って収まる下限(t, 大きさ, 字間, node.w, 測る) : 0;
|
|
@@ -205,7 +212,12 @@ function EthereumDiamond({ cx, cy, h, colors }: { cx: number; cy: number; h: num
|
|
|
205
212
|
}
|
|
206
213
|
|
|
207
214
|
/** shape-smart-contract = 契約書 (前バージョン: document + 契約書テキスト + function 表示 + 底に回転 gear) */
|
|
208
|
-
export function ShapeSmartContractNode({
|
|
215
|
+
export function ShapeSmartContractNode({
|
|
216
|
+
node,
|
|
217
|
+
active,
|
|
218
|
+
lang,
|
|
219
|
+
textMetrics = "proportional",
|
|
220
|
+
}: ShapeProps): JSX.Element {
|
|
209
221
|
const 字引 = 字引きを作る(lang);
|
|
210
222
|
const frame = commonFrame(active);
|
|
211
223
|
const r = shapeRegion(node);
|
|
@@ -254,7 +266,7 @@ export function ShapeSmartContractNode({ node, active, lang }: ShapeProps): JSX.
|
|
|
254
266
|
},
|
|
255
267
|
];
|
|
256
268
|
const art = (倍率: number): JSX.Element => {
|
|
257
|
-
const 倍 = 打ち消しの倍率(倍率,
|
|
269
|
+
const 倍 = 打ち消しの倍率(倍率, 打ち消す字ら, textMetrics);
|
|
258
270
|
return (
|
|
259
271
|
<g>
|
|
260
272
|
<path data-cdl-role="node-body" d={path} {...frame} strokeLinejoin="round" />
|
|
@@ -284,7 +296,7 @@ export function ShapeSmartContractNode({ node, active, lang }: ShapeProps): JSX.
|
|
|
284
296
|
return (
|
|
285
297
|
<g>
|
|
286
298
|
{fitArt(r, smartContractExtent(frame.strokeWidth), art)}
|
|
287
|
-
{bottomLabel(node, r.cx, r.label)}
|
|
299
|
+
{bottomLabel(node, r.cx, r.label, textMetrics)}
|
|
288
300
|
</g>
|
|
289
301
|
);
|
|
290
302
|
}
|
|
@@ -300,7 +312,11 @@ export function ShapeSmartContractNode({ node, active, lang }: ShapeProps): JSX.
|
|
|
300
312
|
* どちらの書体で描かれても実物以上に出る側へ倒す = 下回ると「収まっていない物を収まっていると
|
|
301
313
|
* 判定」 する向きに外れる (`row-align.ts` の `rowColumnWidth` と同じ考え方)。
|
|
302
314
|
*/
|
|
303
|
-
const 字の幅 = (
|
|
315
|
+
const 字の幅 = (
|
|
316
|
+
text: string,
|
|
317
|
+
size: number,
|
|
318
|
+
textMetrics: CdlTextMetrics = "proportional",
|
|
319
|
+
): number => 絵の中の字の幅(text, size, 0, textMetrics);
|
|
304
320
|
|
|
305
321
|
/**
|
|
306
322
|
* 絵の中の 5 行の既定値。 書き手が渡さなかった欄はこれで描く (#469)。
|
|
@@ -353,10 +369,16 @@ const BLOCK_DEFAULT_FIELDS: readonly {
|
|
|
353
369
|
* 位置が node の大きさに依存する。 `rowBaselineY(kind, index)` は node の大きさを受け取らない
|
|
354
370
|
* ので、 この種別の位置を表せない。 開くなら述語の形から変える話になる (#469 の対象外 scope)。
|
|
355
371
|
*/
|
|
356
|
-
export function ShapeBlockchainBlockNode({
|
|
372
|
+
export function ShapeBlockchainBlockNode({
|
|
373
|
+
node,
|
|
374
|
+
active,
|
|
375
|
+
lang,
|
|
376
|
+
textMetrics = "proportional",
|
|
377
|
+
}: ShapeProps): JSX.Element {
|
|
357
378
|
const 字引 = 字引きを作る(lang);
|
|
358
379
|
const frame = commonFrame(active);
|
|
359
380
|
const r = shapeRegion(node);
|
|
381
|
+
const 測る字 = (text: string, size: number): number => 字の幅(text, size, textMetrics);
|
|
360
382
|
const bw = BLOCK_W;
|
|
361
383
|
const bh = BLOCK_H;
|
|
362
384
|
const cx = r.cx;
|
|
@@ -392,9 +414,10 @@ export function ShapeBlockchainBlockNode({ node, active, lang }: ShapeProps): JS
|
|
|
392
414
|
// 上限は帯の残りの半分。 長い肩書は **切らずに字を縮める** = 分類を表す語なので、
|
|
393
415
|
// 末尾を落とすと何の分類か読めなくなる (`台帳…`)。 縮めた後の幅は上限ちょうどになる
|
|
394
416
|
const 肩書の上限 = (帯の残り - 肩書の隙間) / 2;
|
|
395
|
-
const 肩書の倍率 =
|
|
417
|
+
const 肩書の倍率 =
|
|
418
|
+
肩書 === "" ? 1 : 幅の倍率(肩書, 肩書の字, 0, 肩書の上限, textMetrics);
|
|
396
419
|
const 肩書の指定 = 肩書の字 * 肩書の倍率;
|
|
397
|
-
const 肩書の幅 = 肩書 === "" ? 0 :
|
|
420
|
+
const 肩書の幅 = 肩書 === "" ? 0 : 測る字(肩書, 肩書の指定);
|
|
398
421
|
const 見出しの幅 = 帯の残り - (肩書 === "" ? 0 : 肩書の幅 + 肩書の隙間);
|
|
399
422
|
/*
|
|
400
423
|
* 絵を **倍率を受け取ってから** 組む (#871)。 縮んだ分を字の指定で打ち消す。
|
|
@@ -422,12 +445,12 @@ export function ShapeBlockchainBlockNode({ node, active, lang }: ShapeProps): JS
|
|
|
422
445
|
{
|
|
423
446
|
text: f.value,
|
|
424
447
|
size: 値の字,
|
|
425
|
-
帯: { 幅: bw - 28 -
|
|
448
|
+
帯: { 幅: bw - 28 - 測る字(f.label, 名の字) - 8, 高さ: 行の間隔 },
|
|
426
449
|
},
|
|
427
450
|
]),
|
|
428
451
|
];
|
|
429
452
|
const art = (倍率: number): JSX.Element => {
|
|
430
|
-
const 倍 = 打ち消しの倍率(倍率,
|
|
453
|
+
const 倍 = 打ち消しの倍率(倍率, 打ち消す字ら, textMetrics);
|
|
431
454
|
const 題の大きさ = 見出しの字 * 倍;
|
|
432
455
|
const 肩書の大きさ = 肩書の指定 * 倍;
|
|
433
456
|
const 名の大きさ = 名の字 * 倍;
|
|
@@ -440,7 +463,7 @@ export function ShapeBlockchainBlockNode({ node, active, lang }: ShapeProps): JS
|
|
|
440
463
|
{/* header bar */}
|
|
441
464
|
<path d={`M ${x} ${y + 32} L ${x} ${y + 8} Q ${x} ${y}, ${x + 8} ${y} L ${x + bw - 8} ${y} Q ${x + bw} ${y}, ${x + bw} ${y + 8} L ${x + bw} ${y + 32} Z`} fill="var(--cdl-tone-accent, #2d6a8f)" fillOpacity={0.9} stroke="none" />
|
|
442
465
|
<text data-cdl-role="blockchain-block-title" data-cdl-counter-scaled="1" x={x + 14} y={y + 22} fontSize={題の大きさ} fontWeight={700} fontFamily="ui-monospace, SFMono-Regular, Menlo, monospace" fill="#ffffff">
|
|
443
|
-
{幅で切る(見出し, 題の大きさ, 見出しの幅,
|
|
466
|
+
{幅で切る(見出し, 題の大きさ, 見出しの幅, 測る字)}
|
|
444
467
|
</text>
|
|
445
468
|
{肩書 !== "" && (
|
|
446
469
|
<text
|
|
@@ -466,7 +489,12 @@ export function ShapeBlockchainBlockNode({ node, active, lang }: ShapeProps): JS
|
|
|
466
489
|
{f.label}
|
|
467
490
|
</text>
|
|
468
491
|
<text data-cdl-role={`blockchain-block-${f.key}`} data-cdl-counter-scaled="1" x={x + bw - 14} y={y + 54 + i * 行の間隔} fontSize={値の大きさ} fontFamily="ui-monospace, SFMono-Regular, Menlo, monospace" fontWeight={600} textAnchor="end" fill="var(--cdl-text, #1a1f2a)">
|
|
469
|
-
{幅で切る(
|
|
492
|
+
{幅で切る(
|
|
493
|
+
f.value,
|
|
494
|
+
値の大きさ,
|
|
495
|
+
bw - 28 - 測る字(f.label, 名の大きさ) - 8,
|
|
496
|
+
測る字,
|
|
497
|
+
)}
|
|
470
498
|
</text>
|
|
471
499
|
<line x1={x + 14} y1={y + 60 + i * 行の間隔} x2={x + bw - 14} y2={y + 60 + i * 行の間隔} stroke="var(--cdl-divider, #d4d4d8)" strokeWidth={0.5} opacity={0.5} />
|
|
472
500
|
</g>
|
|
@@ -484,7 +512,11 @@ export function ShapeBlockchainBlockNode({ node, active, lang }: ShapeProps): JS
|
|
|
484
512
|
}
|
|
485
513
|
|
|
486
514
|
/** shape-blockchain = 汎用 5 block chain (シンプル hash pointer) */
|
|
487
|
-
export function ShapeBlockchainNode({
|
|
515
|
+
export function ShapeBlockchainNode({
|
|
516
|
+
node,
|
|
517
|
+
active,
|
|
518
|
+
textMetrics = "proportional",
|
|
519
|
+
}: ShapeProps): JSX.Element {
|
|
488
520
|
const frame = commonFrame(active);
|
|
489
521
|
const r = shapeRegion(node);
|
|
490
522
|
const cx = r.cx;
|
|
@@ -506,7 +538,7 @@ export function ShapeBlockchainNode({ node, active }: ShapeProps): JSX.Element {
|
|
|
506
538
|
{ text: "0x…", size: CHAIN_HASH_SIZE, 帯: { 幅: CHAIN_TEXT_W, 高さ: CHAIN_ROW_GAP } },
|
|
507
539
|
];
|
|
508
540
|
const art = (倍率: number): JSX.Element => {
|
|
509
|
-
const 倍 = 打ち消しの倍率(倍率,
|
|
541
|
+
const 倍 = 打ち消しの倍率(倍率, 打ち消す字ら, textMetrics);
|
|
510
542
|
return (
|
|
511
543
|
<g>
|
|
512
544
|
{Array.from({ length: blockCount }, (_, i) => i).map((i) => {
|
|
@@ -559,13 +591,17 @@ export function ShapeBlockchainNode({ node, active }: ShapeProps): JSX.Element {
|
|
|
559
591
|
return (
|
|
560
592
|
<g>
|
|
561
593
|
{fitArt(r, chainExtent(), art)}
|
|
562
|
-
{bottomLabel(node, r.cx, r.label)}
|
|
594
|
+
{bottomLabel(node, r.cx, r.label, textMetrics)}
|
|
563
595
|
</g>
|
|
564
596
|
);
|
|
565
597
|
}
|
|
566
598
|
|
|
567
599
|
/** shape-bitcoin-chain = Bitcoin (橙 accent + ₿ symbol) */
|
|
568
|
-
export function ShapeBitcoinChainNode({
|
|
600
|
+
export function ShapeBitcoinChainNode({
|
|
601
|
+
node,
|
|
602
|
+
active,
|
|
603
|
+
textMetrics = "proportional",
|
|
604
|
+
}: ShapeProps): JSX.Element {
|
|
569
605
|
const frame = commonFrame(active);
|
|
570
606
|
const r = shapeRegion(node);
|
|
571
607
|
const cx = r.cx;
|
|
@@ -586,7 +622,7 @@ export function ShapeBitcoinChainNode({ node, active }: ShapeProps): JSX.Element
|
|
|
586
622
|
{ text: "#820", size: BTC_NO_SIZE, 帯: { 幅: CHAIN_TEXT_W, 高さ: 20 } },
|
|
587
623
|
];
|
|
588
624
|
const art = (倍率: number): JSX.Element => {
|
|
589
|
-
const 倍 = 打ち消しの倍率(倍率,
|
|
625
|
+
const 倍 = 打ち消しの倍率(倍率, 打ち消す字ら, textMetrics);
|
|
590
626
|
return (
|
|
591
627
|
<g>
|
|
592
628
|
{Array.from({ length: blockCount }, (_, i) => i).map((i) => {
|
|
@@ -622,13 +658,17 @@ export function ShapeBitcoinChainNode({ node, active }: ShapeProps): JSX.Element
|
|
|
622
658
|
return (
|
|
623
659
|
<g>
|
|
624
660
|
{fitArt(r, chainExtent(), art)}
|
|
625
|
-
{bottomLabel(node, r.cx, r.label)}
|
|
661
|
+
{bottomLabel(node, r.cx, r.label, textMetrics)}
|
|
626
662
|
</g>
|
|
627
663
|
);
|
|
628
664
|
}
|
|
629
665
|
|
|
630
666
|
/** shape-ethereum-chain = Ethereum (紫 accent + 本物 Ethereum Diamond ロゴ) */
|
|
631
|
-
export function ShapeEthereumChainNode({
|
|
667
|
+
export function ShapeEthereumChainNode({
|
|
668
|
+
node,
|
|
669
|
+
active,
|
|
670
|
+
textMetrics = "proportional",
|
|
671
|
+
}: ShapeProps): JSX.Element {
|
|
632
672
|
const frame = commonFrame(active);
|
|
633
673
|
const r = shapeRegion(node);
|
|
634
674
|
const cx = r.cx;
|
|
@@ -653,7 +693,7 @@ export function ShapeEthereumChainNode({ node, active }: ShapeProps): JSX.Elemen
|
|
|
653
693
|
{ text: "#214", size: ETH_NO_SIZE, 帯: { 幅: CHAIN_TEXT_W, 高さ: 20 } },
|
|
654
694
|
];
|
|
655
695
|
const art = (倍率: number): JSX.Element => {
|
|
656
|
-
const 倍 = 打ち消しの倍率(倍率,
|
|
696
|
+
const 倍 = 打ち消しの倍率(倍率, 打ち消す字ら, textMetrics);
|
|
657
697
|
return (
|
|
658
698
|
<g>
|
|
659
699
|
{Array.from({ length: blockCount }, (_, i) => i).map((i) => {
|
|
@@ -691,13 +731,17 @@ export function ShapeEthereumChainNode({ node, active }: ShapeProps): JSX.Elemen
|
|
|
691
731
|
return (
|
|
692
732
|
<g>
|
|
693
733
|
{fitArt(r, chainExtent(), art)}
|
|
694
|
-
{bottomLabel(node, r.cx, r.label)}
|
|
734
|
+
{bottomLabel(node, r.cx, r.label, textMetrics)}
|
|
695
735
|
</g>
|
|
696
736
|
);
|
|
697
737
|
}
|
|
698
738
|
|
|
699
739
|
/** shape-blockchain-node = P2P full node (中央 hex + 6 peer 外側配置) */
|
|
700
|
-
export function ShapeBlockchainNodeNode({
|
|
740
|
+
export function ShapeBlockchainNodeNode({
|
|
741
|
+
node,
|
|
742
|
+
active,
|
|
743
|
+
textMetrics = "proportional",
|
|
744
|
+
}: ShapeProps): JSX.Element {
|
|
701
745
|
const frame = commonFrame(active);
|
|
702
746
|
const r = shapeRegion(node);
|
|
703
747
|
const cx = r.cx;
|
|
@@ -752,13 +796,18 @@ export function ShapeBlockchainNodeNode({ node, active }: ShapeProps): JSX.Eleme
|
|
|
752
796
|
return (
|
|
753
797
|
<g>
|
|
754
798
|
{fitArt(r, blockchainNodeExtent(frame.strokeWidth), art)}
|
|
755
|
-
{bottomLabel(node, r.cx, r.label)}
|
|
799
|
+
{bottomLabel(node, r.cx, r.label, textMetrics)}
|
|
756
800
|
</g>
|
|
757
801
|
);
|
|
758
802
|
}
|
|
759
803
|
|
|
760
804
|
/** shape-rpc-node = JSON-RPC provider */
|
|
761
|
-
export function ShapeRpcNodeNode({
|
|
805
|
+
export function ShapeRpcNodeNode({
|
|
806
|
+
node,
|
|
807
|
+
active,
|
|
808
|
+
lang,
|
|
809
|
+
textMetrics = "proportional",
|
|
810
|
+
}: ShapeProps): JSX.Element {
|
|
762
811
|
const 字引 = 字引きを作る(lang);
|
|
763
812
|
const frame = commonFrame(active);
|
|
764
813
|
const r = shapeRegion(node);
|
|
@@ -788,13 +837,13 @@ export function ShapeRpcNodeNode({ node, active, lang }: ShapeProps): JSX.Elemen
|
|
|
788
837
|
<rect x={x + 16} y={y + bh - 24} width={(bw - 32) * 0.92} height={8} rx={4} fill="#27c93f" stroke="none">
|
|
789
838
|
<animate attributeName="width" values={`${(bw - 32) * 0.86};${(bw - 32) * 0.96};${(bw - 32) * 0.86}`} dur="3s" repeatCount="indefinite" />
|
|
790
839
|
</rect>
|
|
791
|
-
{bottomLabel(node, r.cx, r.label)}
|
|
840
|
+
{bottomLabel(node, r.cx, r.label, textMetrics)}
|
|
792
841
|
</g>
|
|
793
842
|
);
|
|
794
843
|
}
|
|
795
844
|
|
|
796
845
|
/** shape-wallet = FC-S5 忠実移植 (二つ折り札入れ横長 + fold line 中央 + bar clasp + engraved Ξ text + コイン peek 3) */
|
|
797
|
-
export function ShapeWalletNode({ node, active }: ShapeProps): JSX.Element {
|
|
846
|
+
export function ShapeWalletNode({ node, active, textMetrics = "proportional" }: ShapeProps): JSX.Element {
|
|
798
847
|
const frame = commonFrame(active);
|
|
799
848
|
const r = shapeRegion(node);
|
|
800
849
|
const cx = r.cx;
|
|
@@ -885,13 +934,13 @@ export function ShapeWalletNode({ node, active }: ShapeProps): JSX.Element {
|
|
|
885
934
|
{/* body 上端 highlight (FC-S5 元 = M 42 86 L 258 86) */}
|
|
886
935
|
<path d={`M ${x + 2} ${y + 6} L ${x + w - 2} ${y + 6}`} fill="none" stroke="#ffffff" strokeWidth={1} opacity={0.4} />
|
|
887
936
|
</g>
|
|
888
|
-
{bottomLabel(node, r.cx, r.label)}
|
|
937
|
+
{bottomLabel(node, r.cx, r.label, textMetrics)}
|
|
889
938
|
</g>
|
|
890
939
|
);
|
|
891
940
|
}
|
|
892
941
|
|
|
893
942
|
/** shape-nft = NFT card (白 body + accent art frame + 巨大 NFT 文字、 統一デザイン) */
|
|
894
|
-
export function ShapeNftNode({ node, active }: ShapeProps): JSX.Element {
|
|
943
|
+
export function ShapeNftNode({ node, active, textMetrics = "proportional" }: ShapeProps): JSX.Element {
|
|
895
944
|
const frame = commonFrame(active);
|
|
896
945
|
const r = shapeRegion(node);
|
|
897
946
|
const cx = r.cx;
|
|
@@ -914,7 +963,7 @@ export function ShapeNftNode({ node, active }: ShapeProps): JSX.Element {
|
|
|
914
963
|
{ text: "#1024", size: NFT_ID_SIZE, 帯: { 幅: cardW - 16 - 34, 高さ: cardH - artH } },
|
|
915
964
|
];
|
|
916
965
|
const art = (倍率: number): JSX.Element => {
|
|
917
|
-
const 倍 = 打ち消しの倍率(倍率,
|
|
966
|
+
const 倍 = 打ち消しの倍率(倍率, 打ち消す字ら, textMetrics);
|
|
918
967
|
return (
|
|
919
968
|
<g>
|
|
920
969
|
{/* shadow */}
|
|
@@ -959,13 +1008,13 @@ export function ShapeNftNode({ node, active }: ShapeProps): JSX.Element {
|
|
|
959
1008
|
return (
|
|
960
1009
|
<g>
|
|
961
1010
|
{fitArt(r, nftExtent(frame.strokeWidth), art)}
|
|
962
|
-
{bottomLabel(node, r.cx, r.label)}
|
|
1011
|
+
{bottomLabel(node, r.cx, r.label, textMetrics)}
|
|
963
1012
|
</g>
|
|
964
1013
|
);
|
|
965
1014
|
}
|
|
966
1015
|
|
|
967
1016
|
/** shape-token = coin (白 body + accent 色 rim + accent 色 Diamond ロゴ、 統一デザイン) */
|
|
968
|
-
export function ShapeTokenNode({ node, active }: ShapeProps): JSX.Element {
|
|
1017
|
+
export function ShapeTokenNode({ node, active, textMetrics = "proportional" }: ShapeProps): JSX.Element {
|
|
969
1018
|
const frame = commonFrame(active);
|
|
970
1019
|
const r = shapeRegion(node);
|
|
971
1020
|
const cx = r.cx;
|
|
@@ -1002,7 +1051,7 @@ export function ShapeTokenNode({ node, active }: ShapeProps): JSX.Element {
|
|
|
1002
1051
|
stroke: "#1a4a68",
|
|
1003
1052
|
}}
|
|
1004
1053
|
/>
|
|
1005
|
-
{bottomLabel(node, r.cx, r.label)}
|
|
1054
|
+
{bottomLabel(node, r.cx, r.label, textMetrics)}
|
|
1006
1055
|
</g>
|
|
1007
1056
|
);
|
|
1008
1057
|
}
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
帯で切る,
|
|
11
11
|
} from "./shape-region";
|
|
12
12
|
import { MONO_ADVANCE_RATIO, displayWidth } from "./row-align";
|
|
13
|
-
import type { LaidNode } from "../types";
|
|
13
|
+
import type { CdlTextMetrics, LaidNode } from "../types";
|
|
14
14
|
import { 字引きを作る, type CdlLang } from "../lang";
|
|
15
15
|
|
|
16
16
|
/**
|
|
@@ -29,6 +29,7 @@ type ShapeProps = {
|
|
|
29
29
|
active: boolean;
|
|
30
30
|
/** 描画側の固定の字の言語 (#847)。 渡さない呼び方は英語で描く */
|
|
31
31
|
lang?: CdlLang;
|
|
32
|
+
textMetrics?: CdlTextMetrics;
|
|
32
33
|
};
|
|
33
34
|
|
|
34
35
|
const commonFrame = (active: boolean) => ({
|
|
@@ -82,7 +83,13 @@ function 字の比(c: string): number {
|
|
|
82
83
|
}
|
|
83
84
|
|
|
84
85
|
/** 字の並びの幅の見積り。 字ごとの比を足して、 字間を字数分足す。 */
|
|
85
|
-
function 字幅(
|
|
86
|
+
function 字幅(
|
|
87
|
+
文字列: string,
|
|
88
|
+
大きさ: number,
|
|
89
|
+
字間: number,
|
|
90
|
+
textMetrics: CdlTextMetrics = "proportional",
|
|
91
|
+
): number {
|
|
92
|
+
if (textMetrics === "monospace") return textWidth(文字列, 大きさ, 字間, textMetrics);
|
|
86
93
|
let 幅 = 0;
|
|
87
94
|
let 字数 = 0;
|
|
88
95
|
for (const c of 文字列) {
|
|
@@ -101,28 +108,55 @@ function 字幅(文字列: string, 大きさ: number, 字間: number): number {
|
|
|
101
108
|
*
|
|
102
109
|
* 字間も同じ割合で詰める = 大きさだけ縮めても字間が残ると幅が比例して縮まない。
|
|
103
110
|
*/
|
|
104
|
-
function 幅に収める(
|
|
105
|
-
|
|
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 幅 = 測る(文字列, 大きさ, 字間);
|
|
106
121
|
// 縮めは読める下限で止める (#953)。 下限が無いと長い名前が点にしか見えない大きさになる。
|
|
107
122
|
// 止めた行は箱に入らないので、入るところまで切って `…` を付ける (#433 の決まりを保つ)。
|
|
108
123
|
//
|
|
109
124
|
// **下限を当てるのは題の行だけ** (#957)。 補いの行に当てると、収まっている行を
|
|
110
125
|
// 大きくしてから切ることになる = 書いた字を落として得るのが 1px に満たない
|
|
111
|
-
const 下限 = 題か ? 切って収まる下限(文字列, 大きさ, 字間, 箱幅,
|
|
126
|
+
const 下限 = 題か ? 切って収まる下限(文字列, 大きさ, 字間, 箱幅, 測る) : 0;
|
|
112
127
|
const 倍率 = 幅 > 0 && 箱幅 > 0 ? Math.min(1, Math.max(箱幅 / 幅, 下限)) : 1;
|
|
113
128
|
const size = 大きさ * 倍率;
|
|
114
129
|
const spacing = 字間 * 倍率;
|
|
115
|
-
return { size, spacing, text: 帯で切る(文字列, size, 箱幅, (u, f) =>
|
|
130
|
+
return { size, spacing, text: 帯で切る(文字列, size, 箱幅, (u, f) => 測る(u, f, spacing)) };
|
|
116
131
|
}
|
|
117
132
|
|
|
118
133
|
/**
|
|
119
134
|
* 名前の塊を描く。 縦の位置と大きさは `labelPlan` が決め (箱の下端から積み上げる、 #433)、
|
|
120
135
|
* 横は `幅に収める` が箱の幅に合わせて縮める。
|
|
121
136
|
*/
|
|
122
|
-
function bottomLabel(
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
+
);
|
|
126
160
|
return (
|
|
127
161
|
<>
|
|
128
162
|
{node.eyebrow && plan.肩書 && (
|
|
@@ -160,7 +194,7 @@ const WEBSITE_W_MIN = 160;
|
|
|
160
194
|
const WEBSITE_W_MAX = 280;
|
|
161
195
|
|
|
162
196
|
/** shape-website = ブラウザ (シンプル + シンプルレイアウト) */
|
|
163
|
-
export function ShapeWebsiteNode({ node, active }: ShapeProps): JSX.Element {
|
|
197
|
+
export function ShapeWebsiteNode({ node, active, textMetrics = "proportional" }: ShapeProps): JSX.Element {
|
|
164
198
|
const frame = commonFrame(active);
|
|
165
199
|
const r = shapeRegion(node);
|
|
166
200
|
const cx = r.cx;
|
|
@@ -194,13 +228,13 @@ export function ShapeWebsiteNode({ node, active }: ShapeProps): JSX.Element {
|
|
|
194
228
|
return (
|
|
195
229
|
<g>
|
|
196
230
|
{fitArt(r, 広がり, 絵)}
|
|
197
|
-
{bottomLabel(node, r.cx, r.label)}
|
|
231
|
+
{bottomLabel(node, r.cx, r.label, textMetrics)}
|
|
198
232
|
</g>
|
|
199
233
|
);
|
|
200
234
|
}
|
|
201
235
|
|
|
202
236
|
/** shape-storefront = S-A シンプル shop (平屋根 + SHOP 看板 + 大 window (商品) + 中央 door + OPEN sign) */
|
|
203
|
-
export function ShapeStorefrontNode({ node, active, lang }: ShapeProps): JSX.Element {
|
|
237
|
+
export function ShapeStorefrontNode({ node, active, lang, textMetrics = "proportional" }: ShapeProps): JSX.Element {
|
|
204
238
|
const 字引 = 字引きを作る(lang);
|
|
205
239
|
const frame = commonFrame(active);
|
|
206
240
|
const r = shapeRegion(node);
|
|
@@ -258,7 +292,7 @@ export function ShapeStorefrontNode({ node, active, lang }: ShapeProps): JSX.Ele
|
|
|
258
292
|
<circle cx={doorX + 6} cy={doorY + doorH * 0.55} r={1.5} fill="#c15a4a" />
|
|
259
293
|
{/* base 床 (前 storefront 直下のみ) */}
|
|
260
294
|
<rect x={x - 6} y={y + bh - 6} width={bw + 12} height={8} fill="#5a6270" stroke="none" />
|
|
261
|
-
{bottomLabel(node, r.cx, r.label)}
|
|
295
|
+
{bottomLabel(node, r.cx, r.label, textMetrics)}
|
|
262
296
|
</g>
|
|
263
297
|
);
|
|
264
298
|
}
|
|
@@ -286,7 +320,7 @@ const WAREHOUSE_W_MIN = 160;
|
|
|
286
320
|
const WAREHOUSE_W_MAX = 240;
|
|
287
321
|
|
|
288
322
|
/** shape-warehouse = W-A 大 shutter (平屋根 + WAREHOUSE 看板 + 中央大 shutter 10 rib + loading dock) */
|
|
289
|
-
export function ShapeWarehouseNode({ node, active, lang }: ShapeProps): JSX.Element {
|
|
323
|
+
export function ShapeWarehouseNode({ node, active, lang, textMetrics = "proportional" }: ShapeProps): JSX.Element {
|
|
290
324
|
const 字引 = 字引きを作る(lang);
|
|
291
325
|
const frame = commonFrame(active);
|
|
292
326
|
const r = shapeRegion(node);
|
|
@@ -353,7 +387,7 @@ export function ShapeWarehouseNode({ node, active, lang }: ShapeProps): JSX.Elem
|
|
|
353
387
|
return (
|
|
354
388
|
<g>
|
|
355
389
|
{fitArt(r, 広がり, 絵)}
|
|
356
|
-
{bottomLabel(node, r.cx, r.label)}
|
|
390
|
+
{bottomLabel(node, r.cx, r.label, textMetrics)}
|
|
357
391
|
</g>
|
|
358
392
|
);
|
|
359
393
|
}
|
|
@@ -372,7 +406,7 @@ const HANDLE_GRIP_R = 4;
|
|
|
372
406
|
/** 影の楕円が中心から左右へ出る距離。 絵の中で一番右まで来る部品 */
|
|
373
407
|
const CART_SHADOW_RX = 80;
|
|
374
408
|
|
|
375
|
-
export function ShapeOnlineShopNode({ node, active }: ShapeProps): JSX.Element {
|
|
409
|
+
export function ShapeOnlineShopNode({ node, active, textMetrics = "proportional" }: ShapeProps): JSX.Element {
|
|
376
410
|
const frame = commonFrame(active);
|
|
377
411
|
const r = shapeRegion(node);
|
|
378
412
|
const cy = r.cy + 10;
|
|
@@ -445,13 +479,13 @@ export function ShapeOnlineShopNode({ node, active }: ShapeProps): JSX.Element {
|
|
|
445
479
|
<circle cx={wheelR} cy={wheelY} r={16 * s} fill="#0f1830" stroke="#000" strokeWidth={2 * s} />
|
|
446
480
|
<circle cx={wheelR} cy={wheelY} r={8 * s} fill="#5a6270" stroke="#0f1830" strokeWidth={1.5 * s} />
|
|
447
481
|
<circle cx={wheelR} cy={wheelY} r={2 * s} fill="#0f1830" />
|
|
448
|
-
{bottomLabel(node, r.cx, r.label)}
|
|
482
|
+
{bottomLabel(node, r.cx, r.label, textMetrics)}
|
|
449
483
|
</g>
|
|
450
484
|
);
|
|
451
485
|
}
|
|
452
486
|
|
|
453
487
|
/** shape-cdn-edge = 地球儀 (globe + 5 edge dots + pulse ring + lightning at center) */
|
|
454
|
-
export function ShapeCdnEdgeNode({ node, active }: ShapeProps): JSX.Element {
|
|
488
|
+
export function ShapeCdnEdgeNode({ node, active, textMetrics = "proportional" }: ShapeProps): JSX.Element {
|
|
455
489
|
const frame = commonFrame(active);
|
|
456
490
|
const r = shapeRegion(node);
|
|
457
491
|
const cx = r.cx;
|
|
@@ -523,7 +557,7 @@ export function ShapeCdnEdgeNode({ node, active }: ShapeProps): JSX.Element {
|
|
|
523
557
|
return (
|
|
524
558
|
<g>
|
|
525
559
|
{fitArt(r, { 上: 広がり, 下: 広がり, 左: 広がり, 右: 広がり }, 絵)}
|
|
526
|
-
{bottomLabel(node, r.cx, r.label)}
|
|
560
|
+
{bottomLabel(node, r.cx, r.label, textMetrics)}
|
|
527
561
|
</g>
|
|
528
562
|
);
|
|
529
563
|
}
|
|
@@ -553,7 +587,7 @@ const API_W_MIN = 200;
|
|
|
553
587
|
const API_W_MAX = 300;
|
|
554
588
|
|
|
555
589
|
/** shape-api-gateway = SaaS API panel (dark bg + method + path endpoints + status) */
|
|
556
|
-
export function ShapeApiGatewayNode({ node, active, lang }: ShapeProps): JSX.Element {
|
|
590
|
+
export function ShapeApiGatewayNode({ node, active, lang, textMetrics = "proportional" }: ShapeProps): JSX.Element {
|
|
557
591
|
const 字引 = 字引きを作る(lang);
|
|
558
592
|
const frame = commonFrame(active);
|
|
559
593
|
const r = shapeRegion(node);
|
|
@@ -570,7 +604,14 @@ export function ShapeApiGatewayNode({ node, active, lang }: ShapeProps): JSX.Ele
|
|
|
570
604
|
// 11 のまま入り、長い綴り (日本語の辞書の `DELETE`) だけを縮める。 見積りは等幅と比例の大きい方
|
|
571
605
|
// (`shape-blockchain.tsx` の `字の幅` と同じ考え方で、どちらの書体で描かれても箱からはみ出さない)
|
|
572
606
|
const 種類の字 = (s: string): number =>
|
|
573
|
-
Math.min(
|
|
607
|
+
Math.min(
|
|
608
|
+
11,
|
|
609
|
+
(11 * API_METHOD_TEXT_W) /
|
|
610
|
+
Math.max(
|
|
611
|
+
displayWidth(s) * 11 * MONO_ADVANCE_RATIO,
|
|
612
|
+
textWidth(s, 11, 0, textMetrics),
|
|
613
|
+
),
|
|
614
|
+
);
|
|
574
615
|
const rowPitch = Math.max(
|
|
575
616
|
API_ROW_PITCH_MIN,
|
|
576
617
|
(bh - API_ROW_TOP - API_ROW_H - API_ROW_BOTTOM_PAD) / (API_ROWS - 1),
|
|
@@ -595,7 +636,7 @@ export function ShapeApiGatewayNode({ node, active, lang }: ShapeProps): JSX.Ele
|
|
|
595
636
|
帯: { 幅: bw - 32 - 64 - 16, 高さ: API_ROW_H },
|
|
596
637
|
}));
|
|
597
638
|
const 絵 = (倍率: number): JSX.Element => {
|
|
598
|
-
const 倍 = 打ち消しの倍率(倍率,
|
|
639
|
+
const 倍 = 打ち消しの倍率(倍率, 打ち消す字ら, textMetrics);
|
|
599
640
|
return (
|
|
600
641
|
<g>
|
|
601
642
|
<rect x={x + DROP_SHADOW_D} y={y + DROP_SHADOW_D} width={bw} height={bh} rx={10} fill={frame.stroke} opacity={0.2} stroke="none" />
|
|
@@ -625,7 +666,7 @@ export function ShapeApiGatewayNode({ node, active, lang }: ShapeProps): JSX.Ele
|
|
|
625
666
|
return (
|
|
626
667
|
<g>
|
|
627
668
|
{fitArt(r, 広がり, 絵)}
|
|
628
|
-
{bottomLabel(node, r.cx, r.label)}
|
|
669
|
+
{bottomLabel(node, r.cx, r.label, textMetrics)}
|
|
629
670
|
</g>
|
|
630
671
|
);
|
|
631
672
|
}
|