@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.
Files changed (69) hide show
  1. package/CHANGELOG.md +51 -1
  2. package/README.md +137 -0
  3. package/SPEC.md +1 -1
  4. package/dist/index.cjs +1513 -667
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.d.cts +51 -33
  7. package/dist/index.d.ts +51 -33
  8. package/dist/index.js +1513 -668
  9. package/dist/index.js.map +1 -1
  10. package/dist/react.cjs +1271 -444
  11. package/dist/react.cjs.map +1 -1
  12. package/dist/react.d.cts +1 -1
  13. package/dist/react.d.ts +1 -1
  14. package/dist/react.js +1271 -444
  15. package/dist/react.js.map +1 -1
  16. package/dist/{render-A5AKjNuy.d.cts → render-CVczwfRb.d.cts} +40 -7
  17. package/dist/{render-A5AKjNuy.d.ts → render-CVczwfRb.d.ts} +40 -7
  18. package/package.json +9 -12
  19. package/src/builder.ts +8 -2
  20. package/src/index.ts +3 -0
  21. package/src/kinds/box-lines.ts +17 -4
  22. package/src/kinds/chart-bar.tsx +4 -0
  23. package/src/kinds/chart-gauge.tsx +20 -9
  24. package/src/kinds/chart-pie.tsx +67 -17
  25. package/src/kinds/chart-radial.tsx +7 -2
  26. package/src/kinds/chart-slope.tsx +26 -7
  27. package/src/kinds/chart-stacked-bar.tsx +8 -4
  28. package/src/kinds/chart-stat.tsx +19 -4
  29. package/src/kinds/chart-waffle.tsx +7 -2
  30. package/src/kinds/dyn-shape.tsx +22 -9
  31. package/src/kinds/funnel.tsx +9 -2
  32. package/src/kinds/generic.tsx +5 -2
  33. package/src/kinds/mind-map.tsx +26 -10
  34. package/src/kinds/quadrant.tsx +14 -4
  35. package/src/kinds/row-align.ts +19 -7
  36. package/src/kinds/sequence-board-metrics.ts +13 -3
  37. package/src/kinds/sequence-board.tsx +4 -2
  38. package/src/kinds/shape-basement.tsx +57 -32
  39. package/src/kinds/shape-blockchain.tsx +83 -34
  40. package/src/kinds/shape-commerce.tsx +65 -24
  41. package/src/kinds/shape-finance.tsx +90 -26
  42. package/src/kinds/shape-hardware.tsx +48 -17
  43. package/src/kinds/shape-region.tsx +16 -6
  44. package/src/kinds/shape-software.tsx +30 -8
  45. package/src/kinds/station.tsx +29 -0
  46. package/src/kinds/text-width.ts +17 -3
  47. package/src/kinds/tree.tsx +25 -7
  48. package/src/layout/collisions.ts +50 -28
  49. package/src/layout/curve-path.ts +126 -0
  50. package/src/layout/edges.ts +271 -58
  51. package/src/layout/head-label.ts +11 -5
  52. package/src/layout/lanes.ts +3 -2
  53. package/src/layout/metro-path.ts +185 -0
  54. package/src/layout/nodes.ts +14 -6
  55. package/src/layout/predict-bbox.ts +6 -3
  56. package/src/layout/self-loop.ts +10 -4
  57. package/src/layout/spec.ts +36 -7
  58. package/src/layout/text-width.ts +25 -8
  59. package/src/layout/tokens.ts +2 -0
  60. package/src/layout-with-validation.ts +10 -5
  61. package/src/layout.ts +55 -21
  62. package/src/presets.ts +69 -43
  63. package/src/render/edges.tsx +19 -3
  64. package/src/render/interactive-panel.tsx +34 -7
  65. package/src/render/nodes.tsx +100 -48
  66. package/src/render/stage.tsx +3 -0
  67. package/src/text-metrics.ts +9 -0
  68. package/src/types.ts +41 -4
  69. 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(node: LaidNode, cx: number, plan: ReturnType<typeof labelPlan>) {
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) => 字の幅(u, f) + ls * [...u].length;
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({ node, active, lang }: ShapeProps): JSX.Element {
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 字の幅 = (text: string, size: number): number => 絵の中の字の幅(text, size);
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({ node, active, lang }: ShapeProps): JSX.Element {
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 肩書の倍率 = 肩書 === "" ? 1 : 幅の倍率(肩書, 肩書の字, 0, 肩書の上限);
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 - 字の幅(f.label, 名の字) - 8, 高さ: 行の間隔 },
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
- {幅で切る(f.value, 値の大きさ, bw - 28 - 字の幅(f.label, 名の大きさ) - 8, 字の幅)}
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({ node, active }: ShapeProps): JSX.Element {
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({ node, active }: ShapeProps): JSX.Element {
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({ node, active }: ShapeProps): JSX.Element {
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({ node, active }: ShapeProps): JSX.Element {
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({ node, active, lang }: ShapeProps): JSX.Element {
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 字幅(文字列: string, 大きさ: number, 字間: number): number {
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 幅に収める(文字列: string, 大きさ: number, 字間: number, 箱幅: number, 題か = false) {
105
- const 幅 = 字幅(文字列, 大きさ, 字間);
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 下限 = 題か ? 切って収まる下限(文字列, 大きさ, 字間, 箱幅, 字幅) : 0;
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) => 字幅(u, f, spacing)) };
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(node: LaidNode, cx: number, plan: ReturnType<typeof labelPlan>) {
123
- const 肩 = 幅に収める(node.eyebrow ?? "", plan.肩書?.size ?? 0, LABEL_EYEBROW_SPACING, node.w);
124
- const 名 = 幅に収める(node.title ?? "", plan.名前.size, 0, node.w, true);
125
- const 説 = 幅に収める(node.subtitle ?? "", plan.説明?.size ?? 0, 0, node.w);
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(11, (11 * API_METHOD_TEXT_W) / Math.max(displayWidth(s) * 11 * MONO_ADVANCE_RATIO, textWidth(s, 11)));
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
  }