@cardenelabs/cdl 0.5.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 (108) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +343 -0
  3. package/SPEC.md +374 -0
  4. package/dist/index.cjs +28085 -0
  5. package/dist/index.cjs.map +1 -0
  6. package/dist/index.d.cts +3415 -0
  7. package/dist/index.d.ts +3415 -0
  8. package/dist/index.js +27962 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/react.cjs +23043 -0
  11. package/dist/react.cjs.map +1 -0
  12. package/dist/react.d.cts +2 -0
  13. package/dist/react.d.ts +2 -0
  14. package/dist/react.js +23041 -0
  15. package/dist/react.js.map +1 -0
  16. package/dist/render-C78lIXeC.d.cts +2449 -0
  17. package/dist/render-C78lIXeC.d.ts +2449 -0
  18. package/examples/quick-start.md +88 -0
  19. package/package.json +79 -0
  20. package/src/anim/core/easing.ts +77 -0
  21. package/src/anim/core/timeline.ts +335 -0
  22. package/src/anim/core/types.ts +42 -0
  23. package/src/anim/index.ts +20 -0
  24. package/src/anim/react/index.ts +3 -0
  25. package/src/anim/react/useReducedMotion.ts +27 -0
  26. package/src/anim/react/useTimeline.ts +48 -0
  27. package/src/assert-never.ts +16 -0
  28. package/src/author-intent-verify.ts +587 -0
  29. package/src/builder.ts +3740 -0
  30. package/src/compile.ts +12 -0
  31. package/src/dom-verify-core.ts +121 -0
  32. package/src/dom-verify-types.ts +21 -0
  33. package/src/dom-verify.ts +948 -0
  34. package/src/event-handler/index.ts +184 -0
  35. package/src/formula/ast.ts +46 -0
  36. package/src/formula/evaluator.ts +163 -0
  37. package/src/formula/formula-computeds.ts +83 -0
  38. package/src/formula/index.ts +5 -0
  39. package/src/formula/parser.ts +399 -0
  40. package/src/index.ts +284 -0
  41. package/src/input-signals.ts +74 -0
  42. package/src/kinds/actor.tsx +79 -0
  43. package/src/kinds/card.tsx +61 -0
  44. package/src/kinds/chart-bar.tsx +121 -0
  45. package/src/kinds/chart-line.tsx +163 -0
  46. package/src/kinds/chart-pie.tsx +107 -0
  47. package/src/kinds/compact-title.ts +164 -0
  48. package/src/kinds/dyn-shape.tsx +394 -0
  49. package/src/kinds/event.tsx +70 -0
  50. package/src/kinds/function.tsx +53 -0
  51. package/src/kinds/funnel.tsx +122 -0
  52. package/src/kinds/gantt.tsx +193 -0
  53. package/src/kinds/generic.tsx +368 -0
  54. package/src/kinds/mind-map.tsx +367 -0
  55. package/src/kinds/mind-radial.tsx +209 -0
  56. package/src/kinds/node-tone.ts +18 -0
  57. package/src/kinds/quadrant.tsx +164 -0
  58. package/src/kinds/row-align.ts +179 -0
  59. package/src/kinds/shape-basement.tsx +683 -0
  60. package/src/kinds/shape-blockchain.tsx +750 -0
  61. package/src/kinds/shape-commerce.tsx +553 -0
  62. package/src/kinds/shape-finance.tsx +706 -0
  63. package/src/kinds/shape-hardware.tsx +862 -0
  64. package/src/kinds/shape-people.tsx +439 -0
  65. package/src/kinds/shape-region.tsx +383 -0
  66. package/src/kinds/shape-software.tsx +859 -0
  67. package/src/kinds/storage.tsx +180 -0
  68. package/src/kinds/text-width.ts +73 -0
  69. package/src/kinds/tree.tsx +275 -0
  70. package/src/kinds/user-journey.tsx +240 -0
  71. package/src/label-text.ts +71 -0
  72. package/src/layout/clearance-constants.ts +47 -0
  73. package/src/layout/collisions.ts +2078 -0
  74. package/src/layout/edges.ts +2498 -0
  75. package/src/layout/footer-shape.ts +97 -0
  76. package/src/layout/geometry.ts +135 -0
  77. package/src/layout/label-shift.ts +28 -0
  78. package/src/layout/lanes.ts +328 -0
  79. package/src/layout/nodes.ts +190 -0
  80. package/src/layout/predict-bbox.ts +76 -0
  81. package/src/layout/px-projection.ts +176 -0
  82. package/src/layout/spec.ts +872 -0
  83. package/src/layout/text-width.ts +133 -0
  84. package/src/layout/tokens.ts +181 -0
  85. package/src/layout/viewbox.ts +47 -0
  86. package/src/layout-with-validation.ts +175 -0
  87. package/src/layout.ts +381 -0
  88. package/src/presets.ts +2108 -0
  89. package/src/reactive/batch.ts +48 -0
  90. package/src/reactive/computed.ts +85 -0
  91. package/src/reactive/effect.ts +145 -0
  92. package/src/reactive/index.ts +6 -0
  93. package/src/reactive/internal.ts +109 -0
  94. package/src/reactive/signal.ts +113 -0
  95. package/src/render/edges.tsx +318 -0
  96. package/src/render/header.tsx +146 -0
  97. package/src/render/interactive-panel.tsx +9620 -0
  98. package/src/render/nodes.tsx +319 -0
  99. package/src/render/stage.tsx +377 -0
  100. package/src/render/tone.ts +64 -0
  101. package/src/render/utils.ts +238 -0
  102. package/src/render.tsx +221 -0
  103. package/src/scroll-trigger/index.ts +109 -0
  104. package/src/scroll-trigger/progress.ts +49 -0
  105. package/src/thumbnail.tsx +114 -0
  106. package/src/types.ts +2371 -0
  107. package/src/validate.ts +268 -0
  108. package/src/visual-validate.ts +4381 -0
