@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
@@ -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} />
@@ -7,6 +7,7 @@ import { FunctionNode } from "../kinds/function";
7
7
  import { StorageNode } from "../kinds/storage";
8
8
  import { GenericNode } from "../kinds/generic";
9
9
  import { MarkStartNode, MarkEndNode } from "../kinds/terminal-mark";
10
+ import { StationNode } from "../kinds/station";
10
11
  import { DynShapeNode } from "../kinds/dyn-shape";
11
12
  import { ChartLineNode, type ChartLineScale } from "../kinds/chart-line";
12
13
  import { ChartPieNode } from "../kinds/chart-pie";
@@ -86,7 +87,7 @@ import {
86
87
  ShapeTraderNode,
87
88
  ShapeCustomerServiceNode,
88
89
  } from "../kinds/shape-people";
89
- import type { LaidNode } from "../types";
90
+ import type { CdlTextMetrics, LaidNode } from "../types";
90
91
  import type { CdlLang } from "../lang";
91
92
  import { hasUnresolvedRef, interpolate, resolveNumericAttr } from "./utils";
92
93
  import { drawsFigureTitle } from "../layout/spec";
@@ -120,6 +121,7 @@ export function CdlNodeView({
120
121
  chartLineScale,
121
122
  関係の受け口,
122
123
  lang,
124
+ textMetrics,
123
125
  値の名前,
124
126
  }: {
125
127
  node: LaidNode;
@@ -131,6 +133,8 @@ export function CdlNodeView({
131
133
  値の名前?: 値の名前の表;
132
134
  /** 描画側の固定の字の言語 (#847)。 字を持つ形の部品へ渡す。 渡さない呼び方は英語で描く */
133
135
  lang?: CdlLang;
136
+ /** 図全体の字幅規則。測る描画部品へ明示的に渡す */
137
+ textMetrics?: CdlTextMetrics;
134
138
  /**
135
139
  * この phase の `draw` に載っているか (cdl#512)。 true の間だけ、 対象の種別が
136
140
  * phase の進み (`progress`) に合わせて左の起点から描かれる。
@@ -200,7 +204,13 @@ export function CdlNodeView({
200
204
  return <CardNode node={resolvedNode} active={active} />;
201
205
  // 時系列のやり取り (#578)。 1 つの箱が図を丸ごと描く = 縦が時間で、参加者は箱を持たない
202
206
  case "sequence-board":
203
- return <SequenceBoardNode node={resolvedNode} stateValues={stateValues} />;
207
+ return (
208
+ <SequenceBoardNode
209
+ node={resolvedNode}
210
+ stateValues={stateValues}
211
+ textMetrics={textMetrics}
212
+ />
213
+ );
204
214
  // 図表 9 種は payload が `{signal}` を取れるので stateValues を渡す
205
215
  // (`render/payload-binding.ts` が描画直前に解決する)。 取れる欄は種別で違い、
206
216
  // 数と語を取る 7 種に対し、mind-map / tree-hierarchy は **名前だけ** を取る (#467)。
@@ -224,6 +234,7 @@ export function CdlNodeView({
224
234
  stateValues={stateValues}
225
235
  drawing={drawing}
226
236
  progress={progress}
237
+ textMetrics={textMetrics}
227
238
  />
228
239
  );
229
240
  case "chart-gauge":
@@ -235,6 +246,7 @@ export function CdlNodeView({
235
246
  stateValues={stateValues}
236
247
  drawing={drawing}
237
248
  progress={progress}
249
+ textMetrics={textMetrics}
238
250
  />
239
251
  );
240
252
  case "chart-stacked-bar":
@@ -247,6 +259,7 @@ export function CdlNodeView({
247
259
  stateValues={stateValues}
248
260
  drawing={drawing}
249
261
  progress={progress}
262
+ textMetrics={textMetrics}
250
263
  />
251
264
  );
252
265
  case "chart-slope":
@@ -258,6 +271,7 @@ export function CdlNodeView({
258
271
  stateValues={stateValues}
259
272
  drawing={drawing}
260
273
  progress={progress}
274
+ textMetrics={textMetrics}
261
275
  />
262
276
  );
263
277
  case "chart-waffle":
@@ -269,11 +283,19 @@ export function CdlNodeView({
269
283
  stateValues={stateValues}
270
284
  drawing={drawing}
271
285
  progress={progress}
286
+ textMetrics={textMetrics}
272
287
  />
273
288
  );
274
289
  case "chart-stat":
275
290
  // 数値が主役で、弧は輪郭だけの補助。 段の進みで数値が動く演出は既存の tween が担う
276
- 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
+ );
277
299
  case "chart-radial":
278
300
  // 段の進みでは各輪が 12 時から開く (cdl#715)
279
301
  return (
@@ -283,6 +305,7 @@ export function CdlNodeView({
283
305
  stateValues={stateValues}
284
306
  drawing={drawing}
285
307
  progress={progress}
308
+ textMetrics={textMetrics}
286
309
  />
287
310
  );
288
311
  case "chart-bar":
@@ -313,6 +336,7 @@ export function CdlNodeView({
313
336
  stateValues={stateValues}
314
337
  drawing={drawing}
315
338
  progress={progress}
339
+ textMetrics={textMetrics}
316
340
  />
317
341
  );
318
342
  case "funnel-stages":
@@ -323,10 +347,18 @@ export function CdlNodeView({
323
347
  stateValues={stateValues}
324
348
  drawing={drawing}
325
349
  progress={progress}
350
+ textMetrics={textMetrics}
326
351
  />
327
352
  );
328
353
  case "quadrant-matrix":
329
- 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
+ );
330
362
  case "tree-hierarchy":
331
363
  return (
332
364
  <TreeHierarchyNode
@@ -335,6 +367,7 @@ export function CdlNodeView({
335
367
  stateValues={stateValues}
336
368
  drawing={drawing}
337
369
  progress={progress}
370
+ textMetrics={textMetrics}
338
371
  />
339
372
  );
340
373
  case "journey-map":
@@ -348,91 +381,91 @@ export function CdlNodeView({
348
381
  />
349
382
  );
350
383
  case "shape-file":
351
- return <ShapeFileNode node={resolvedNode} active={active} />;
384
+ return <ShapeFileNode node={resolvedNode} active={active} textMetrics={textMetrics} />;
352
385
  case "shape-folder":
353
- return <ShapeFolderNode node={resolvedNode} active={active} />;
386
+ return <ShapeFolderNode node={resolvedNode} active={active} textMetrics={textMetrics} />;
354
387
  case "shape-cloud":
355
- return <ShapeCloudNode node={resolvedNode} active={active} />;
388
+ return <ShapeCloudNode node={resolvedNode} active={active} textMetrics={textMetrics} />;
356
389
  case "shape-cylinder":
357
- return <ShapeCylinderNode node={resolvedNode} active={active} />;
390
+ return <ShapeCylinderNode node={resolvedNode} active={active} textMetrics={textMetrics} />;
358
391
  case "shape-hexagon":
359
- return <ShapeHexagonNode node={resolvedNode} active={active} />;
392
+ return <ShapeHexagonNode node={resolvedNode} active={active} textMetrics={textMetrics} />;
360
393
  case "shape-diamond":
361
- return <ShapeDiamondNode node={resolvedNode} active={active} />;
394
+ return <ShapeDiamondNode node={resolvedNode} active={active} textMetrics={textMetrics} />;
362
395
  case "shape-stack":
363
- return <ShapeStackNode node={resolvedNode} active={active} />;
396
+ return <ShapeStackNode node={resolvedNode} active={active} textMetrics={textMetrics} />;
364
397
  case "shape-person":
365
- return <ShapePersonNode node={resolvedNode} active={active} />;
398
+ return <ShapePersonNode node={resolvedNode} active={active} textMetrics={textMetrics} />;
366
399
  case "shape-window":
367
400
  return <ShapeWindowNode node={resolvedNode} active={active} />;
368
401
  case "shape-terminal":
369
402
  return <ShapeTerminalNode node={resolvedNode} active={active} lang={lang} />;
370
403
  case "shape-code-block":
371
- return <ShapeCodeBlockNode node={resolvedNode} active={active} lang={lang} />;
404
+ return <ShapeCodeBlockNode node={resolvedNode} active={active} lang={lang} textMetrics={textMetrics} />;
372
405
  case "shape-kanban-card":
373
- return <ShapeKanbanCardNode node={resolvedNode} active={active} lang={lang} />;
406
+ return <ShapeKanbanCardNode node={resolvedNode} active={active} lang={lang} textMetrics={textMetrics} />;
374
407
  case "shape-message-bubble":
375
408
  return <ShapeMessageBubbleNode node={resolvedNode} active={active} />;
376
409
  case "shape-gear":
377
410
  return <ShapeGearNode node={resolvedNode} active={active} />;
378
411
  case "shape-server-rack":
379
- return <ShapeServerRackNode node={resolvedNode} active={active} />;
412
+ return <ShapeServerRackNode node={resolvedNode} active={active} textMetrics={textMetrics} />;
380
413
  case "shape-network-node":
381
- return <ShapeNetworkNodeNode node={resolvedNode} active={active} />;
414
+ return <ShapeNetworkNodeNode node={resolvedNode} active={active} textMetrics={textMetrics} />;
382
415
  case "shape-mobile-device":
383
- return <ShapeMobileDeviceNode node={resolvedNode} active={active} />;
416
+ return <ShapeMobileDeviceNode node={resolvedNode} active={active} textMetrics={textMetrics} />;
384
417
  case "shape-iot-sensor":
385
418
  return <ShapeIotSensorNode node={resolvedNode} active={active} />;
386
419
  case "shape-robot-arm":
387
420
  return <ShapeRobotArmNode node={resolvedNode} active={active} />;
388
421
  case "shape-satellite":
389
- return <ShapeSatelliteNode node={resolvedNode} active={active} />;
422
+ return <ShapeSatelliteNode node={resolvedNode} active={active} textMetrics={textMetrics} />;
390
423
  case "shape-smart-contract":
391
- return <ShapeSmartContractNode node={resolvedNode} active={active} lang={lang} />;
424
+ return <ShapeSmartContractNode node={resolvedNode} active={active} lang={lang} textMetrics={textMetrics} />;
392
425
  case "shape-blockchain-block":
393
- return <ShapeBlockchainBlockNode node={resolvedNode} active={active} lang={lang} />;
426
+ return <ShapeBlockchainBlockNode node={resolvedNode} active={active} lang={lang} textMetrics={textMetrics} />;
394
427
  case "shape-rpc-node":
395
- return <ShapeRpcNodeNode node={resolvedNode} active={active} lang={lang} />;
428
+ return <ShapeRpcNodeNode node={resolvedNode} active={active} lang={lang} textMetrics={textMetrics} />;
396
429
  case "shape-wallet":
397
- return <ShapeWalletNode node={resolvedNode} active={active} />;
430
+ return <ShapeWalletNode node={resolvedNode} active={active} textMetrics={textMetrics} />;
398
431
  case "shape-nft":
399
- return <ShapeNftNode node={resolvedNode} active={active} />;
432
+ return <ShapeNftNode node={resolvedNode} active={active} textMetrics={textMetrics} />;
400
433
  case "shape-token":
401
- return <ShapeTokenNode node={resolvedNode} active={active} />;
434
+ return <ShapeTokenNode node={resolvedNode} active={active} textMetrics={textMetrics} />;
402
435
  case "shape-blockchain":
403
- return <ShapeBlockchainNode node={resolvedNode} active={active} />;
436
+ return <ShapeBlockchainNode node={resolvedNode} active={active} textMetrics={textMetrics} />;
404
437
  case "shape-bitcoin-chain":
405
- return <ShapeBitcoinChainNode node={resolvedNode} active={active} />;
438
+ return <ShapeBitcoinChainNode node={resolvedNode} active={active} textMetrics={textMetrics} />;
406
439
  case "shape-ethereum-chain":
407
- return <ShapeEthereumChainNode node={resolvedNode} active={active} />;
440
+ return <ShapeEthereumChainNode node={resolvedNode} active={active} textMetrics={textMetrics} />;
408
441
  case "shape-blockchain-node":
409
- return <ShapeBlockchainNodeNode node={resolvedNode} active={active} />;
442
+ return <ShapeBlockchainNodeNode node={resolvedNode} active={active} textMetrics={textMetrics} />;
410
443
  case "shape-bank":
411
- return <ShapeBankNode node={resolvedNode} active={active} />;
444
+ return <ShapeBankNode node={resolvedNode} active={active} textMetrics={textMetrics} />;
412
445
  case "shape-trust-bank":
413
- return <ShapeTrustBankNode node={resolvedNode} active={active} lang={lang} />;
446
+ return <ShapeTrustBankNode node={resolvedNode} active={active} lang={lang} textMetrics={textMetrics} />;
414
447
  case "shape-payment-provider":
415
- return <ShapePaymentProviderNode node={resolvedNode} active={active} lang={lang} />;
448
+ return <ShapePaymentProviderNode node={resolvedNode} active={active} lang={lang} textMetrics={textMetrics} />;
416
449
  case "shape-credit-card":
417
- return <ShapeCreditCardNode node={resolvedNode} active={active} />;
450
+ return <ShapeCreditCardNode node={resolvedNode} active={active} textMetrics={textMetrics} />;
418
451
  case "shape-brokerage":
419
- return <ShapeBrokerageNode node={resolvedNode} active={active} lang={lang} />;
452
+ return <ShapeBrokerageNode node={resolvedNode} active={active} lang={lang} textMetrics={textMetrics} />;
420
453
  case "shape-exchange":
421
- return <ShapeExchangeNode node={resolvedNode} active={active} lang={lang} />;
454
+ return <ShapeExchangeNode node={resolvedNode} active={active} lang={lang} textMetrics={textMetrics} />;
422
455
  case "shape-atm":
423
- return <ShapeAtmNode node={resolvedNode} active={active} lang={lang} />;
456
+ return <ShapeAtmNode node={resolvedNode} active={active} lang={lang} textMetrics={textMetrics} />;
424
457
  case "shape-website":
425
- return <ShapeWebsiteNode node={resolvedNode} active={active} />;
458
+ return <ShapeWebsiteNode node={resolvedNode} active={active} textMetrics={textMetrics} />;
426
459
  case "shape-storefront":
427
- return <ShapeStorefrontNode node={resolvedNode} active={active} lang={lang} />;
460
+ return <ShapeStorefrontNode node={resolvedNode} active={active} lang={lang} textMetrics={textMetrics} />;
428
461
  case "shape-warehouse":
429
- return <ShapeWarehouseNode node={resolvedNode} active={active} lang={lang} />;
462
+ return <ShapeWarehouseNode node={resolvedNode} active={active} lang={lang} textMetrics={textMetrics} />;
430
463
  case "shape-online-shop":
431
- return <ShapeOnlineShopNode node={resolvedNode} active={active} />;
464
+ return <ShapeOnlineShopNode node={resolvedNode} active={active} textMetrics={textMetrics} />;
432
465
  case "shape-cdn-edge":
433
- return <ShapeCdnEdgeNode node={resolvedNode} active={active} />;
466
+ return <ShapeCdnEdgeNode node={resolvedNode} active={active} textMetrics={textMetrics} />;
434
467
  case "shape-api-gateway":
435
- return <ShapeApiGatewayNode node={resolvedNode} active={active} lang={lang} />;
468
+ return <ShapeApiGatewayNode node={resolvedNode} active={active} lang={lang} textMetrics={textMetrics} />;
436
469
  case "shape-auditor":
437
470
  return <ShapeAuditorNode node={resolvedNode} active={active} />;
438
471
  case "shape-regulator":
@@ -449,9 +482,20 @@ export function CdlNodeView({
449
482
  return <MarkStartNode node={resolvedNode} active={active} />;
450
483
  case "mark-end":
451
484
  return <MarkEndNode node={resolvedNode} active={active} />;
485
+ case "station":
486
+ return <StationNode node={resolvedNode} active={active} />;
452
487
  default:
453
488
  // 新 NodeKind 25 種は GenericNode で汎用描画 (kind 別 icon / color / shape)
454
- 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
+ );
455
499
  }
456
500
  })();
457
501
  // node bounding box を node.w/h に固定するための invisible rect。
@@ -529,7 +573,7 @@ export function CdlNodeView({
529
573
  pointerEvents="none"
530
574
  />
531
575
  {inner}
532
- {箱の上の語(resolvedNode)}
576
+ {箱の上の語(resolvedNode, textMetrics ?? "proportional")}
533
577
  </g>
534
578
  );
535
579
  }
@@ -545,8 +589,13 @@ const FIGURE_EYEBROW_SIZE = 11;
545
589
  const FIGURE_EYEBROW_LIFT = 1.1;
546
590
 
547
591
  /** 箱の幅に収める倍率。 収めないと長い語が箱の外へ出る */
548
- function 幅に収めた大きさ(語: string, 素の大きさ: number, 幅: number): number {
549
- const 実幅 = textWidth(語, 素の大きさ, 0);
592
+ function 幅に収めた大きさ(
593
+ 語: string,
594
+ 素の大きさ: number,
595
+ 幅: number,
596
+ textMetrics: CdlTextMetrics = "proportional",
597
+ ): number {
598
+ const 実幅 = textWidth(語, 素の大きさ, 0, textMetrics);
550
599
  return 実幅 > 幅 ? (素の大きさ * 幅) / 実幅 : 素の大きさ;
551
600
  }
552
601
 
@@ -567,13 +616,16 @@ function 幅に収めた大きさ(語: string, 素の大きさ: number, 幅: num
567
616
  * 題を書かず小見出しだけを書いた図では、小見出しを題の位置に置く。 題の有無に紐付けて
568
617
  * 消すと、題を書かない図で小見出しが出なくなる。
569
618
  */
570
- function 箱の上の語(node: LaidNode): JSX.Element | null {
619
+ function 箱の上の語(
620
+ node: LaidNode,
621
+ textMetrics: CdlTextMetrics = "proportional",
622
+ ): JSX.Element | null {
571
623
  if (!drawsFigureTitle(node.kind)) return null;
572
624
  const 名前 = (node.title ?? "").trim();
573
625
  const 小見出し = (node.eyebrow ?? "").trim();
574
626
  if (名前 === "" && 小見出し === "") return null;
575
627
  const 題のy = node.cy - node.h / 2 - FIGURE_TITLE_GAP;
576
- const 題の大きさ = 幅に収めた大きさ(名前, FIGURE_TITLE_SIZE, node.w);
628
+ const 題の大きさ = 幅に収めた大きさ(名前, FIGURE_TITLE_SIZE, node.w, textMetrics);
577
629
  const 小見出しのy = 名前 === "" ? 題のy : 題のy - 題の大きさ * FIGURE_EYEBROW_LIFT;
578
630
  return (
579
631
  <>
@@ -583,7 +635,7 @@ function 箱の上の語(node: LaidNode): JSX.Element | null {
583
635
  x={node.cx}
584
636
  y={小見出しのy}
585
637
  textAnchor="middle"
586
- fontSize={幅に収めた大きさ(小見出し, FIGURE_EYEBROW_SIZE, node.w)}
638
+ fontSize={幅に収めた大きさ(小見出し, FIGURE_EYEBROW_SIZE, node.w, textMetrics)}
587
639
  fontWeight={600}
588
640
  letterSpacing={0.6}
589
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、
@@ -186,6 +195,8 @@ export const NODE_KINDS = [
186
195
  // 状態遷移図が使う。 印そのものが意味を持つので文字は描かない
187
196
  "mark-start",
188
197
  "mark-end",
198
+ // 路線図の駅 (#996) ... 線路上の点を小さな丸で示し、名前と説明は丸の外に置く
199
+ "station",
189
200
  // 時系列のやり取り (#578) ... 1 つの箱が図を丸ごと描く。 縦が時間で、参加者は箱を持たない
190
201
  "sequence-board",
191
202
  ] as const;
@@ -207,6 +218,18 @@ export type Side = "top" | "right" | "bottom" | "left";
207
218
  */
208
219
  export type EdgeStyle = "solid" | "dotted-flow" | "dashed";
209
220
 
221
+ /**
222
+ * 線の道筋。
223
+ *
224
+ * - `"default"` ... 自動。 箱を避け、角を丸めた直角の線を引く。 書かない時も同じ
225
+ * - `"back-detour"` ... 箱の上を回って前の箱へ戻る
226
+ * - `"curve"` ... 始点から水平に出て終点へ水平に入る 3 次曲線。 途中の箱は避けず、
227
+ * 2 点が同じ高さなら直線になる。 終点の矢印は水平を向く
228
+ * - `"metro"` ... 横線と傾き ±1 の斜線だけで結び、角を半径 r の円弧で丸める。
229
+ * 途中の箱は避けず、2 点が同じ高さなら横 1 本になる
230
+ */
231
+ export type EdgeRouting = "default" | "back-detour" | "curve" | "metro";
232
+
210
233
  /** 辺の役目 (#618)。 `main` = 図の主となる道 */
211
234
  export type EdgeRole = "main";
212
235
 
@@ -506,8 +529,8 @@ export type CdlNode = {
506
529
  * 変わるのは kind の既定色を使って描かれる部分で、 枠線 / eyebrow の文字色 / icon /
507
530
  * 強調表示中の行の値。 本文の文字色と背景は変わらない。
508
531
  *
509
- * 効くのは `actor` / `function` / `storage` / `event` / `card` と `person` 以降の汎用 kind の
510
- * 計 26 種。 それ以外は自前の配色を持つため指定しても変わらず、 検査も通す。 効く kind の
532
+ * 効くのは `actor` / `function` / `storage` / `event` / `card`、`person` 以降の汎用 kind、
533
+ * 流れの印、路線図の駅。 それ以外は自前の配色を持つため指定しても変わらず、検査も通す。
511
534
  * 一覧は `test/node-styling.test.tsx` が実装から計算した集合と突き合わせている。
512
535
  *
513
536
  * 既定色と同じ名前を明示指定しても色が変わる kind がある (既定色の hex が `render/tone.ts`
@@ -867,8 +890,12 @@ export type CdlEdge = {
867
890
  labelOffsetX?: number;
868
891
  /** label の y 方向 fine offset (default 0、 + で下、 - で上) */
869
892
  labelOffsetY?: number;
870
- /** routing mode (default "default" = 自動 routing、 "back-detour" = back transition 用の上方 detour 強制) */
871
- routing?: "default" | "back-detour";
893
+ /**
894
+ * 線の道筋。 `default` は箱を避ける自動経路、 `back-detour` は箱の上を回る戻り線、
895
+ * `curve` は途中の箱を避けず、両端で水平になる 3 次曲線。 `metro` は途中の箱を避けず、
896
+ * 横線と傾き ±1 の斜線を半径 r の円弧で結ぶ。2 点が同じ高さなら横 1 本。書かなければ `default`。
897
+ */
898
+ routing?: EdgeRouting;
872
899
  /**
873
900
  * CAR-549 SSOT ... label を edge path 上に「被せる」 か「離す」 かの制御 flag。
874
901
  * default (false or undefined) = LABEL_INIT_CLEARANCE (34 world、 CAR-630 で 26 → 34 復帰) 離して配置。
@@ -2736,6 +2763,14 @@ export type CdlDiagram = {
2736
2763
  * 型の外の値は `layout` が既定に落とす (記法から文字列で届く場合の書き損じ)。
2737
2764
  */
2738
2765
  lang?: CdlLang;
2766
+ /**
2767
+ * 図の字幅を測る規則。`"proportional"` または `"monospace"` を取り、既定は
2768
+ * `"proportional"`。
2769
+ *
2770
+ * 消費側が等幅書体で描く図では、比例幅の配置計算と実際の描画幅が食い違うため指定する。
2771
+ * 型の外の値は `layout` が既定へ落とす。
2772
+ */
2773
+ textMetrics?: CdlTextMetrics;
2739
2774
  /**
2740
2775
  * この図がどの種類か (#645)。 書かない図は種類を持たない。
2741
2776
  *
@@ -2968,6 +3003,8 @@ export type LaidDiagram = {
2968
3003
  * 無い時は `"en"` で描く。
2969
3004
  */
2970
3005
  lang?: CdlLang;
3006
+ /** 等幅で測る図だけが持つ。無い時は `"proportional"` */
3007
+ textMetrics?: "monospace";
2971
3008
  /** どの種類の図か (#645)。 `CdlDiagram.type` をそのまま持ち越す */
2972
3009
  type?: string;
2973
3010
  viewBox: { x: number; y: number; w: number; h: number };