@cardenelabs/cdl 0.122.0 → 0.124.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 +28 -1
  2. package/README.md +70 -1
  3. package/dist/index.cjs +1125 -618
  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 +1125 -619
  8. package/dist/index.js.map +1 -1
  9. package/dist/react.cjs +894 -404
  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 +894 -404
  14. package/dist/react.js.map +1 -1
  15. package/dist/{render-U_pC-Ksh.d.cts → render-U6omSNH2.d.cts} +23 -4
  16. package/dist/{render-U_pC-Ksh.d.ts → render-U6omSNH2.d.ts} +23 -4
  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 +173 -29
  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 +22 -2
  64. package/src/visual-validate.ts +67 -17
@@ -2,7 +2,13 @@ import { 等幅の字 } from "./mono-text";
2
2
  import type { JSX } from "react";
3
3
  import { computeLabelBBoxWorld, hasRenderedLabel, LABEL_PILL_PAD_X } from "../layout/spec";
4
4
  import { 端の字の大きさ, 端の字を置く, type 線の端 } from "../layout/head-label";
5
- import { EDGE_HEAD_MARKER_SIZE, type EdgeRole, type LaidEdge, type Tone } from "../types";
5
+ import {
6
+ EDGE_HEAD_MARKER_SIZE,
7
+ type CdlTextMetrics,
8
+ type EdgeRole,
9
+ type LaidEdge,
10
+ type Tone,
11
+ } from "../types";
6
12
  import { edgeHeadMarkerName } from "./edge-head";
7
13
  import { toneColor } from "./tone";
8
14
 
