@cardenelabs/cdl 0.122.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 (64) hide show
  1. package/CHANGELOG.md +16 -1
  2. package/README.md +64 -0
  3. package/dist/index.cjs +1065 -617
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.d.cts +38 -26
  6. package/dist/index.d.ts +38 -26
  7. package/dist/index.js +1065 -618
  8. package/dist/index.js.map +1 -1
  9. package/dist/react.cjs +834 -403
  10. package/dist/react.cjs.map +1 -1
  11. package/dist/react.d.cts +1 -1
  12. package/dist/react.d.ts +1 -1
  13. package/dist/react.js +834 -403
  14. package/dist/react.js.map +1 -1
  15. package/dist/{render-U_pC-Ksh.d.cts → render-CVczwfRb.d.cts} +20 -2
  16. package/dist/{render-U_pC-Ksh.d.ts → render-CVczwfRb.d.ts} +20 -2
  17. package/package.json +9 -12
  18. package/src/builder.ts +5 -0
  19. package/src/index.ts +2 -0
  20. package/src/kinds/box-lines.ts +17 -4
  21. package/src/kinds/chart-bar.tsx +4 -0
  22. package/src/kinds/chart-gauge.tsx +20 -9
  23. package/src/kinds/chart-pie.tsx +67 -17
  24. package/src/kinds/chart-radial.tsx +7 -2
  25. package/src/kinds/chart-slope.tsx +26 -7
  26. package/src/kinds/chart-stacked-bar.tsx +8 -4
  27. package/src/kinds/chart-stat.tsx +19 -4
  28. package/src/kinds/chart-waffle.tsx +7 -2
  29. package/src/kinds/funnel.tsx +9 -2
  30. package/src/kinds/generic.tsx +5 -2
  31. package/src/kinds/mind-map.tsx +26 -10
  32. package/src/kinds/quadrant.tsx +14 -4
  33. package/src/kinds/row-align.ts +19 -7
  34. package/src/kinds/sequence-board-metrics.ts +13 -3
  35. package/src/kinds/sequence-board.tsx +4 -2
  36. package/src/kinds/shape-basement.tsx +57 -32
  37. package/src/kinds/shape-blockchain.tsx +83 -34
  38. package/src/kinds/shape-commerce.tsx +65 -24
  39. package/src/kinds/shape-finance.tsx +90 -26
  40. package/src/kinds/shape-hardware.tsx +48 -17
  41. package/src/kinds/shape-region.tsx +16 -6
  42. package/src/kinds/shape-software.tsx +30 -8
  43. package/src/kinds/text-width.ts +17 -3
  44. package/src/kinds/tree.tsx +25 -7
  45. package/src/layout/collisions.ts +21 -8
  46. package/src/layout/curve-path.ts +3 -2
  47. package/src/layout/edges.ts +70 -25
  48. package/src/layout/head-label.ts +3 -1
  49. package/src/layout/lanes.ts +3 -2
  50. package/src/layout/metro-path.ts +3 -2
  51. package/src/layout/nodes.ts +14 -6
  52. package/src/layout/predict-bbox.ts +6 -3
  53. package/src/layout/self-loop.ts +10 -4
  54. package/src/layout/spec.ts +20 -6
  55. package/src/layout/text-width.ts +25 -8
  56. package/src/layout.ts +39 -15
  57. package/src/presets.ts +69 -43
  58. package/src/render/edges.tsx +18 -3
  59. package/src/render/interactive-panel.tsx +34 -7
  60. package/src/render/nodes.tsx +97 -48
  61. package/src/render/stage.tsx +3 -0
  62. package/src/text-metrics.ts +9 -0
  63. package/src/types.ts +19 -0
  64. package/src/visual-validate.ts +67 -17
