@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.
- package/CHANGELOG.md +28 -1
- package/README.md +70 -1
- package/dist/index.cjs +1125 -618
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +38 -26
- package/dist/index.d.ts +38 -26
- package/dist/index.js +1125 -619
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +894 -404
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js +894 -404
- package/dist/react.js.map +1 -1
- package/dist/{render-U_pC-Ksh.d.cts → render-U6omSNH2.d.cts} +23 -4
- package/dist/{render-U_pC-Ksh.d.ts → render-U6omSNH2.d.ts} +23 -4
- package/package.json +9 -12
- package/src/builder.ts +5 -0
- package/src/index.ts +2 -0
- package/src/kinds/box-lines.ts +17 -4
- package/src/kinds/chart-bar.tsx +4 -0
- package/src/kinds/chart-gauge.tsx +20 -9
- package/src/kinds/chart-pie.tsx +67 -17
- package/src/kinds/chart-radial.tsx +7 -2
- package/src/kinds/chart-slope.tsx +26 -7
- package/src/kinds/chart-stacked-bar.tsx +8 -4
- package/src/kinds/chart-stat.tsx +19 -4
- package/src/kinds/chart-waffle.tsx +7 -2
- package/src/kinds/funnel.tsx +9 -2
- package/src/kinds/generic.tsx +5 -2
- package/src/kinds/mind-map.tsx +26 -10
- package/src/kinds/quadrant.tsx +14 -4
- package/src/kinds/row-align.ts +19 -7
- package/src/kinds/sequence-board-metrics.ts +13 -3
- package/src/kinds/sequence-board.tsx +4 -2
- package/src/kinds/shape-basement.tsx +57 -32
- package/src/kinds/shape-blockchain.tsx +83 -34
- package/src/kinds/shape-commerce.tsx +65 -24
- package/src/kinds/shape-finance.tsx +90 -26
- package/src/kinds/shape-hardware.tsx +48 -17
- package/src/kinds/shape-region.tsx +16 -6
- package/src/kinds/shape-software.tsx +30 -8
- package/src/kinds/text-width.ts +17 -3
- package/src/kinds/tree.tsx +25 -7
- package/src/layout/collisions.ts +21 -8
- package/src/layout/curve-path.ts +3 -2
- package/src/layout/edges.ts +173 -29
- package/src/layout/head-label.ts +3 -1
- package/src/layout/lanes.ts +3 -2
- package/src/layout/metro-path.ts +3 -2
- package/src/layout/nodes.ts +14 -6
- package/src/layout/predict-bbox.ts +6 -3
- package/src/layout/self-loop.ts +10 -4
- package/src/layout/spec.ts +20 -6
- package/src/layout/text-width.ts +25 -8
- package/src/layout.ts +39 -15
- package/src/presets.ts +69 -43
- package/src/render/edges.tsx +18 -3
- package/src/render/interactive-panel.tsx +34 -7
- package/src/render/nodes.tsx +97 -48
- package/src/render/stage.tsx +3 -0
- package/src/text-metrics.ts +9 -0
- package/src/types.ts +22 -2
- 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 絵の中の字の幅(
|
|
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 打ち消しの倍率(
|
|
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({
|
|
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(
|
|
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={
|
|
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({
|
|
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
|
>
|
package/src/kinds/text-width.ts
CHANGED
|
@@ -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
|
-
*
|
|
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(
|
|
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) {
|
package/src/kinds/tree.tsx
CHANGED
|
@@ -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 {
|
|
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(
|
|
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
|
};
|
package/src/layout/collisions.ts
CHANGED
|
@@ -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(
|
|
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) => ({
|
|
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
|
}
|
package/src/layout/curve-path.ts
CHANGED
|
@@ -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) +
|