@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
@@ -20,7 +20,7 @@
20
20
  * では名前だけの時と同じ倍率 0.308 まで戻り、 3px の線を約 0.92px で描ける。
21
21
  */
22
22
  import type { JSX } from "react";
23
- import type { LaidNode, NodeKind } from "../types";
23
+ import type { CdlTextMetrics, LaidNode, NodeKind } from "../types";
24
24
  import { MONO_ADVANCE_RATIO, displayWidth } from "./row-align";
25
25
  import { textWidth } from "./text-width";
26
26
  import { 幅で切る } from "./box-lines";
@@ -56,10 +56,11 @@ export function 幅の倍率(
56
56
  size: number,
57
57
  letterSpacing: number,
58
58
  幅: number,
59
+ textMetrics: CdlTextMetrics = "proportional",
59
60
  ): number {
60
61
  if (!(幅 > 0) || !(size > 0)) return 1;
61
62
  // 階級表を二重に持つと、 片方を直した時にもう片方だけ古くなる。
62
- const 実幅 = textWidth(text, size, letterSpacing);
63
+ const 実幅 = textWidth(text, size, letterSpacing, textMetrics);
63
64
  return 実幅 > 幅 ? 幅 / 実幅 : 1;
64
65
  }
65
66
 
@@ -684,10 +685,15 @@ const 帯の高さに対する上限 = 0.8;
684
685
  * 「収まっていない物を収まっていると判定」 する向きに外れる (`row-align.ts` の
685
686
  * `rowColumnWidth` と同じ考え方)。
686
687
  */