@@ -0,0 +1,133 @@
1
+ /**
2
+ * 文字列の pixel 幅を字種別に精度計算する engine SSOT。
3
+ *
4
+ * label / lane-label / node title 等の bbox 計算に使用。 SVG 実 render (Inter Bold / JetBrains Mono
5
+ * @ Chrome + macOS) の実測 advance width と ±5-10% 精度で一致する char-class × per-char 係数化。
6
+ *
7
+ * 過去の一律 `text.length * 18` は 30-40% 過小評価で G5 (predicted-vs-actual) warn 84 件の
8
+ * 主因だった。 実測係数化で warn を大幅削減する。
9
+ *
10
+ * 係数 SSOT の設計。
11
+ * - 全て `advance / em` (fontSize=1 相当) で保持、 実 fontSize は乗算して算出する
12
+ * - fontFamily は "sans" (Inter Bold @ fontWeight 700) と "mono" (JetBrains Mono) の 2 系統
13
+ * - letterSpacing は char 間 tracking (SVG letter-spacing 属性の world 単位) を charCount 倍で加算
14
+ * - 記号 / 括弧 は char class 別、 特に `(` `)` `:` `.` `,` の実測は naive 平均より狭い / 広い
15
+ * - CJK / 全角 は Inter fallback を経て CJK font (macOS "Hiragino Sans" / "Yu Gothic") で描画、
16
+ * fontSize と等幅 (1.0 em)、 kerning なし
17
+ *
18
+ * Inter Bold @ Chrome 実測 advance width (em 単位)。 主要 char のみ、 未定義は class default に fallback。
19
+ * Bold は Regular に対し英字で +8-15%、 CJK は同じ、 記号は char により大差 (bold ならではの pen 太り)。
20
+ */
21
+
22
+ type FontFamily = "sans" | "mono";
23
+
24
+ interface MeasureOptions {
25
+ fontSize?: number;
26
+ fontWeight?: number;
27
+ letterSpacing?: number;
28
+ fontFamily?: FontFamily;
29
+ }
30
+
31
+ /**
32
+ * Inter Bold 700 @ Chrome + macOS の SVG 実 render 実測 advance width (em 単位)。
33
+ *
34
+ * G5 gate (predicted-vs-actual) 実測で英字 lowercase 平均が Inter Bold 仕様値 0.55 em ではなく
35
+ * 実測 0.68 em (+25%) だと判明したため、 実測係数化して +20-25% 補正した table を採用する。
36
+ * 原因は Chrome の `text-rendering: optimizeLegibility` + subpixel positioning + macOS 側の
37
+ * font fallback (Inter → SF Pro Text 部分置換) 込みで advance width が Inter Regular 仕様より
38
+ * 広く描画されるため。 数値は cookbook / presets / flow-demo の dw 実測から逆算した mean。
39
+ */
40
+ const INTER_BOLD_ADVANCE_EM: Record<string, number> = {
41
+ // uppercase (実測係数化、 Inter Bold 仕様 × 1.20)
42
+ A: 0.86, B: 0.86, C: 0.88, D: 0.93, E: 0.82, F: 0.79, G: 0.94, H: 0.92, I: 0.44,
43
+ J: 0.66, K: 0.86, L: 0.74, M: 1.12, N: 0.94, O: 0.94, P: 0.85, Q: 0.94, R: 0.87,
44
+ S: 0.79, T: 0.82, U: 0.91, V: 0.85, W: 1.22, X: 0.85, Y: 0.82, Z: 0.82,
45
+ // lowercase (実測係数化、 Inter Bold 仕様 × 1.22)
46
+ a: 0.73, b: 0.77, c: 0.68, d: 0.77, e: 0.73, f: 0.51, g: 0.76, h: 0.77, i: 0.39,
47
+ j: 0.41, k: 0.73, l: 0.39, m: 1.15, n: 0.77, o: 0.76, p: 0.77, q: 0.77, r: 0.54,
48
+ s: 0.65, t: 0.54, u: 0.77, v: 0.71, w: 1.05, x: 0.71, y: 0.71, z: 0.66,
49
+ // digits (Inter Bold tabular-nums、 実測係数化 × 1.20)
50
+ "0": 0.72, "1": 0.72, "2": 0.72, "3": 0.72, "4": 0.72, "5": 0.72,
51
+ "6": 0.72, "7": 0.72, "8": 0.72, "9": 0.72,
52
+ // ascii symbols (実測係数化 × 1.15、 punct 系は kerning が広く効くため 1.20)
53
+ " ": 0.35, "!": 0.44, "\"": 0.59, "#": 0.78, "$": 0.72, "%": 1.00, "&": 0.86,
54
+ "'": 0.32, "(": 0.46, ")": 0.46, "*": 0.63, "+": 0.78, ",": 0.36, "-": 0.50,
55
+ ".": 0.36, "/": 0.51, ":": 0.36, ";": 0.36, "<": 0.78, "=": 0.78, ">": 0.78,
56
+ "?": 0.63, "@": 1.17, "[": 0.48, "\\": 0.51, "]": 0.48, "^": 0.63, "_": 0.63,
57
+ "`": 0.48, "{": 0.52, "|": 0.37, "}": 0.52, "~": 0.78,
58
+ };
59
+
60
+ /**
61
+ * JetBrains Mono 実測 advance width (em)。 mono は全 char 等幅の仕様値 0.60 em だが、
62
+ * Chrome 実測 (fontSize 19 の sub label) では 0.63 em (+5%) で描画される。
63
+ */
64
+ const JETBRAINS_MONO_ADVANCE_EM = 0.63;
65
+
66
+ function isCJK(code: number): boolean {
67
+ return (
68
+ (code >= 0x3000 && code <= 0x30ff) || // CJK symbols / hiragana / katakana
69
+ (code >= 0x3400 && code <= 0x9fff) || // CJK ideographs
70
+ (code >= 0xf900 && code <= 0xfaff) || // CJK compat ideographs
71
+ (code >= 0xff01 && code <= 0xff60) || // fullwidth ASCII
72
+ (code >= 0xffe0 && code <= 0xffe6) // fullwidth signs
73
+ );
74
+ }
75
+
76
+ function isHalfwidthKana(code: number): boolean {
77
+ return code >= 0xff61 && code <= 0xffdc;
78
+ }
79
+
80
+ /**
81
+ * char class 別 default advance (em)。 char 表に個別値が無い場合の fallback。
82
+ * Inter Bold で ASCII 平均 = 0.58、 記号平均 = 0.42、 CJK = 1.05 (font-fallback overhang 込)、 半角カナ = 0.60。
83
+ */
84
+ function defaultAdvanceEm(ch: string, code: number): number {
85
+ if (isCJK(code)) return 1.08;
86
+ if (isHalfwidthKana(code)) return 0.72;
87
+ if (ch >= "a" && ch <= "z") return 0.73;
88
+ if (ch >= "A" && ch <= "Z") return 0.86;
89
+ if (ch >= "0" && ch <= "9") return 0.72;
90
+ return 0.65;
91
+ }
92
+
93
+ /**
94
+ * 文字列の SVG render pixel 幅を実測係数で算出する。
95
+ *
96
+ * @param text 対象文字列 (空文字は最低幅にクランプされる)
97
+ * @param opts fontSize / fontWeight / letterSpacing / fontFamily の描画 context
98
+ *
99
+ * default = fontSize 22, fontWeight 700, letterSpacing 0, fontFamily "sans" (Inter Bold)。
100
+ * edge label main text の caller (collisions / predict-bbox / edges) と一致する。
101
+ * sub text (fontSize 19, fontFamily "mono") / lane label (fontSize 20, letterSpacing 2) は
102
+ * opts で明示的に渡す。
103
+ *
104
+ * 最低幅 = fontSize × 1.2 (超短 label で bbox が細くなり shift 判定不安定になる問題を防ぐ)。
105
+ */
106
+ export function measureTextWidth(text: string, opts: MeasureOptions = {}): number {
107
+ const fontSize = opts.fontSize ?? 22;
108
+ const letterSpacing = opts.letterSpacing ?? 0;
109
+ const fontFamily = opts.fontFamily ?? "sans";
110
+ let advanceEmSum = 0;
111
+ let charCount = 0;
112
+ for (const ch of text) {
113
+ charCount++;
114
+ if (fontFamily === "mono") {
115
+ const code = ch.codePointAt(0) ?? 0;
116
+ advanceEmSum += isCJK(code) ? 1.08 : JETBRAINS_MONO_ADVANCE_EM;
117
+ continue;
118
+ }
119
+ const code = ch.codePointAt(0) ?? 0;
120
+ if (isCJK(code)) {
121
+ // CJK 実測 = Chrome + macOS system CJK font (Hiragino Sans / Yu Gothic) で
122
+ // Inter fontSize 22 baseline に対し 1.08 em 前後で描画される (実測係数化)。
123
+ // 1.08 が cookbook / presets / text-dsl の合計 G5 warn 最小 (10 件)、
124
+ // 1.15 以上に上げると flow-demo が過大化して +2-6 件増える tradeoff。
125
+ advanceEmSum += 1.08;
126
+ continue;
127
+ }
128
+ const table = INTER_BOLD_ADVANCE_EM[ch];
129
+ advanceEmSum += table !== undefined ? table : defaultAdvanceEm(ch, code);
130
+ }
131
+ const w = advanceEmSum * fontSize + letterSpacing * charCount;
132
+ return Math.max(w, fontSize * 1.2);
133
+ }
@@ -0,0 +1,181 @@
1
+ import type { NodeKind } from "../types";
2
+
3
+ /**
4
+ * DESIGN TOKENS (全 layout 寸法の SSOT)
5
+ *
6
+ * 全要素は token を参照、 magic number は書かない。
7
+ * label と node の clearance も、 並走 path 同士の gap も、
8
+ * 全部この 1 ブロックで管理することで「キャンバス全体を engine が把握」 する状態を保つ。
9
+ */
10
+ export const TOKENS = {
11
+ // viewBox / lane margins
12
+ header_reserve: 28,
13
+ view_pad: 60,
14
+ lane_label_h: 40,
15
+ lane_top_pad: 100,
16
+ contain_pad: 28,
17
+
18
+ // node sizes (kind ごと、 unified row h は engine が max() で算出)
19
+ node: {
20
+ actor: { w: 340, h: 200 },
21
+ function: { w: 380, h: 170 },
22
+ storage: { w: 400, h: 240 },
23
+ event: { w: 360, h: 200 },
24
+ card: { w: 320, h: 150 },
25
+ // 人系
26
+ person: { w: 280, h: 180 },
27
+ "user-group": { w: 320, h: 180 },
28
+ admin: { w: 280, h: 180 },
29
+ developer: { w: 280, h: 180 },
30
+ "external-user": { w: 300, h: 180 },
31
+ // インフラ
32
+ database: { w: 320, h: 200 },
33
+ cache: { w: 280, h: 160 },
34
+ queue: { w: 340, h: 140 },
35
+ "message-bus": { w: 380, h: 140 },
36
+ cloud: { w: 320, h: 180 },
37
+ cdn: { w: 320, h: 160 },
38
+ // アプリ系
39
+ service: { w: 320, h: 180 },
40
+ api: { w: 320, h: 160 },
41
+ frontend: { w: 320, h: 180 },
42
+ backend: { w: 320, h: 180 },
43
+ webhook: { w: 320, h: 160 },
44
+ microservice: { w: 320, h: 180 },
45
+ // データ / 判定
46
+ signer: { w: 300, h: 180 },
47
+ oracle: { w: 320, h: 180 },
48
+ "merkle-tree": { w: 340, h: 220 },
49
+ decision: { w: 300, h: 180 },
50
+ // Chart 系 (CAR-994 Phase A) ... 1 node に datum 配列を持ち、 kind 内で SVG geometry 描画。
51
+ // 実際の w/h は preset chart() が w/h override で指定 (default fallback は preset 未指定時のみ)。
52
+ "chart-line": { w: 640, h: 360 },
53
+ "chart-pie": { w: 640, h: 320 },
54
+ "chart-bar": { w: 640, h: 360 },
55
+ // Timeline / Radial 系 (CAR-994 Phase B)
56
+ "gantt-timeline": { w: 720, h: 360 },
57
+ "mind-map": { w: 720, h: 480 },
58
+ "mind-radial": { w: 640, h: 640 },
59
+ // Analytic 系 (CAR-994 Phase C)
60
+ "funnel-stages": { w: 560, h: 480 },
61
+ "quadrant-matrix": { w: 640, h: 480 },
62
+ "tree-hierarchy": { w: 720, h: 480 },
63
+ "journey-map": { w: 720, h: 480 },
64
+ // Shape-driven basement 8 (CAR-1099)
65
+ "shape-file": { w: 260, h: 200 },
66
+ "shape-folder": { w: 260, h: 200 },
67
+ "shape-cloud": { w: 320, h: 200 },
68
+ "shape-cylinder": { w: 260, h: 220 },
69
+ "shape-hexagon": { w: 280, h: 200 },
70
+ "shape-diamond": { w: 260, h: 200 },
71
+ "shape-stack": { w: 260, h: 220 },
72
+ "shape-person": { w: 220, h: 240 },
73
+ // Shape-driven software 6 (CAR-1111 Phase 2-B)
74
+ "shape-window": { w: 320, h: 200 },
75
+ "shape-terminal": { w: 340, h: 240 },
76
+ "shape-code-block": { w: 340, h: 220 },
77
+ "shape-kanban-card": { w: 300, h: 200 },
78
+ "shape-message-bubble": { w: 300, h: 200 },
79
+ "shape-gear": { w: 320, h: 320 },
80
+ // Shape-driven hardware 6 (CAR-1111 Phase 2-C)
81
+ "shape-server-rack": { w: 260, h: 260 },
82
+ "shape-network-node": { w: 300, h: 260 },
83
+ "shape-mobile-device": { w: 200, h: 260 },
84
+ "shape-iot-sensor": { w: 260, h: 240 },
85
+ "shape-robot-arm": { w: 260, h: 280 },
86
+ "shape-satellite": { w: 340, h: 260 },
87
+ // Shape-driven blockchain / web3 10 (CAR-1111 Phase 2-D)
88
+ "shape-smart-contract": { w: 300, h: 280 },
89
+ "shape-blockchain-block": { w: 280, h: 280 },
90
+ "shape-rpc-node": { w: 320, h: 260 },
91
+ "shape-wallet": { w: 300, h: 220 },
92
+ "shape-nft": { w: 260, h: 320 },
93
+ "shape-token": { w: 300, h: 320 },
94
+ "shape-blockchain": { w: 380, h: 220 },
95
+ "shape-bitcoin-chain": { w: 380, h: 220 },
96
+ "shape-ethereum-chain": { w: 380, h: 220 },
97
+ "shape-blockchain-node": { w: 300, h: 260 },
98
+ // Shape-driven finance 7 (CAR-1111 Phase 2-D)
99
+ "shape-bank": { w: 340, h: 260 },
100
+ "shape-trust-bank": { w: 340, h: 280 },
101
+ "shape-payment-provider": { w: 300, h: 260 },
102
+ "shape-credit-card": { w: 300, h: 220 },
103
+ "shape-brokerage": { w: 300, h: 300 },
104
+ "shape-exchange": { w: 380, h: 260 },
105
+ "shape-atm": { w: 260, h: 300 },
106
+ // Shape-driven commerce / web 6 (CAR-1111 Phase 2-D)
107
+ "shape-website": { w: 340, h: 280 },
108
+ "shape-storefront": { w: 340, h: 300 },
109
+ "shape-warehouse": { w: 340, h: 280 },
110
+ "shape-online-shop": { w: 260, h: 320 },
111
+ "shape-cdn-edge": { w: 320, h: 300 },
112
+ "shape-api-gateway": { w: 340, h: 280 },
113
+ // dynamic shape 5 (CAR: shape primitive) ... 汎用 container、
114
+ // author が w/h override 前提だがデフォルト固有値を持つ
115
+ "dyn-rect": { w: 120, h: 200 },
116
+ "dyn-circle": { w: 160, h: 160 },
117
+ "dyn-arc": { w: 180, h: 180 },
118
+ "dyn-wave": { w: 140, h: 180 },
119
+ "dyn-polygon": { w: 160, h: 160 },
120
+ // Shape-driven people 6 (CAR-1111 Phase 2-D)
121
+ "shape-auditor": { w: 280, h: 300 },
122
+ "shape-regulator": { w: 280, h: 320 },
123
+ "shape-notary": { w: 280, h: 320 },
124
+ "shape-lawyer": { w: 280, h: 300 },
125
+ "shape-trader": { w: 300, h: 300 },
126
+ "shape-customer-service": { w: 320, h: 300 },
127
+ } as Record<NodeKind, { w: number; h: number }>,
128
+
129
+ // row gap (stack 間の vertical gap、 lane 内 / 跨ぎ共通)
130
+ // edge label が node の上 36 px に配置されるため、 boxH 68 + 36 + 余裕 = 100 必要。
131
+ // pill h / w / padding SSOT は `spec.ts` の LABEL_PILL_H_WITH_SUB / LABEL_PILL_H_MAIN_ONLY /
132
+ // LABEL_PILL_PAD_X (CAR-431 で 3 定義箇所を集約)。
133
+ row_gap: 100,
134
+
135
+ // edge label parallel routing offset (SSOT はここで独立、 pill 高 / padding は spec.ts SSOT)
136
+ label: {
137
+ parallel_x_offset: 140,
138
+ parallel_y_offset: 50,
139
+ clearance_to_path: 18,
140
+ clearance_to_node: 16,
141
+ },
142
+
143
+ // edge stroke
144
+ edge: {
145
+ active_w: 3.5,
146
+ inactive_w: 2,
147
+ glow_w: 14,
148
+ inactive_dash: "4 5",
149
+ },
150
+ } as const;
151
+
152
+ // 互換 alias (既存 layout 関数が参照)
153
+ export const LANE_LABEL_H = TOKENS.lane_label_h;
154
+ export const LANE_TOP_PAD = TOKENS.lane_top_pad;
155
+ export const STACK_GAP = TOKENS.row_gap;
156
+ export const NODE_SIZE = TOKENS.node;
157
+ export const CONTAIN_PAD = TOKENS.contain_pad;
158
+ export const VIEW_PAD = TOKENS.view_pad;
159
+ export const HEADER_RESERVE = TOKENS.header_reserve;
160
+ export const LANE_AUTO_GAP = 80;
161
+
162
+ /**
163
+ * viewport.laneGap / nodeGap / labelMargin の default (CdlViewport で author 上書き可)。
164
+ *
165
+ * - DEFAULT_LANE_GAP=80 ... lanes 間 horizontal gap (LANE_AUTO_GAP と同値、 lane 自動配置で使用)
166
+ * - DEFAULT_NODE_GAP=24 ... nodes 間 vertical gap、 row_gap=100 の base に追加する増分として扱う
167
+ * (row_gap は edge label + node 余白を含む total なので、 nodeGap は author 追加 padding として加算)
168
+ * - DEFAULT_LABEL_MARGIN=8 ... edge label 周辺余白、 author offset として label x/y 計算に加算
169
+ */
170
+ export const DEFAULT_LANE_GAP = 80;
171
+ export const DEFAULT_NODE_GAP = 24;
172
+ export const DEFAULT_LABEL_MARGIN = 8;
173
+
174
+ /**
175
+ * lane を node 幅に合わせて広げる時の左右余白の下限 (world unit)。
176
+ *
177
+ * 著者が lane を十分に離して置いている図では、 policy を満たすための埋め合わせが要らない。
178
+ * その場合に lane が node に張り付かないよう、 見た目の余白としてこの値を下限に使う。
179
+ * 実際の余白は `expandLanesForNodes` が policy から逆算した値と本値の大きい方になる。
180
+ */
181
+ export const LANE_PAD_MIN = 25;
@@ -0,0 +1,47 @@
1
+ import type { BBox, CdlViewport, LaidEdge, LaidLane, LaidNode } from "../types";
2
+ import { footerShapeDrops } from "./footer-shape";
3
+ import { VIEW_PAD } from "./tokens";
4
+
5
+ export function computeViewBox(
6
+ lanes: LaidLane[],
7
+ nodes: LaidNode[],
8
+ edges: LaidEdge[],
9
+ bboxes: BBox[],
10
+ viewport?: CdlViewport,
11
+ ): { x: number; y: number; w: number; h: number } {
12
+ let maxX = 0;
13
+ let maxY = 0;
14
+ let minX = Number.POSITIVE_INFINITY;
15
+ let minY = Number.POSITIVE_INFINITY;
16
+ for (const bbox of bboxes) {
17
+ maxX = Math.max(maxX, bbox.x + bbox.w);
18
+ maxY = Math.max(maxY, bbox.y + bbox.h);
19
+ minX = Math.min(minX, bbox.x);
20
+ minY = Math.min(minY, bbox.y);
21
+ }
22
+ for (const lane of lanes) {
23
+ maxX = Math.max(maxX, lane.x + lane.width);
24
+ maxY = Math.max(maxY, lane.y + lane.height);
25
+ }
26
+ for (const n of nodes) {
27
+ maxX = Math.max(maxX, n.cx + n.w / 2);
28
+ maxY = Math.max(maxY, n.cy + n.h / 2);
29
+ }
30
+ // 下端の名札で下げた絵は箱より下に出る (`footerShapeDrops`)。 その分を図の下端に含めないと
31
+ // 絵と名前が viewBox の外に落ちて切れる = `<svg>` は既定で外側を描かない。
32
+ const footerDrops = footerShapeDrops(lanes, nodes);
33
+ for (const n of nodes) {
34
+ const dy = footerDrops.get(n.id);
35
+ if (dy !== undefined) maxY = Math.max(maxY, n.cy + n.h / 2 + dy);
36
+ }
37
+ if (!Number.isFinite(minX)) minX = 0;
38
+ if (!Number.isFinite(minY)) minY = 0;
39
+ const x = minX - VIEW_PAD;
40
+ const y = minY - VIEW_PAD;
41
+ let w = maxX - x + VIEW_PAD;
42
+ let h = maxY - y + VIEW_PAD;
43
+ // viewport で author が明示宣言した場合は最低 size として強制 (auto 計算結果が小さければそれを優先)
44
+ if (viewport?.width !== undefined && viewport.width > w) w = viewport.width;
45
+ if (viewport?.height !== undefined && viewport.height > h) h = viewport.height;
46
+ return { x, y, w, h };
47
+ }
@@ -0,0 +1,175 @@
1
+ /**
2
+ * layout + visualValidate を 1 回 loop 化して自動修復する SSOT 経路。
3
+ *
4
+ * `layout()` は 1 pass の layout 計算、 17 軸 visualValidate の warn / error は fixWithRetry で
5
+ * 3 loop まで自動修復を試みる。 修復不能な error は throw 前に戻り値の `report.violations` に集約。
6
+ *
7
+ * 呼出しモデル。
8
+ * - CI / test = `layoutWithValidation(diag, { fix: false })` で diagnose のみ
9
+ * - dragon build = `layoutWithValidation(diag)` (fix default true) で修復 pass
10
+ * - 開発中 = `layoutWithValidation(diag, { emitConsole: true })` で warn を console に流す
11
+ *
12
+ * 修復 heuristics (順次適用、 1 loop で全 heuristic を試行 → 再 validate)。
13
+ * - h1 label-offset-reset ... label-char-range / label-inside-viewbox / edge-label-overlap を
14
+ * 持つ edge の labelOffsetX/Y を 0 にリセット (作者の手動 offset で bbox 範囲外に押し出された
15
+ * label を default 位置に戻す)
16
+ * - h2 node-shift-down ... node-overlap error node pair の下 node の stack を +1
17
+ * - h3 edge-back-detour ... edge-node-cross error edge の routing を "back-detour" に切替
18
+ */
19
+ import type { CdlDiagram, CdlEdge, LaidDiagram } from "./types";
20
+ import { layout } from "./layout";
21
+ import { visualValidate, type Violation, type VisualValidationReport } from "./visual-validate";
22
+
23
+ export interface LayoutWithValidationOptions {
24
+ /** 修復 loop の最大回数 (default 3)。 0 で修復 skip。 */
25
+ maxFixLoops?: number;
26
+ /** true なら warn / error を console.warn / console.error に流す (dev のみ推奨)。 */
27
+ emitConsole?: boolean;
28
+ /** true なら修復 heuristics 適用、 false なら diagnose のみ (default true)。 */
29
+ fix?: boolean;
30
+ }
31
+
32
+ export interface LayoutWithValidationResult {
33
+ laid: LaidDiagram;
34
+ report: VisualValidationReport;
35
+ /** 実際に走った修復 loop 数 (0 = 修復不要 or fix:false or maxFixLoops:0) */
36
+ fixLoops: number;
37
+ /** loop ごとに適用された heuristic 名の履歴 */
38
+ appliedHeuristics: string[];
39
+ }
40
+
41
+ export function layoutWithValidation(
42
+ diag: CdlDiagram,
43
+ opts: LayoutWithValidationOptions = {},
44
+ ): LayoutWithValidationResult {
45
+ const maxLoops = opts.maxFixLoops ?? 3;
46
+ const doFix = opts.fix ?? true;
47
+
48
+ let currentDiag: CdlDiagram = diag;
49
+ let laid: LaidDiagram = layout(currentDiag);
50
+ let report: VisualValidationReport = visualValidate(currentDiag);
51
+ const appliedHeuristics: string[] = [];
52
+ let fixLoops = 0;
53
+
54
+ while (doFix && fixLoops < maxLoops && needsRetry(report.violations)) {
55
+ const nextDiag = applyHeuristics(currentDiag, laid, report.violations, appliedHeuristics);
56
+ if (nextDiag === currentDiag) break;
57
+ currentDiag = nextDiag;
58
+ laid = layout(currentDiag);
59
+ report = visualValidate(currentDiag);
60
+ fixLoops++;
61
+ }
62
+
63
+ if (opts.emitConsole) {
64
+ for (const v of report.violations) {
65
+ if (v.severity === "error") {
66
+ console.error(`[cdl geometry ${v.axis}] ${v.detail} (diagram=${v.diagramId})`);
67
+ } else {
68
+ console.warn(`[cdl geometry ${v.axis}] ${v.detail} (diagram=${v.diagramId})`);
69
+ }
70
+ }
71
+ }
72
+
73
+ return { laid, report, fixLoops, appliedHeuristics };
74
+ }
75
+
76
+ const RETRY_AXES = new Set([
77
+ "edge-label-overlap",
78
+ "edge-label-proximity",
79
+ "arrow-endpoint-anchoring",
80
+ "label-char-range",
81
+ "node-overlap",
82
+ "edge-crossing",
83
+ "edge-node-cross",
84
+ "label-inside-viewbox",
85
+ ]);
86
+
87
+ function needsRetry(violations: Violation[]): boolean {
88
+ return violations.some((v) => RETRY_AXES.has(v.axis));
89
+ }
90
+
91
+ function applyHeuristics(
92
+ diag: CdlDiagram,
93
+ laid: LaidDiagram,
94
+ violations: Violation[],
95
+ applied: string[],
96
+ ): CdlDiagram {
97
+ let mutated = false;
98
+ let edges = diag.edges;
99
+ let nodes = diag.nodes;
100
+
101
+ const h1Edges = new Set(
102
+ violations
103
+ .filter(
104
+ (v) =>
105
+ v.axis === "label-char-range" ||
106
+ v.axis === "label-inside-viewbox" ||
107
+ v.axis === "edge-label-overlap",
108
+ )
109
+ .map((v) => extractEdgeId(v.detail))
110
+ .filter((id): id is string => id !== null),
111
+ );
112
+ if (h1Edges.size > 0) {
113
+ const originalEdges = edges;
114
+ edges = edges.map((e) =>
115
+ h1Edges.has(e.id) && (e.labelOffsetX || e.labelOffsetY)
116
+ ? { ...e, labelOffsetX: 0, labelOffsetY: 0 }
117
+ : e,
118
+ );
119
+ if (edges !== originalEdges) {
120
+ applied.push("h1-label-offset-reset");
121
+ mutated = true;
122
+ }
123
+ }
124
+
125
+ const h2NodeIds = new Set<string>();
126
+ for (const v of violations) {
127
+ if (v.axis !== "node-overlap") continue;
128
+ const pair = extractNodePair(v.detail);
129
+ if (pair) {
130
+ const a = laid.nodes.find((n) => n.id === pair.a);
131
+ const b = laid.nodes.find((n) => n.id === pair.b);
132
+ if (a && b) {
133
+ h2NodeIds.add(a.cy > b.cy ? pair.a : pair.b);
134
+ }
135
+ }
136
+ }
137
+ if (h2NodeIds.size > 0) {
138
+ const originalNodes = nodes;
139
+ nodes = nodes.map((n) => (h2NodeIds.has(n.id) ? { ...n, stack: n.stack + 1 } : n));
140
+ if (nodes !== originalNodes) {
141
+ applied.push("h2-node-shift-down");
142
+ mutated = true;
143
+ }
144
+ }
145
+
146
+ const h3Edges = new Set(
147
+ violations
148
+ .filter((v) => v.axis === "edge-node-cross")
149
+ .map((v) => extractEdgeId(v.detail))
150
+ .filter((id): id is string => id !== null),
151
+ );
152
+ if (h3Edges.size > 0) {
153
+ const originalEdges = edges;
154
+ edges = edges.map((e): CdlEdge =>
155
+ h3Edges.has(e.id) && e.routing !== "back-detour" ? { ...e, routing: "back-detour" } : e,
156
+ );
157
+ if (edges !== originalEdges) {
158
+ applied.push("h3-edge-back-detour");
159
+ mutated = true;
160
+ }
161
+ }
162
+
163
+ if (!mutated) return diag;
164
+ return { ...diag, edges, nodes };
165
+ }
166
+
167
+ function extractEdgeId(detail: string): string | null {
168
+ const m = detail.match(/edge "([^"]+)"/) ?? detail.match(/edge-label "([^"]+)"/);
169
+ return m ? m[1]! : null;
170
+ }
171
+
172
+ function extractNodePair(detail: string): { a: string; b: string } | null {
173
+ const m = detail.match(/node "([^"]+)" ↔ "([^"]+)"/);
174
+ return m ? { a: m[1]!, b: m[2]! } : null;
175
+ }