@@ -131,6 +137,7 @@ export function CdlEdgeView({
131
137
  data-cdl-edge-label={edge.label}
132
138
  data-cdl-edge-sub={edge.sub ?? ""}
133
139
  data-cdl-tone={edge.tone}
140
+ data-cdl-edge-role={edge.role}
134
141
  >
135
142
  {isDotted ? (
136
143
  <g>
@@ -349,9 +356,11 @@ export function CdlEdgeGlowView({
349
356
  export function CdlEdgeLabelView({
350
357
  edge,
351
358
  active,
359
+ textMetrics = "proportional",
352
360
  }: {
353
361
  edge: LaidEdge;
354
362
  active: boolean;
363
+ textMetrics?: CdlTextMetrics;
355
364
  }): JSX.Element | null {
356
365
  // routing v9 = 空 label / 空 sub の edge は DOM 描画自体を skip、
357
366
  // collisions.ts 側 (bbox 生成 skip、 CAR-430 で SSOT 統一) と対称に render 層でも noop。
@@ -363,7 +372,7 @@ export function CdlEdgeLabelView({
363
372
  // bbox 計算 drift を解消)。 pill h / w / padding / boxLeftX / boxTopY は本 helper 経由で取得、
364
373
  // <g transform="translate(labelX, labelY)"> 内 rect は相対座標なので world x/y を helper から
365
374
  // 引き算して rect x/y に渡す。
366
- const bbox = computeLabelBBoxWorld(edge);
375
+ const bbox = computeLabelBBoxWorld(edge, textMetrics);
367
376
  const boxW = bbox.w;
368
377
  const boxH = bbox.h;
369
378
  const boxLeftX = bbox.x - edge.labelX;
@@ -383,6 +392,8 @@ export function CdlEdgeLabelView({
383
392
  data-cdl-edge-label-for={edge.id}
384
393
  data-cdl-edge-label-text={edge.label}
385
394
  data-cdl-edge-sub-text={edge.sub ?? ""}
395
+ data-cdl-tone={edge.tone}
396
+ data-cdl-edge-role={edge.role}
386
397
  >
387
398
  {/* 下地は既定で敷く。 丸い下地は箱と同じ形なので、 罫の細い図では外せるようにした (#618) */}
388
399
  {edge.labelPlate !== false && (
@@ -441,12 +452,14 @@ export function CdlEdgeEndLabelView({
441
452
  edge,
442
453
  active,
443
454
  端,
455
+ textMetrics = "proportional",
444
456
  }: {
445
457
  edge: LaidEdge;
446
458
  active: boolean;
447
459
  端: 線の端;
460
+ textMetrics?: CdlTextMetrics;
448
461
  }): JSX.Element | null {
449
- const 置き場所 = 端の字を置く(edge, 端);
462
+ const 置き場所 = 端の字を置く(edge, 端, textMetrics);
450
463
  if (!置き場所) return null;
451
464
  const color = edgeColor(edge);
452
465
  return (
@@ -454,6 +467,8 @@ export function CdlEdgeEndLabelView({
454
467
  data-cdl-role="edge-end-label"
455
468
  data-cdl-edge-end-label-for={edge.id}
456
469
  data-cdl-edge-end={端}
470
+ data-cdl-tone={edge.tone}
471
+ data-cdl-edge-role={edge.role}
457
472
  x={置き場所.x}
458
473
  y={置き場所.y}
459
474
  fontSize={端の字の大きさ}
@@ -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、
@@ -213,7 +222,7 @@ export type EdgeStyle = "solid" | "dotted-flow" | "dashed";
213
222
  * 線の道筋。
214
223
  *
215
224
  * - `"default"` ... 自動。 箱を避け、角を丸めた直角の線を引く。 書かない時も同じ
216
- * - `"back-detour"` ... 箱の上を回って前の箱へ戻る
225
+ * - `"back-detour"` ... 箱の上を回って前の箱へ戻り、途中の箱と交わる時は外側を回る
217
226
  * - `"curve"` ... 始点から水平に出て終点へ水平に入る 3 次曲線。 途中の箱は避けず、
218
227
  * 2 点が同じ高さなら直線になる。 終点の矢印は水平を向く
219
228
  * - `"metro"` ... 横線と傾き ±1 の斜線だけで結び、角を半径 r の円弧で丸める。
@@ -882,7 +891,8 @@ export type CdlEdge = {
882
891
  /** label の y 方向 fine offset (default 0、 + で下、 - で上) */
883
892
  labelOffsetY?: number;
884
893
  /**
885
- * 線の道筋。 `default` は箱を避ける自動経路、 `back-detour` は箱の上を回る戻り線、
894
+ * 線の道筋。 `default` は箱を避ける自動経路、 `back-detour` は箱の上を回り、途中の箱と
895
+ * 交わる時は外側を回る戻り線、
886
896
  * `curve` は途中の箱を避けず、両端で水平になる 3 次曲線。 `metro` は途中の箱を避けず、
887
897
  * 横線と傾き ±1 の斜線を半径 r の円弧で結ぶ。2 点が同じ高さなら横 1 本。書かなければ `default`。
888
898
  */
@@ -2754,6 +2764,14 @@ export type CdlDiagram = {
2754
2764
  * 型の外の値は `layout` が既定に落とす (記法から文字列で届く場合の書き損じ)。
2755
2765
  */
2756
2766
  lang?: CdlLang;
2767
+ /**
2768
+ * 図の字幅を測る規則。`"proportional"` または `"monospace"` を取り、既定は
2769
+ * `"proportional"`。
2770
+ *
2771
+ * 消費側が等幅書体で描く図では、比例幅の配置計算と実際の描画幅が食い違うため指定する。
2772
+ * 型の外の値は `layout` が既定へ落とす。
2773
+ */
2774
+ textMetrics?: CdlTextMetrics;
2757
2775
  /**
2758
2776
  * この図がどの種類か (#645)。 書かない図は種類を持たない。
2759
2777
  *
@@ -2986,6 +3004,8 @@ export type LaidDiagram = {
2986
3004
  * 無い時は `"en"` で描く。
2987
3005
  */
2988
3006
  lang?: CdlLang;
3007
+ /** 等幅で測る図だけが持つ。無い時は `"proportional"` */
3008
+ textMetrics?: "monospace";
2989
3009
  /** どの種類の図か (#645)。 `CdlDiagram.type` をそのまま持ち越す */
2990
3010
  type?: string;
2991
3011
  viewBox: { x: number; y: number; w: number; h: number };