687
- export function 絵の中の字の幅(text: string, size: number, letterSpacing = 0): number {
688
+ export function 絵の中の字の幅(
689
+ text: string,
690
+ size: number,
691
+ letterSpacing = 0,
692
+ textMetrics: CdlTextMetrics = "proportional",
693
+ ): number {
688
694
  return Math.max(
689
695
  displayWidth(text) * size * MONO_ADVANCE_RATIO,
690
- textWidth(text, size, letterSpacing),
696
+ textWidth(text, size, letterSpacing, textMetrics),
691
697
  );
692
698
  }
693
699
 
@@ -715,7 +721,11 @@ export function 絵の中の字の幅(text: string, size: number, letterSpacing
715
721
  * 帯の値が有限の正の値でない絵は、収まるかの判定ができない。
716
722
  * `fitArt` が判定できない入力で手を触れないのと同じ向きに倒す。
717
723
  */
718
- export function 打ち消しの倍率(倍率: number, 字ら: readonly 打ち消す字[]): number {
724
+ export function 打ち消しの倍率(
725
+ 倍率: number,
726
+ 字ら: readonly 打ち消す字[],
727
+ textMetrics: CdlTextMetrics = "proportional",
728
+ ): number {
719
729
  const 戻し = Number.isFinite(倍率) && 倍率 > 0 ? 1 / 倍率 : 1;
720
730
  let 上限 = Number.POSITIVE_INFINITY;
721
731
  for (const 字 of 字ら) {
@@ -724,7 +734,7 @@ export function 打ち消しの倍率(倍率: number, 字ら: readonly 打ち消
724
734
  if (Number.isFinite(高さ) && 高さ > 0) {
725
735
  上限 = Math.min(上限, (高さ * 帯の高さに対する上限) / 字.size);
726
736
  }
727
- const 実幅 = 絵の中の字の幅(字.text, 字.size, 字.letterSpacing ?? 0);
737
+ const 実幅 = 絵の中の字の幅(字.text, 字.size, 字.letterSpacing ?? 0, textMetrics);
728
738
  if (Number.isFinite(幅) && 幅 > 0 && 実幅 > 0) {
729
739
  上限 = Math.min(上限, 幅 / 実幅);
730
740
  }
@@ -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 { 字引きを作る, type CdlLang } from "../lang";
4
4
  import {
5
5
  textWidth,
@@ -50,6 +50,10 @@ type ShapeProps = {
50
50
  lang?: CdlLang;
51
51
  };
52
52
 
53
+ type TextMeasuredShapeProps = ShapeProps & {
54
+ textMetrics?: CdlTextMetrics;
55
+ };
56
+
53
57
  const commonFrame = (active: boolean) => ({
54
58
  fill: "var(--cdl-node-fill, #ffffff)",
55
59
  stroke: active ? "var(--cdl-tone-accent, #2d6a8f)" : "var(--cdl-divider, #d4d4d8)",
@@ -345,7 +349,12 @@ export function ShapeTerminalNode({ node, active, lang }: ShapeProps): JSX.Eleme
345
349
  }
346
350
 
347
351
  /** shape-code-block = code editor (tab + gutter + 4 syntax-colored stub lines) */
348
- export function ShapeCodeBlockNode({ node, active, lang }: ShapeProps): JSX.Element {
352
+ export function ShapeCodeBlockNode({
353
+ node,
354
+ active,
355
+ lang,
356
+ textMetrics = "proportional",
357
+ }: TextMeasuredShapeProps): JSX.Element {
349
358
  const 字引 = 字引きを作る(lang);
350
359
  const frame = commonFrame(active);
351
360
  const 線 = frame.strokeWidth / 2;
@@ -364,7 +373,10 @@ export function ShapeCodeBlockNode({ node, active, lang }: ShapeProps): JSX.Elem
364
373
  * 固定のままだと、控えより長い値を渡した図で字が札からはみ出す。 本体の幅を上限に置くのは、
365
374
  * 札が本体より広くなると `path` で描く角丸が本体の右端を越えるため。
366
375
  */
367
- const タブ幅 = Math.min(Math.max(100, textWidth(札, 札の字, 0) + 24), Math.max(0, w));
376
+ const タブ幅 = Math.min(
377
+ Math.max(100, textWidth(札, 札の字, 0, textMetrics) + 24),
378
+ Math.max(0, w),
379
+ );
368
380
  const タブ高 = 24;
369
381
  const 角 = 10;
370
382
  const 溝 = 32;
@@ -422,7 +434,7 @@ export function ShapeCodeBlockNode({ node, active, lang }: ShapeProps): JSX.Elem
422
434
  { text: 札, size: 札の字, 帯: { 幅: タブ幅, 高さ: タブ高 } },
423
435
  { text: "1", size: 行番号の字, 帯: { 幅: 溝 - 持ち上げ, 高さ: 行間 } },
424
436
  ];
425
- const 倍返し = 打ち消しの倍率(倍, 打ち消す字ら);
437
+ const 倍返し = 打ち消しの倍率(倍, 打ち消す字ら, textMetrics);
426
438
  const tabW = タブ幅 * 倍;
427
439
  const tabH = タブ高 * 倍;
428
440
  const 丸 = 角 * 倍;
@@ -456,7 +468,12 @@ export function ShapeCodeBlockNode({ node, active, lang }: ShapeProps): JSX.Elem
456
468
  data-cdl-counter-scaled="1"
457
469
  x={x + tabW / 2}
458
470
  y={y + tabH / 2 + 4 * 倍}
459
- fontSize={札の字 * 倍 * 倍返し * 幅の倍率(札, 札の字 * 倍 * 倍返し, 0, tabW)}
471
+ fontSize={
472
+ 札の字 *
473
+ 倍 *
474
+ 倍返し *
475
+ 幅の倍率(札, 札の字 * 倍 * 倍返し, 0, tabW, textMetrics)
476
+ }
460
477
  fontFamily={lineFont}
461
478
  textAnchor="middle"
462
479
  fill="#e6edf3"
@@ -538,7 +555,12 @@ export function ShapeCodeBlockNode({ node, active, lang }: ShapeProps): JSX.Elem
538
555
  }
539
556
 
540
557
  /** shape-kanban-card = kanban ticket (top color bar + 3 stub line) */
541
- export function ShapeKanbanCardNode({ node, active, lang }: ShapeProps): JSX.Element {
558
+ export function ShapeKanbanCardNode({
559
+ node,
560
+ active,
561
+ lang,
562
+ textMetrics = "proportional",
563
+ }: TextMeasuredShapeProps): JSX.Element {
542
564
  const 字引 = 字引きを作る(lang);
543
565
  const frame = commonFrame(active);
544
566
  const 線 = frame.strokeWidth / 2;
@@ -628,7 +650,7 @@ export function ShapeKanbanCardNode({ node, active, lang }: ShapeProps): JSX.Ele
628
650
  * = 書いてもいない語が図の一部として読める状態だった (見本帳で 3 件)。
629
651
  */
630
652
  const 状態 = 書いた値か控え(node.eyebrow, 字引("kanbanCard.status"));
631
- const 状態の倍率 = 幅の倍率(状態, PILL字, PILL字間, PILL字幅);
653
+ const 状態の倍率 = 幅の倍率(状態, PILL字, PILL字間, PILL字幅, textMetrics);
632
654
  const 説明の字 = 名前.size * 説明字比;
633
655
  // 札の内側の幅。 左右の余白は札と同じ倍率で縮むので、倍率を掛けてから引く
634
656
  const 使える幅 = Math.max(0, w - 左右余白 * 倍 * 2);
@@ -707,7 +729,7 @@ export function ShapeKanbanCardNode({ node, active, lang }: ShapeProps): JSX.Ele
707
729
  <text
708
730
  x={x + w / 2}
709
731
  y={名前.y + 名前.size * 説明基準比}
710
- fontSize={説明の字 * 幅の倍率(説明, 説明の字, 0, 使える幅)}
732
+ fontSize={説明の字 * 幅の倍率(説明, 説明の字, 0, 使える幅, textMetrics)}
711
733
  textAnchor="middle"
712
734
  fill="var(--cdl-text-dim, #5a6270)"
713
735
  >
@@ -5,6 +5,8 @@
5
5
  * (`row-align.ts`) の両方が使うが、 前者から取ると React まで引き込まれる。 測定の検査で
6
6
  * 読込みの時間が計測に乗り、 図の走査時間の予算 (100ms) を超えた (実測 = 226ms)。
7
7
  */
8
+ import type { CdlTextMetrics } from "../types";
9
+ import { measureAdvanceWidth } from "../layout/text-width";
8
10
 
9
11
  /**
10
12
  * 字 1 つが字の大きさに対して占める横幅。
@@ -40,8 +42,8 @@
40
42
  * ASCII の内側は分類を残す。 ここは `W` 1.038 と `;` 0.343 で 3 倍開いており、 最大に寄せると
41
43
  * 普通の名前が 1.5 倍の見積りになって過剰に縮む。
42
44
  *
43
- * 等幅の書体なら全字 0.62 で足りるが、 `shape-` はどれも `font-family` を指定せず継承した
44
- * 比例書体で描くため、 等幅の値は使わない。
45
+ * `textMetrics: "monospace"` の図は JetBrains Mono の実測値を持つ `fontFamily: "mono"` の
46
+ * 経路で測る。 比例幅の図はこの階級表を使い、従来の見積りを保つ。
45
47
  */
46
48
  const WIDE_RATIO = 1.04;
47
49
  const NARROW_RATIO = 0.37;
@@ -58,7 +60,19 @@ const NARROW_CHARS = "ijlI.,:;'! ";
58
60
  * `字送り` (`letter-spacing`) は字ごとに加わるため字数を掛ける。 最後の 1 字ぶんは実装に
59
61
  * よって付かないが、 広い側に外す方を採る。
60
62
  */
61
- export function textWidth(s: string, size: number, 字送り = 0): number {
63
+ export function textWidth(
64
+ s: string,
65
+ size: number,
66
+ 字送り = 0,
67
+ textMetrics: CdlTextMetrics = "proportional",
68
+ ): number {
69
+ if (textMetrics === "monospace") {
70
+ return measureAdvanceWidth(s, {
71
+ fontSize: size,
72
+ letterSpacing: 字送り,
73
+ fontFamily: "mono",
74
+ });
75
+ }
62
76
  let 幅 = 0;
63
77
  let 字数 = 0;
64
78
  for (const c of s) {
@@ -1,8 +1,15 @@
1
1
  import type { JSX } from "react";
2
- import type { LaidNode, TreeNodePayload } from "../types";
2
+ import type { CdlTextMetrics, LaidNode, TreeNodePayload } from "../types";
3
3
  import { TONE } from "../render/tone";
4
4
  import { resolveTreeData } from "../render/payload-binding";
5
- import { 使える幅, 幅に収める, 箱に積む, type 積む行, type 積んだ行 } from "../kinds/box-lines";
5
+ import {
6
+ 使える幅,
7
+ 幅に収める,
8
+ 箱に積む,
9
+ 箱の字を測る,
10
+ type 積む行,
11
+ type 積んだ行,
12
+ } from "../kinds/box-lines";
6
13
  import { measureTextWidth } from "../layout/text-width";
7
14
  import { 進みを畳む, 伸ばすdash, 段の伸び } from "./draw-progress";
8
15
 
@@ -157,10 +164,16 @@ export const 木の枝の太さ = (段: number): number => Math.max(1.1, 2.6 -
157
164
  export function 木の中身(
158
165
  node: LaidNode,
159
166
  stateValues: Record<string, string> = {},
167
+ textMetrics: CdlTextMetrics = "proportional",
160
168
  ): { nodes: LayoutNode[]; edges: LayoutEdge[] } {
161
169
  // 名前は状態を読める (#467)。 解決できない名前は書いたまま残して印を付ける
162
170
  const treeNodes = resolveTreeData(node.treeData ?? [], stateValues);
163
- return computeTreeLayout(treeNodes, node.w - PAD_LEFT - PAD_RIGHT, node.h - PAD_TOP - PAD_BOTTOM);
171
+ return computeTreeLayout(
172
+ treeNodes,
173
+ node.w - PAD_LEFT - PAD_RIGHT,
174
+ node.h - PAD_TOP - PAD_BOTTOM,
175
+ textMetrics,
176
+ );
164
177
  }
165
178
 
166
179
  /**
@@ -182,6 +195,7 @@ export function TreeNode({
182
195
  stateValues = {},
183
196
  drawing = false,
184
197
  progress = 1,
198
+ textMetrics = "proportional",
185
199
  }: {
186
200
  node: LaidNode;
187
201
  active: boolean;
@@ -190,12 +204,15 @@ export function TreeNode({
190
204
  drawing?: boolean;
191
205
  /** phase の進み (0→1)。 `drawing` が true の時だけ読む */
192
206
  progress?: number;
207
+ /** 図全体の字幅規則。省略時は比例幅 */
208
+ textMetrics?: CdlTextMetrics;
193
209
  }): JSX.Element {
194
210
  const x0 = node.cx - node.w / 2;
195
211
  const y0 = node.cy - node.h / 2;
196
212
  const gradientId = `tree-root-grad-${node.id}`;
197
213
 
198
- const layout = 木の中身(node, stateValues);
214
+ const layout = 木の中身(node, stateValues, textMetrics);
215
+ const 測る = 箱の字を測る(textMetrics);
199
216
 
200
217
  /**
201
218
  * 枝がどこまで伸びたか (0..1、 cdl#520)。 描く指定が無ければ常に 1 = 全部出ている。
@@ -273,7 +290,7 @@ export function TreeNode({
273
290
  const isRoot = n.depth === 0;
274
291
  // 補足は箱の中の 2 行目に出す (#471)。 箱の高さは変えないため、 入らない時は落ちる
275
292
  const 材料 = 木の行(n);
276
- const 行 = 箱に積む(材料, n.w, n.h);
293
+ const 行 = 箱に積む(材料, n.w, n.h, 測る);
277
294
  // **判定は行数でなく「補足を渡したか」 で行う**。 行数で見ると、 箱が低くて補足が落ちた時に
278
295
  // 従来の 1 行の経路へ戻り、 幅で切っていない名前が箱から横にはみ出す (#471 Round 1)
279
296
  const 補足あり = (n.subtitle ?? "").trim() !== "";
@@ -283,7 +300,7 @@ export function TreeNode({
283
300
  const 名前 = 材料[0]!;
284
301
  const 収めた名前 = 補足あり
285
302
  ? undefined
286
- : 幅に収める(名前.text, 名前.fontSize, 使える幅(n.w));
303
+ : 幅に収める(名前.text, 名前.fontSize, 使える幅(n.w), 測る);
287
304
  return (
288
305
  <g
289
306
  key={`n-${n.id}`}
@@ -395,6 +412,7 @@ export function computeTreeLayout(
395
412
  treeNodes: TreeNodePayload[],
396
413
  canvasW: number,
397
414
  canvasH: number,
415
+ textMetrics: CdlTextMetrics = "proportional",
398
416
  ): {
399
417
  nodes: LayoutNode[];
400
418
  edges: LayoutEdge[];
@@ -480,7 +498,7 @@ export function computeTreeLayout(
480
498
  const 名前 = 材料[0]!;
481
499
  // `使える幅` が引く左右の余白を足し戻す = 余白の値をここに書き写さない
482
500
  const 要る =
483
- measureTextWidth(名前.text, { fontSize: 名前.fontSize, fontWeight: 600 }) +
501
+ measureTextWidth(名前.text, { fontSize: 名前.fontSize, fontWeight: 600, textMetrics }) +
484
502
  (nodeW - 使える幅(nodeW));
485
503
  return Math.min(Math.max(要る, nodeW), slotW * 0.92);
486
504
  };
@@ -1,4 +1,4 @@
1
- import type { BBox, LaidEdge, LaidLane, LaidNode } from "../types";
1
+ import type { BBox, CdlTextMetrics, LaidEdge, LaidLane, LaidNode } from "../types";
2
2
  import { CHAIN_SHIFT_MAX_ACCUMULATED, computeLabelBBoxWorld, EDGE_STUB_OUT, hasRenderedLabel, LABEL_TO_PATH_CLEARANCE, requiredNearClearance } from "./spec";
3
3
  import { DIST_LABEL_PATH_MAX } from "./clearance-constants";
4
4
  import { footerShapeDrops } from "./footer-shape";
@@ -22,7 +22,12 @@ const NEAR_MISS_MARGIN = LANE_LABEL_CLEARANCE_MARGIN;
22
22
  /**
23
23
  * 全要素の AABB を集約。 engine が「キャンバスに何があるか」 を全部把握する SSOT。
24
24
  */
25
- export function collectBBoxes(lanes: LaidLane[], nodes: LaidNode[], edges: LaidEdge[]): BBox[] {
25
+ export function collectBBoxes(
26
+ lanes: LaidLane[],
27
+ nodes: LaidNode[],
28
+ edges: LaidEdge[],
29
+ textMetrics: CdlTextMetrics = "proportional",
30
+ ): BBox[] {
26
31
  const out: BBox[] = [];
27
32
 
28
33
  for (const lane of lanes) {
@@ -32,7 +37,7 @@ export function collectBBoxes(lanes: LaidLane[], nodes: LaidNode[], edges: LaidE
32
37
  id: lane.id,
33
38
  x: lane.x + 16,
34
39
  y: lane.y + 8,
35
- w: measureTextWidth(lane.label, { fontSize: 20, letterSpacing: 2 }) + 24,
40
+ w: measureTextWidth(lane.label, { fontSize: 20, letterSpacing: 2, textMetrics }) + 24,
36
41
  h: 28,
37
42
  });
38
43
  }
@@ -62,7 +67,7 @@ export function collectBBoxes(lanes: LaidLane[], nodes: LaidNode[], edges: LaidE
62
67
  // だったが、 render 側は boxH = 68 + yOffset shifted で描画するため実測 bbox と layout 予測が
63
68
  // 乖離、 near-collision detection の偽陰性を生んでいた。 本 SSOT 統一で render 実 rect 位置と
64
69
  // 一致する。
65
- const bbox = computeLabelBBoxWorld(e);
70
+ const bbox = computeLabelBBoxWorld(e, textMetrics);
66
71
  out.push({
67
72
  kind: "edge-label",
68
73
  id: e.id,
@@ -1349,6 +1354,7 @@ export function resolveEdgeLabelOverlapsWithChain(
1349
1354
  fixedObstacles: BBox[],
1350
1355
  minClearance = 16,
1351
1356
  maxIter = 20,
1357
+ textMetrics: CdlTextMetrics = "proportional",
1352
1358
  ): LaidEdge[] {
1353
1359
  if (edges.length === 0) return edges;
1354
1360
 
@@ -1356,7 +1362,7 @@ export function resolveEdgeLabelOverlapsWithChain(
1356
1362
  // 本 function / render/edges.tsx / predict-bbox.ts 4 箇所の drift を解消)。 shift 探索中の
1357
1363
  // offsetX / offsetY 追加は label 位置探索の temporal offset なので helper 呼出後に加算する。
1358
1364
  const labelBBox = (e: LaidEdge, offsetX: number, offsetY: number): BBox => {
1359
- const base = computeLabelBBoxWorld(e);
1365
+ const base = computeLabelBBoxWorld(e, textMetrics);
1360
1366
  return {
1361
1367
  kind: "edge-label",
1362
1368
  id: e.id,
@@ -1544,6 +1550,7 @@ export function resolveEdgeLabelOverlapsWithChainAndPropagate(
1544
1550
  * などで label が sibling path 近傍 (LABEL_INIT_CLEARANCE 26) にある正常状態を誤検知しない。
1545
1551
  */
1546
1552
  pathLabelClearance = LABEL_TO_PATH_CLEARANCE,
1553
+ textMetrics: CdlTextMetrics = "proportional",
1547
1554
  ): {
1548
1555
  edges: LaidEdge[];
1549
1556
  nodeShifts: Map<string, { dx: number; dy: number }>;
@@ -1564,7 +1571,7 @@ export function resolveEdgeLabelOverlapsWithChainAndPropagate(
1564
1571
 
1565
1572
  // label bbox 生成 helper (spec.ts SSOT、 shift offset 込み)。
1566
1573
  const labelBBox = (e: LaidEdge, offsetX: number, offsetY: number): BBox => {
1567
- const base = computeLabelBBoxWorld(e);
1574
+ const base = computeLabelBBoxWorld(e, textMetrics);
1568
1575
  return {
1569
1576
  kind: "edge-label",
1570
1577
  id: e.id,
@@ -2597,13 +2604,18 @@ export function 札が動けない理由(
2597
2604
  相手: BBox,
2598
2605
  edges: readonly LaidEdge[],
2599
2606
  obstacles: readonly BBox[],
2607
+ textMetrics: CdlTextMetrics = "proportional",
2600
2608
  ): string | null {
2601
2609
  const edgeById = new Map(edges.map((e) => [e.id, e] as const));
2602
2610
  const 自分 = edgeById.get(labelId);
2603
2611
  if (!自分) return null;
2604
2612
  const labels: BBox[] = edges
2605
2613
  .filter((e) => e.label?.trim() || e.sub?.trim())
2606
- .map((e) => ({ kind: "edge-label" as const, id: e.id, ...computeLabelBBoxWorld(e) }));
2614
+ .map((e) => ({
2615
+ kind: "edge-label" as const,
2616
+ id: e.id,
2617
+ ...computeLabelBBoxWorld(e, textMetrics),
2618
+ }));
2607
2619
  const label = labels.find((l) => l.id === labelId);
2608
2620
  if (!label) return null;
2609
2621
  const 測り方 = 線の測り方を作る();
@@ -2694,6 +2706,7 @@ export function resolveLabelClearanceAfterPathPasses(
2694
2706
  edges: LaidEdge[],
2695
2707
  obstacles: BBox[],
2696
2708
  maxIter = 8,
2709
+ textMetrics: CdlTextMetrics = "proportional",
2697
2710
  ): LaidEdge[] {
2698
2711
  if (edges.length === 0) return edges;
2699
2712
 
@@ -2717,7 +2730,7 @@ export function resolveLabelClearanceAfterPathPasses(
2717
2730
  const out: BBox[] = [];
2718
2731
  for (const e of edges) {
2719
2732
  if (!labeledIds.has(e.id)) continue;
2720
- const base = computeLabelBBoxWorld(e);
2733
+ const base = computeLabelBBoxWorld(e, textMetrics);
2721
2734
  const s = getShift(e.id);
2722
2735
  out.push({ kind: "edge-label", id: e.id, x: base.x + s.dx, y: base.y + s.dy, w: base.w, h: base.h });
2723
2736
  }
@@ -1,4 +1,4 @@
1
- import type { CdlEdge, LaidNode, Side } from "../types";
1
+ import type { CdlEdge, CdlTextMetrics, LaidNode, Side } from "../types";
2
2
  import { computeLabelBoxW, EDGE_STUB_OUT, labelPathClearanceHorizontal, labelPillH } from "./spec";
3
3
 
4
4
  type 箱 = Pick<LaidNode, "cx" | "cy" | "w" | "h">;
@@ -31,6 +31,7 @@ export function curvePath(
31
31
  to: 箱,
32
32
  edge?: Pick<CdlEdge, "label" | "sub" | "style" | "overlay">,
33
33
  labelMarginExtra = 0,
34
+ textMetrics: CdlTextMetrics = "proportional",
34
35
  ): 曲線の結果 {
35
36
  const fromLeft = from.cx - from.w / 2;
36
37
  const fromRight = from.cx + from.w / 2;
@@ -107,7 +108,7 @@ export function curvePath(
107
108
  normalY *= -1;
108
109
  }
109
110
 
110
- const labelW = computeLabelBoxW(edge?.label, edge?.sub);
111
+ const labelW = computeLabelBoxW(edge?.label, edge?.sub, textMetrics);
111
112
  const labelH = labelPillH(edge?.sub);
112
113
  const distance =
113
114
  labelPathClearanceHorizontal(edge) +