@@ -2,7 +2,8 @@ import { createContext, useContext, useEffect, useLayoutEffect, useMemo, useStat
2
2
  import type { JSX } from "react";
3
3
  import { CDL_TEXT, resolveLang, 字を差し込む, type CdlLang, type CdlTextKey } from "../lang";
4
4
  import { assertNever } from "../assert-never";
5
- import type { CdlDiagram, CdlInput, CdlReadout } from "../types";
5
+ import type { CdlDiagram, CdlInput, CdlReadout, CdlTextMetrics } from "../types";
6
+ import { resolveTextMetrics } from "../text-metrics";
6
7
  import { createInputSignals, inputDefaultValue } from "../input-signals";
7
8
  import { 選択肢の値, 選択肢の名前 } from "../input-option";
8
9
  import { createFormulaComputeds } from "../formula";
@@ -19,7 +20,7 @@ import type { Signal, Computed } from "../reactive";
19
20
  import { effect } from "../reactive";
20
21
  import { textWidth } from "../kinds/text-width";
21
22
  import { 桁で割る, 桁で省く } from "../kinds/row-align";
22
- import { 幅で切る } from "../kinds/box-lines";
23
+ import { 幅で切る, 箱の字を測る } from "../kinds/box-lines";
23
24
 
24
25
  /**
25
26
  * interactive primitive (input widget / formula / scroll trigger / event handler) を SVG diagram に
@@ -685,7 +686,12 @@ export function useInteractiveIntegration(
685
686
  {readouts.length > 0 && (
686
687
  <div className="cdl-ip-group">
687
688
  {readouts.map((r) => (
688
- <ReadoutWidget key={r.id} readout={r} stateOverrides={mergedStateValues} />
689
+ <ReadoutWidget
690
+ key={r.id}
691
+ readout={r}
692
+ stateOverrides={mergedStateValues}
693
+ textMetrics={resolveTextMetrics(diagram.textMetrics)}
694
+ />
689
695
  ))}
690
696
  </div>
691
697
  )}
@@ -1173,9 +1179,11 @@ function StepperWidget({
1173
1179
  function ReadoutWidget({
1174
1180
  readout,
1175
1181
  stateOverrides,
1182
+ textMetrics = "proportional",
1176
1183
  }: {
1177
1184
  readout: CdlReadout;
1178
1185
  stateOverrides: Record<string, number | string | boolean>;
1186
+ textMetrics?: CdlTextMetrics;
1179
1187
  }): JSX.Element {
1180
1188
  // stacked-bar は sourceA / sourceB を持ち source を持たない、 その場合は rawValue = undefined 扱い
1181
1189
  const rawValue = "source" in readout ? stateOverrides[readout.source] : undefined;
@@ -1414,6 +1422,7 @@ function ReadoutWidget({
1414
1422
  viewW={readout.viewW ?? 280}
1415
1423
  viewH={readout.viewH ?? 60}
1416
1424
  color={readout.color ?? "#2563eb"}
1425
+ textMetrics={textMetrics}
1417
1426
  />
1418
1427
  );
1419
1428
  case "radar": {
@@ -2166,6 +2175,7 @@ function ReadoutWidget({
2166
2175
  { status: "error", color: "#ef4444" },
2167
2176
  ]}
2168
2177
  max={readout.max ?? 10}
2178
+ textMetrics={textMetrics}
2169
2179
  />
2170
2180
  );
2171
2181
  case "calendar-week":
@@ -3559,8 +3569,14 @@ function ProgressGroupReadout({
3559
3569
  * 札の幅は描く前に分からないので `textWidth` で見積もる。 見積りは実物以上に倒してあるため、
3560
3570
  * 収まる札を端に寄せることはあっても、はみ出す札を中央に残すことは無い。
3561
3571
  */
3562
- function sequenceLabelAnchor(x: number, name: string, fontSize: number, viewW: number): "start" | "middle" | "end" {
3563
- const half = textWidth(name, fontSize) / 2;
3572
+ function sequenceLabelAnchor(
3573
+ x: number,
3574
+ name: string,
3575
+ fontSize: number,
3576
+ viewW: number,
3577
+ textMetrics: CdlTextMetrics = "proportional",
3578
+ ): "start" | "middle" | "end" {
3579
+ const half = textWidth(name, fontSize, 0, textMetrics) / 2;
3564
3580
  if (x - half < 0) return "start";
3565
3581
  if (x + half > viewW) return "end";
3566
3582
  return "middle";
@@ -3575,6 +3591,7 @@ function SequenceTimelineReadout({
3575
3591
  viewW,
3576
3592
  viewH,
3577
3593
  color,
3594
+ textMetrics = "proportional",
3578
3595
  }: {
3579
3596
  id: string;
3580
3597
  label: string;
@@ -3584,6 +3601,7 @@ function SequenceTimelineReadout({
3584
3601
  viewW: number;
3585
3602
  viewH: number;
3586
3603
  color: string;
3604
+ textMetrics?: CdlTextMetrics;
3587
3605
  }): JSX.Element {
3588
3606
  // event parse: [[t, name], ...] または [t, t, t, ...]
3589
3607
  const events: { t: number; name: string }[] = [];
@@ -3636,7 +3654,13 @@ function SequenceTimelineReadout({
3636
3654
  <circle cx={x} cy={trackY} r={5} fill={color} stroke="#fff" strokeWidth={1.5}>
3637
3655
  <字の見出し k="readout.sequenceTimeline.title" v={{ name: e.name, t: e.t }} />
3638
3656
  </circle>
3639
- <text x={x} y={trackY - 12} textAnchor={sequenceLabelAnchor(x, e.name, 10, viewW)} fontSize={10} fill="var(--v4-ink, #1a1f2a)">
3657
+ <text
3658
+ x={x}
3659
+ y={trackY - 12}
3660
+ textAnchor={sequenceLabelAnchor(x, e.name, 10, viewW, textMetrics)}
3661
+ fontSize={10}
3662
+ fill="var(--v4-ink, #1a1f2a)"
3663
+ >
3640
3664
  {e.name}
3641
3665
  </text>
3642
3666
  <text x={x} y={trackY + 18} textAnchor="middle" fontSize={9} fill="var(--v4-mute, #64748b)">
@@ -7715,12 +7739,14 @@ function StatusTimelineReadout({
7715
7739
  value,
7716
7740
  colorMap,
7717
7741
  max,
7742
+ textMetrics = "proportional",
7718
7743
  }: {
7719
7744
  id: string;
7720
7745
  label: string;
7721
7746
  value: string;
7722
7747
  colorMap: Array<{ status: string; color: string }>;
7723
7748
  max: number;
7749
+ textMetrics?: CdlTextMetrics;
7724
7750
  }): JSX.Element {
7725
7751
  type Event = { timestamp: string; status: string };
7726
7752
  const events: Event[] = [];
@@ -7757,6 +7783,7 @@ function StatusTimelineReadout({
7757
7783
  status === "active" || status === "idle" || status === "error"
7758
7784
  ? 引く(`readout.statusTimeline.${status}`)
7759
7785
  : status.toUpperCase();
7786
+ const 測る = 箱の字を測る(textMetrics);
7760
7787
  return (
7761
7788
  <div className="cdl-ip-item cdl-ip-readout-status-timeline" data-cdl-readout={id}>
7762
7789
  <span className="cdl-ip-label">{label}</span>
@@ -7771,7 +7798,7 @@ function StatusTimelineReadout({
7771
7798
  const c = colorLookup.get(ev.status) ?? "#cbd5e1";
7772
7799
  const 字 = 区間の字(ev.status);
7773
7800
  // 左右に 3px ずつ余白を取る (区間の枠は幅 - 2 で描く)
7774
- const 描く字 = 幅で切る(字, 10, cellW - 8);
7801
+ const 描く字 = 幅で切る(字, 10, cellW - 8, 測る);
7775
7802
  return (
7776
7803
  <g key={i} data-cdl-status-event={i} data-cdl-status={ev.status}>
7777
7804
  <rect x={x} y={10} width={cellW - 2} height={stripHeight} fill={c} rx={2} />
@@ -87,7 +87,7 @@ import {
87
87
  ShapeTraderNode,
88
88
  ShapeCustomerServiceNode,
89
89
  } from "../kinds/shape-people";
90
- import type { LaidNode } from "../types";
90
+ import type { CdlTextMetrics, LaidNode } from "../types";
91
91
  import type { CdlLang } from "../lang";
92
92
  import { hasUnresolvedRef, interpolate, resolveNumericAttr } from "./utils";
93
93
  import { drawsFigureTitle } from "../layout/spec";
@@ -121,6 +121,7 @@ export function CdlNodeView({
121
121
  chartLineScale,
122
122
  関係の受け口,
123
123
  lang,
124
+ textMetrics,
124
125
  値の名前,
125
126
  }: {
126
127
  node: LaidNode;
@@ -132,6 +133,8 @@ export function CdlNodeView({
132
133
  値の名前?: 値の名前の表;
133
134
  /** 描画側の固定の字の言語 (#847)。 字を持つ形の部品へ渡す。 渡さない呼び方は英語で描く */
134
135
  lang?: CdlLang;
136
+ /** 図全体の字幅規則。測る描画部品へ明示的に渡す */
137
+ textMetrics?: CdlTextMetrics;
135
138
  /**
136
139
  * この phase の `draw` に載っているか (cdl#512)。 true の間だけ、 対象の種別が
137
140
  * phase の進み (`progress`) に合わせて左の起点から描かれる。
@@ -201,7 +204,13 @@ export function CdlNodeView({
201
204
  return <CardNode node={resolvedNode} active={active} />;
202
205
  // 時系列のやり取り (#578)。 1 つの箱が図を丸ごと描く = 縦が時間で、参加者は箱を持たない
203
206
  case "sequence-board":
204
- return <SequenceBoardNode node={resolvedNode} stateValues={stateValues} />;
207
+ return (
208
+ <SequenceBoardNode
209
+ node={resolvedNode}
210
+ stateValues={stateValues}
211
+ textMetrics={textMetrics}
212
+ />
213
+ );
205
214
  // 図表 9 種は payload が `{signal}` を取れるので stateValues を渡す
206
215
  // (`render/payload-binding.ts` が描画直前に解決する)。 取れる欄は種別で違い、
207
216
  // 数と語を取る 7 種に対し、mind-map / tree-hierarchy は **名前だけ** を取る (#467)。
@@ -225,6 +234,7 @@ export function CdlNodeView({
225
234
  stateValues={stateValues}
226
235
  drawing={drawing}
227
236
  progress={progress}
237
+ textMetrics={textMetrics}
228
238
  />
229
239
  );
230
240
  case "chart-gauge":
@@ -236,6 +246,7 @@ export function CdlNodeView({
236
246
  stateValues={stateValues}
237
247
  drawing={drawing}
238
248
  progress={progress}
249
+ textMetrics={textMetrics}
239
250
  />
240
251
  );
241
252
  case "chart-stacked-bar":
@@ -248,6 +259,7 @@ export function CdlNodeView({
248
259
  stateValues={stateValues}
249
260
  drawing={drawing}
250
261
  progress={progress}
262
+ textMetrics={textMetrics}
251
263
  />
252
264
  );
253
265
  case "chart-slope":
@@ -259,6 +271,7 @@ export function CdlNodeView({
259
271
  stateValues={stateValues}
260
272
  drawing={drawing}
261
273
  progress={progress}
274
+ textMetrics={textMetrics}
262
275
  />
263
276
  );
264
277
  case "chart-waffle":
@@ -270,11 +283,19 @@ export function CdlNodeView({
270
283
  stateValues={stateValues}
271
284
  drawing={drawing}
272
285
  progress={progress}
286
+ textMetrics={textMetrics}
273
287
  />
274
288
  );
275
289
  case "chart-stat":
276
290
  // 数値が主役で、弧は輪郭だけの補助。 段の進みで数値が動く演出は既存の tween が担う
277
- return <ChartStatNode node={resolvedNode} active={active} stateValues={stateValues} />;
291
+ return (
292
+ <ChartStatNode
293
+ node={resolvedNode}
294
+ active={active}
295
+ stateValues={stateValues}
296
+ textMetrics={textMetrics}
297
+ />
298
+ );
278
299
  case "chart-radial":
279
300
  // 段の進みでは各輪が 12 時から開く (cdl#715)
280
301
  return (
@@ -284,6 +305,7 @@ export function CdlNodeView({
284
305
  stateValues={stateValues}
285
306
  drawing={drawing}
286
307
  progress={progress}
308
+ textMetrics={textMetrics}
287
309
  />
288
310
  );
289
311
  case "chart-bar":
@@ -314,6 +336,7 @@ export function CdlNodeView({
314
336
  stateValues={stateValues}
315
337
  drawing={drawing}
316
338
  progress={progress}
339
+ textMetrics={textMetrics}
317
340
  />
318
341
  );
319
342
  case "funnel-stages":
@@ -324,10 +347,18 @@ export function CdlNodeView({
324
347
  stateValues={stateValues}
325
348
  drawing={drawing}
326
349
  progress={progress}
350
+ textMetrics={textMetrics}
327
351
  />
328
352
  );
329
353
  case "quadrant-matrix":
330
- return <QuadrantNode node={resolvedNode} active={active} stateValues={stateValues} />;
354
+ return (
355
+ <QuadrantNode
356
+ node={resolvedNode}
357
+ active={active}
358
+ stateValues={stateValues}
359
+ textMetrics={textMetrics}
360
+ />
361
+ );
331
362
  case "tree-hierarchy":
332
363
  return (
333
364
  <TreeHierarchyNode
@@ -336,6 +367,7 @@ export function CdlNodeView({
336
367
  stateValues={stateValues}
337
368
  drawing={drawing}
338
369
  progress={progress}
370
+ textMetrics={textMetrics}
339
371
  />
340
372
  );
341
373
  case "journey-map":
@@ -349,91 +381,91 @@ export function CdlNodeView({
349
381
  />
350
382
  );
351
383
  case "shape-file":
352
- return <ShapeFileNode node={resolvedNode} active={active} />;
384
+ return <ShapeFileNode node={resolvedNode} active={active} textMetrics={textMetrics} />;
353
385
  case "shape-folder":
354
- return <ShapeFolderNode node={resolvedNode} active={active} />;
386
+ return <ShapeFolderNode node={resolvedNode} active={active} textMetrics={textMetrics} />;
355
387
  case "shape-cloud":
356
- return <ShapeCloudNode node={resolvedNode} active={active} />;
388
+ return <ShapeCloudNode node={resolvedNode} active={active} textMetrics={textMetrics} />;
357
389
  case "shape-cylinder":
358
- return <ShapeCylinderNode node={resolvedNode} active={active} />;
390
+ return <ShapeCylinderNode node={resolvedNode} active={active} textMetrics={textMetrics} />;
359
391
  case "shape-hexagon":
360
- return <ShapeHexagonNode node={resolvedNode} active={active} />;
392
+ return <ShapeHexagonNode node={resolvedNode} active={active} textMetrics={textMetrics} />;
361
393
  case "shape-diamond":
362
- return <ShapeDiamondNode node={resolvedNode} active={active} />;
394
+ return <ShapeDiamondNode node={resolvedNode} active={active} textMetrics={textMetrics} />;
363
395
  case "shape-stack":
364
- return <ShapeStackNode node={resolvedNode} active={active} />;
396
+ return <ShapeStackNode node={resolvedNode} active={active} textMetrics={textMetrics} />;
365
397
  case "shape-person":
366
- return <ShapePersonNode node={resolvedNode} active={active} />;
398
+ return <ShapePersonNode node={resolvedNode} active={active} textMetrics={textMetrics} />;
367
399
  case "shape-window":
368
400
  return <ShapeWindowNode node={resolvedNode} active={active} />;
369
401
  case "shape-terminal":
370
402
  return <ShapeTerminalNode node={resolvedNode} active={active} lang={lang} />;
371
403
  case "shape-code-block":
372
- return <ShapeCodeBlockNode node={resolvedNode} active={active} lang={lang} />;
404
+ return <ShapeCodeBlockNode node={resolvedNode} active={active} lang={lang} textMetrics={textMetrics} />;
373
405
  case "shape-kanban-card":
374
- return <ShapeKanbanCardNode node={resolvedNode} active={active} lang={lang} />;
406
+ return <ShapeKanbanCardNode node={resolvedNode} active={active} lang={lang} textMetrics={textMetrics} />;
375
407
  case "shape-message-bubble":
376
408
  return <ShapeMessageBubbleNode node={resolvedNode} active={active} />;
377
409
  case "shape-gear":
378
410
  return <ShapeGearNode node={resolvedNode} active={active} />;
379
411
  case "shape-server-rack":
380
- return <ShapeServerRackNode node={resolvedNode} active={active} />;
412
+ return <ShapeServerRackNode node={resolvedNode} active={active} textMetrics={textMetrics} />;
381
413
  case "shape-network-node":
382
- return <ShapeNetworkNodeNode node={resolvedNode} active={active} />;
414
+ return <ShapeNetworkNodeNode node={resolvedNode} active={active} textMetrics={textMetrics} />;
383
415
  case "shape-mobile-device":
384
- return <ShapeMobileDeviceNode node={resolvedNode} active={active} />;
416
+ return <ShapeMobileDeviceNode node={resolvedNode} active={active} textMetrics={textMetrics} />;
385
417
  case "shape-iot-sensor":
386
418
  return <ShapeIotSensorNode node={resolvedNode} active={active} />;
387
419
  case "shape-robot-arm":
388
420
  return <ShapeRobotArmNode node={resolvedNode} active={active} />;
389
421
  case "shape-satellite":
390
- return <ShapeSatelliteNode node={resolvedNode} active={active} />;
422
+ return <ShapeSatelliteNode node={resolvedNode} active={active} textMetrics={textMetrics} />;
391
423
  case "shape-smart-contract":
392
- return <ShapeSmartContractNode node={resolvedNode} active={active} lang={lang} />;
424
+ return <ShapeSmartContractNode node={resolvedNode} active={active} lang={lang} textMetrics={textMetrics} />;
393
425
  case "shape-blockchain-block":
394
- return <ShapeBlockchainBlockNode node={resolvedNode} active={active} lang={lang} />;
426
+ return <ShapeBlockchainBlockNode node={resolvedNode} active={active} lang={lang} textMetrics={textMetrics} />;
395
427
  case "shape-rpc-node":
396
- return <ShapeRpcNodeNode node={resolvedNode} active={active} lang={lang} />;
428
+ return <ShapeRpcNodeNode node={resolvedNode} active={active} lang={lang} textMetrics={textMetrics} />;
397
429
  case "shape-wallet":
398
- return <ShapeWalletNode node={resolvedNode} active={active} />;
430
+ return <ShapeWalletNode node={resolvedNode} active={active} textMetrics={textMetrics} />;
399
431
  case "shape-nft":
400
- return <ShapeNftNode node={resolvedNode} active={active} />;
432
+ return <ShapeNftNode node={resolvedNode} active={active} textMetrics={textMetrics} />;
401
433
  case "shape-token":
402
- return <ShapeTokenNode node={resolvedNode} active={active} />;
434
+ return <ShapeTokenNode node={resolvedNode} active={active} textMetrics={textMetrics} />;
403
435
  case "shape-blockchain":
404
- return <ShapeBlockchainNode node={resolvedNode} active={active} />;
436
+ return <ShapeBlockchainNode node={resolvedNode} active={active} textMetrics={textMetrics} />;
405
437
  case "shape-bitcoin-chain":
406
- return <ShapeBitcoinChainNode node={resolvedNode} active={active} />;
438
+ return <ShapeBitcoinChainNode node={resolvedNode} active={active} textMetrics={textMetrics} />;
407
439
  case "shape-ethereum-chain":
408
- return <ShapeEthereumChainNode node={resolvedNode} active={active} />;
440
+ return <ShapeEthereumChainNode node={resolvedNode} active={active} textMetrics={textMetrics} />;
409
441
  case "shape-blockchain-node":
410
- return <ShapeBlockchainNodeNode node={resolvedNode} active={active} />;
442
+ return <ShapeBlockchainNodeNode node={resolvedNode} active={active} textMetrics={textMetrics} />;
411
443
  case "shape-bank":
412
- return <ShapeBankNode node={resolvedNode} active={active} />;
444
+ return <ShapeBankNode node={resolvedNode} active={active} textMetrics={textMetrics} />;
413
445
  case "shape-trust-bank":
414
- return <ShapeTrustBankNode node={resolvedNode} active={active} lang={lang} />;
446
+ return <ShapeTrustBankNode node={resolvedNode} active={active} lang={lang} textMetrics={textMetrics} />;
415
447
  case "shape-payment-provider":
416
- return <ShapePaymentProviderNode node={resolvedNode} active={active} lang={lang} />;
448
+ return <ShapePaymentProviderNode node={resolvedNode} active={active} lang={lang} textMetrics={textMetrics} />;
417
449
  case "shape-credit-card":
418
- return <ShapeCreditCardNode node={resolvedNode} active={active} />;
450
+ return <ShapeCreditCardNode node={resolvedNode} active={active} textMetrics={textMetrics} />;
419
451
  case "shape-brokerage":
420
- return <ShapeBrokerageNode node={resolvedNode} active={active} lang={lang} />;
452
+ return <ShapeBrokerageNode node={resolvedNode} active={active} lang={lang} textMetrics={textMetrics} />;
421
453
  case "shape-exchange":
422
- return <ShapeExchangeNode node={resolvedNode} active={active} lang={lang} />;
454
+ return <ShapeExchangeNode node={resolvedNode} active={active} lang={lang} textMetrics={textMetrics} />;
423
455
  case "shape-atm":
424
- return <ShapeAtmNode node={resolvedNode} active={active} lang={lang} />;
456
+ return <ShapeAtmNode node={resolvedNode} active={active} lang={lang} textMetrics={textMetrics} />;
425
457
  case "shape-website":
426
- return <ShapeWebsiteNode node={resolvedNode} active={active} />;
458
+ return <ShapeWebsiteNode node={resolvedNode} active={active} textMetrics={textMetrics} />;
427
459
  case "shape-storefront":
428
- return <ShapeStorefrontNode node={resolvedNode} active={active} lang={lang} />;
460
+ return <ShapeStorefrontNode node={resolvedNode} active={active} lang={lang} textMetrics={textMetrics} />;
429
461
  case "shape-warehouse":
430
- return <ShapeWarehouseNode node={resolvedNode} active={active} lang={lang} />;
462
+ return <ShapeWarehouseNode node={resolvedNode} active={active} lang={lang} textMetrics={textMetrics} />;
431
463
  case "shape-online-shop":
432
- return <ShapeOnlineShopNode node={resolvedNode} active={active} />;
464
+ return <ShapeOnlineShopNode node={resolvedNode} active={active} textMetrics={textMetrics} />;
433
465
  case "shape-cdn-edge":
434
- return <ShapeCdnEdgeNode node={resolvedNode} active={active} />;
466
+ return <ShapeCdnEdgeNode node={resolvedNode} active={active} textMetrics={textMetrics} />;
435
467
  case "shape-api-gateway":
436
- return <ShapeApiGatewayNode node={resolvedNode} active={active} lang={lang} />;
468
+ return <ShapeApiGatewayNode node={resolvedNode} active={active} lang={lang} textMetrics={textMetrics} />;
437
469
  case "shape-auditor":
438
470
  return <ShapeAuditorNode node={resolvedNode} active={active} />;
439
471
  case "shape-regulator":
@@ -454,7 +486,16 @@ export function CdlNodeView({
454
486
  return <StationNode node={resolvedNode} active={active} />;
455
487
  default:
456
488
  // 新 NodeKind 25 種は GenericNode で汎用描画 (kind 別 icon / color / shape)
457
- return <GenericNode node={resolvedNode} active={active} value={node.value} stateValues={stateValues} 値の名前={値の名前} />;
489
+ return (
490
+ <GenericNode
491
+ node={resolvedNode}
492
+ active={active}
493
+ value={node.value}
494
+ stateValues={stateValues}
495
+ 値の名前={値の名前}
496
+ textMetrics={textMetrics}
497
+ />
498
+ );
458
499
  }
459
500
  })();
460
501
  // node bounding box を node.w/h に固定するための invisible rect。
@@ -532,7 +573,7 @@ export function CdlNodeView({
532
573
  pointerEvents="none"
533
574
  />
534
575
  {inner}
535
- {箱の上の語(resolvedNode)}
576
+ {箱の上の語(resolvedNode, textMetrics ?? "proportional")}
536
577
  </g>
537
578
  );
538
579
  }
@@ -548,8 +589,13 @@ const FIGURE_EYEBROW_SIZE = 11;
548
589
  const FIGURE_EYEBROW_LIFT = 1.1;
549
590
 
550
591
  /** 箱の幅に収める倍率。 収めないと長い語が箱の外へ出る */
551
- function 幅に収めた大きさ(語: string, 素の大きさ: number, 幅: number): number {
552
- const 実幅 = textWidth(語, 素の大きさ, 0);
592
+ function 幅に収めた大きさ(
593
+ 語: string,
594
+ 素の大きさ: number,
595
+ 幅: number,
596
+ textMetrics: CdlTextMetrics = "proportional",
597
+ ): number {
598
+ const 実幅 = textWidth(語, 素の大きさ, 0, textMetrics);
553
599
  return 実幅 > 幅 ? (素の大きさ * 幅) / 実幅 : 素の大きさ;
554
600
  }
555
601
 
@@ -570,13 +616,16 @@ function 幅に収めた大きさ(語: string, 素の大きさ: number, 幅: num
570
616
  * 題を書かず小見出しだけを書いた図では、小見出しを題の位置に置く。 題の有無に紐付けて
571
617
  * 消すと、題を書かない図で小見出しが出なくなる。
572
618
  */
573
- function 箱の上の語(node: LaidNode): JSX.Element | null {
619
+ function 箱の上の語(
620
+ node: LaidNode,
621
+ textMetrics: CdlTextMetrics = "proportional",
622
+ ): JSX.Element | null {
574
623
  if (!drawsFigureTitle(node.kind)) return null;
575
624
  const 名前 = (node.title ?? "").trim();
576
625
  const 小見出し = (node.eyebrow ?? "").trim();
577
626
  if (名前 === "" && 小見出し === "") return null;
578
627
  const 題のy = node.cy - node.h / 2 - FIGURE_TITLE_GAP;
579
- const 題の大きさ = 幅に収めた大きさ(名前, FIGURE_TITLE_SIZE, node.w);
628
+ const 題の大きさ = 幅に収めた大きさ(名前, FIGURE_TITLE_SIZE, node.w, textMetrics);
580
629
  const 小見出しのy = 名前 === "" ? 題のy : 題のy - 題の大きさ * FIGURE_EYEBROW_LIFT;
581
630
  return (
582
631
  <>
@@ -586,7 +635,7 @@ function 箱の上の語(node: LaidNode): JSX.Element | null {
586
635
  x={node.cx}
587
636
  y={小見出しのy}
588
637
  textAnchor="middle"
589
- fontSize={幅に収めた大きさ(小見出し, FIGURE_EYEBROW_SIZE, node.w)}
638
+ fontSize={幅に収めた大きさ(小見出し, FIGURE_EYEBROW_SIZE, node.w, textMetrics)}
590
639
  fontWeight={600}
591
640
  letterSpacing={0.6}
592
641
  fill="var(--cdl-text-dim, #5a6270)"
@@ -455,6 +455,7 @@ export function CdlStage({
455
455
  active={activeSet.has(node.id)}
456
456
  // 描画側の固定の字の言語 (#847)。 `layout` が既定以外の時だけ `lang` を持たせる
457
457
  lang={laid.lang}
458
+ textMetrics={laid.textMetrics}
458
459
  関係の受け口={箱の受け口}
459
460
  drawing={描く}
460
461
  progress={描く ? 描く進み : progress}
@@ -480,6 +481,7 @@ export function CdlStage({
480
481
  key={`label-${edge.id}`}
481
482
  edge={edge}
482
483
  active={activeSet.has(edge.id)}
484
+ textMetrics={laid.textMetrics}
483
485
  />
484
486
  ))}
485
487
 
@@ -493,6 +495,7 @@ export function CdlStage({
493
495
  edge={edge}
494
496
  active={activeSet.has(edge.id)}
495
497
  端={端}
498
+ textMetrics={laid.textMetrics}
496
499
  />
497
500
  )),
498
501
  )}
@@ -0,0 +1,9 @@
1
+ import type { CdlTextMetrics } from "./types";
2
+
3
+ /**
4
+ * 記法などから届いた値を字幅の測り方へ直す。
5
+ * 完全一致する `"monospace"` 以外は既定の `"proportional"` に落とす。
6
+ */
7
+ export function resolveTextMetrics(value: unknown): CdlTextMetrics {
8
+ return value === "monospace" ? "monospace" : "proportional";
9
+ }
package/src/types.ts CHANGED
@@ -20,6 +20,15 @@ export const TONES = ["accent", "teal", "success", "error", "warning", "info"] a
20
20
 
21
21
  export type Tone = (typeof TONES)[number];
22
22
 
23
+ /**
24
+ * 図の字幅をどの規則で測るか。`"proportional"` は現在の Inter 比例幅表、`"monospace"` は
25
+ * 等幅書体向けの幅を使う。既定は `"proportional"`。
26
+ *
27
+ * 消費側が図を等幅書体で描く場合、比例幅で配置すると英数字の描画幅と箱・札の計算幅が
28
+ * 食い違うため、図単位で測り方を揃えられるようにする。
29
+ */
30
+ export type CdlTextMetrics = "proportional" | "monospace";
31
+
23
32
  /**
24
33
  * cdl SVG に付与される data-cdl-role attribute の SSOT 型 (現在 12 role)。
25
34
  * consumer app (dragon 等) は `[data-cdl-role="<role>"]` selector で CSS を宛てる。 role を追加する時は本 union と該当 render 箇所 (kinds/*.tsx、
@@ -2754,6 +2763,14 @@ export type CdlDiagram = {
2754
2763
  * 型の外の値は `layout` が既定に落とす (記法から文字列で届く場合の書き損じ)。
2755
2764
  */
2756
2765
  lang?: CdlLang;
2766
+ /**
2767
+ * 図の字幅を測る規則。`"proportional"` または `"monospace"` を取り、既定は
2768
+ * `"proportional"`。
2769
+ *
2770
+ * 消費側が等幅書体で描く図では、比例幅の配置計算と実際の描画幅が食い違うため指定する。
2771
+ * 型の外の値は `layout` が既定へ落とす。
2772
+ */
2773
+ textMetrics?: CdlTextMetrics;
2757
2774
  /**
2758
2775
  * この図がどの種類か (#645)。 書かない図は種類を持たない。
2759
2776
  *
@@ -2986,6 +3003,8 @@ export type LaidDiagram = {
2986
3003
  * 無い時は `"en"` で描く。
2987
3004
  */
2988
3005
  lang?: CdlLang;
3006
+ /** 等幅で測る図だけが持つ。無い時は `"proportional"` */
3007
+ textMetrics?: "monospace";
2989
3008
  /** どの種類の図か (#645)。 `CdlDiagram.type` をそのまま持ち越す */
2990
3009
  type?: string;
2991
3010
  viewBox: { x: number; y: number; w: number; h: number };