@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.
- package/LICENSE +21 -0
- package/README.md +343 -0
- package/SPEC.md +374 -0
- package/dist/index.cjs +28085 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +3415 -0
- package/dist/index.d.ts +3415 -0
- package/dist/index.js +27962 -0
- package/dist/index.js.map +1 -0
- package/dist/react.cjs +23043 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +2 -0
- package/dist/react.d.ts +2 -0
- package/dist/react.js +23041 -0
- package/dist/react.js.map +1 -0
- package/dist/render-C78lIXeC.d.cts +2449 -0
- package/dist/render-C78lIXeC.d.ts +2449 -0
- package/examples/quick-start.md +88 -0
- package/package.json +79 -0
- package/src/anim/core/easing.ts +77 -0
- package/src/anim/core/timeline.ts +335 -0
- package/src/anim/core/types.ts +42 -0
- package/src/anim/index.ts +20 -0
- package/src/anim/react/index.ts +3 -0
- package/src/anim/react/useReducedMotion.ts +27 -0
- package/src/anim/react/useTimeline.ts +48 -0
- package/src/assert-never.ts +16 -0
- package/src/author-intent-verify.ts +587 -0
- package/src/builder.ts +3740 -0
- package/src/compile.ts +12 -0
- package/src/dom-verify-core.ts +121 -0
- package/src/dom-verify-types.ts +21 -0
- package/src/dom-verify.ts +948 -0
- package/src/event-handler/index.ts +184 -0
- package/src/formula/ast.ts +46 -0
- package/src/formula/evaluator.ts +163 -0
- package/src/formula/formula-computeds.ts +83 -0
- package/src/formula/index.ts +5 -0
- package/src/formula/parser.ts +399 -0
- package/src/index.ts +284 -0
- package/src/input-signals.ts +74 -0
- package/src/kinds/actor.tsx +79 -0
- package/src/kinds/card.tsx +61 -0
- package/src/kinds/chart-bar.tsx +121 -0
- package/src/kinds/chart-line.tsx +163 -0
- package/src/kinds/chart-pie.tsx +107 -0
- package/src/kinds/compact-title.ts +164 -0
- package/src/kinds/dyn-shape.tsx +394 -0
- package/src/kinds/event.tsx +70 -0
- package/src/kinds/function.tsx +53 -0
- package/src/kinds/funnel.tsx +122 -0
- package/src/kinds/gantt.tsx +193 -0
- package/src/kinds/generic.tsx +368 -0
- package/src/kinds/mind-map.tsx +367 -0
- package/src/kinds/mind-radial.tsx +209 -0
- package/src/kinds/node-tone.ts +18 -0
- package/src/kinds/quadrant.tsx +164 -0
- package/src/kinds/row-align.ts +179 -0
- package/src/kinds/shape-basement.tsx +683 -0
- package/src/kinds/shape-blockchain.tsx +750 -0
- package/src/kinds/shape-commerce.tsx +553 -0
- package/src/kinds/shape-finance.tsx +706 -0
- package/src/kinds/shape-hardware.tsx +862 -0
- package/src/kinds/shape-people.tsx +439 -0
- package/src/kinds/shape-region.tsx +383 -0
- package/src/kinds/shape-software.tsx +859 -0
- package/src/kinds/storage.tsx +180 -0
- package/src/kinds/text-width.ts +73 -0
- package/src/kinds/tree.tsx +275 -0
- package/src/kinds/user-journey.tsx +240 -0
- package/src/label-text.ts +71 -0
- package/src/layout/clearance-constants.ts +47 -0
- package/src/layout/collisions.ts +2078 -0
- package/src/layout/edges.ts +2498 -0
- package/src/layout/footer-shape.ts +97 -0
- package/src/layout/geometry.ts +135 -0
- package/src/layout/label-shift.ts +28 -0
- package/src/layout/lanes.ts +328 -0
- package/src/layout/nodes.ts +190 -0
- package/src/layout/predict-bbox.ts +76 -0
- package/src/layout/px-projection.ts +176 -0
- package/src/layout/spec.ts +872 -0
- package/src/layout/text-width.ts +133 -0
- package/src/layout/tokens.ts +181 -0
- package/src/layout/viewbox.ts +47 -0
- package/src/layout-with-validation.ts +175 -0
- package/src/layout.ts +381 -0
- package/src/presets.ts +2108 -0
- package/src/reactive/batch.ts +48 -0
- package/src/reactive/computed.ts +85 -0
- package/src/reactive/effect.ts +145 -0
- package/src/reactive/index.ts +6 -0
- package/src/reactive/internal.ts +109 -0
- package/src/reactive/signal.ts +113 -0
- package/src/render/edges.tsx +318 -0
- package/src/render/header.tsx +146 -0
- package/src/render/interactive-panel.tsx +9620 -0
- package/src/render/nodes.tsx +319 -0
- package/src/render/stage.tsx +377 -0
- package/src/render/tone.ts +64 -0
- package/src/render/utils.ts +238 -0
- package/src/render.tsx +221 -0
- package/src/scroll-trigger/index.ts +109 -0
- package/src/scroll-trigger/progress.ts +49 -0
- package/src/thumbnail.tsx +114 -0
- package/src/types.ts +2371 -0
- package/src/validate.ts +268 -0
- package/src/visual-validate.ts +4381 -0
package/src/layout.ts
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import type { CdlDiagram, LaidDiagram } from "./types";
|
|
2
|
+
import {
|
|
3
|
+
CLEARANCE_LABEL_LABEL,
|
|
4
|
+
CLEARANCE_NODE_LABEL,
|
|
5
|
+
} from "./layout/clearance-constants";
|
|
6
|
+
import {
|
|
7
|
+
collectBBoxes,
|
|
8
|
+
detectCollisions,
|
|
9
|
+
detectNearCollisions,
|
|
10
|
+
resolveEdgeLabelOverlapsWithChainAndPropagate,
|
|
11
|
+
resolveOverlaps,
|
|
12
|
+
} from "./layout/collisions";
|
|
13
|
+
import { bowSharedStraightPaths, layoutEdges, repositionParallelLabels } from "./layout/edges";
|
|
14
|
+
import {
|
|
15
|
+
expandLaneGapsForEdgeLabels,
|
|
16
|
+
expandLanesForNodes,
|
|
17
|
+
layoutLanes,
|
|
18
|
+
recomputeContainedLanes,
|
|
19
|
+
} from "./layout/lanes";
|
|
20
|
+
import { layoutNodes } from "./layout/nodes";
|
|
21
|
+
import { computeViewBox } from "./layout/viewbox";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* chain 伝搬 shift の escape hatch (CAR-428 で導入、 CAR-430 で label-shift.ts 削除後も残置)。
|
|
25
|
+
*
|
|
26
|
+
* default = ON、 env `CDL_DISABLE_CHAIN_EDGE_LABEL=1` で OFF (chain 伝搬 shift を bypass)。
|
|
27
|
+
* CAR-430 で `label-shift.ts` の shift 探索は削除済のため、 OFF 時は
|
|
28
|
+
* `edges.ts routePath()` が返す init 位置がそのまま最終 label 位置になる。
|
|
29
|
+
* snapshot / preset regression 発生時の切戻し用として維持する。
|
|
30
|
+
*/
|
|
31
|
+
function isChainEdgeLabelDisabled(): boolean {
|
|
32
|
+
if (typeof process === "undefined") return false;
|
|
33
|
+
return process.env?.CDL_DISABLE_CHAIN_EDGE_LABEL === "1";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 同じ集合に属する node の shift を 1 つに集約する (Issue #202 の共通 logic)。
|
|
38
|
+
*
|
|
39
|
+
* 絶対値が最大のものを採用する。 最も強く押されている node が収まる位置まで集合全体を動かせば、
|
|
40
|
+
* 同じ向きに押されている他の node も干渉から外れる。
|
|
41
|
+
*
|
|
42
|
+
* ただし集合内で正負が混在する場合、 どちらを採用しても反対側の node の干渉が残る。
|
|
43
|
+
* この場合は集合を動かさない。 整列を壊してまで片側だけを救うことはせず、
|
|
44
|
+
* 残った干渉は visualValidate が検出する。
|
|
45
|
+
*/
|
|
46
|
+
function aggregateByGroup<K>(entries: readonly (readonly [K, number])[]): Map<K, number> {
|
|
47
|
+
const byGroup = new Map<K, number[]>();
|
|
48
|
+
for (const [key, delta] of entries) {
|
|
49
|
+
if (delta === 0) continue;
|
|
50
|
+
byGroup.set(key, [...(byGroup.get(key) ?? []), delta]);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const out = new Map<K, number>();
|
|
54
|
+
for (const [key, deltas] of byGroup) {
|
|
55
|
+
const hasNeg = deltas.some((d) => d < 0);
|
|
56
|
+
const hasPos = deltas.some((d) => d > 0);
|
|
57
|
+
if (hasNeg && hasPos) continue;
|
|
58
|
+
let strongest = 0;
|
|
59
|
+
for (const d of deltas) {
|
|
60
|
+
if (Math.abs(d) > Math.abs(strongest)) strongest = d;
|
|
61
|
+
}
|
|
62
|
+
out.set(key, strongest);
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* chain 伝搬の縦 shift を row (stack) 単位に集約する (Issue #202)。
|
|
69
|
+
*
|
|
70
|
+
* layout/nodes.ts は「同 stack 番号の node は全 lane で同じ cy になり、 lane 間 edge が
|
|
71
|
+
* 水平で揃う」 を不変条件として保証する。 chain 伝搬が node を 1 件ずつ縦に動かすと
|
|
72
|
+
* この不変条件が壊れ、 visualValidate の row-alignment が発火する。
|
|
73
|
+
*
|
|
74
|
+
* @param nodes 集約対象の node (id と stack のみ参照)
|
|
75
|
+
* @param nodeShifts chain 伝搬が返した node 別 shift
|
|
76
|
+
* @returns stack → 適用する dy。 shift 不要な stack と符号混在の stack は含まない
|
|
77
|
+
*/
|
|
78
|
+
export function aggregateRowDy(
|
|
79
|
+
nodes: readonly { id: string; stack: number }[],
|
|
80
|
+
nodeShifts: ReadonlyMap<string, { dx: number; dy: number }>,
|
|
81
|
+
): Map<number, number> {
|
|
82
|
+
return aggregateByGroup(nodes.map((n) => [n.stack, nodeShifts.get(n.id)?.dy ?? 0] as const));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* chain 伝搬の横 shift を lane 単位に集約する (Issue #202)。
|
|
87
|
+
*
|
|
88
|
+
* layout/nodes.ts は node の cx を所属 lane の中心に固定する。 chain 伝搬が node を
|
|
89
|
+
* 1 件ずつ横に動かすと同 lane 内の cx が揃わなくなり、 visualValidate の alignment /
|
|
90
|
+
* column-alignment が発火する (row 方向で cy が揃わなくなるのと同じ構造)。
|
|
91
|
+
*
|
|
92
|
+
* 実測 (dragon golden の T06 microservice mesh)。 label 回避のため svc1 だけが 100 world 左へ
|
|
93
|
+
* 押され、 同 lane の gw / svc2 / svc3 と cx がずれていた。 lane 単位に集約すると 4 node が
|
|
94
|
+
* 揃って動き、 整列を保ったまま label 回避が効く。
|
|
95
|
+
*
|
|
96
|
+
* @param nodes 集約対象の node (id と lane のみ参照)
|
|
97
|
+
* @param nodeShifts chain 伝搬が返した node 別 shift
|
|
98
|
+
* @returns lane → 適用する dx。 shift 不要な lane と符号混在の lane は含まない
|
|
99
|
+
*/
|
|
100
|
+
export function aggregateLaneDx(
|
|
101
|
+
nodes: readonly { id: string; lane: string }[],
|
|
102
|
+
nodeShifts: ReadonlyMap<string, { dx: number; dy: number }>,
|
|
103
|
+
): Map<string, number> {
|
|
104
|
+
return aggregateByGroup(nodes.map((n) => [n.lane, nodeShifts.get(n.id)?.dx ?? 0] as const));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* lane pitch の均一性を壊す横 shift を捨てる (Issue #202)。
|
|
109
|
+
*
|
|
110
|
+
* lane 中心間距離 (pitch) は `expandLaneGapsForEdgeLabels` が全対で均一に揃えている (PR #206)。
|
|
111
|
+
* visualValidate の row-gap-uniform は「同 stack の node の cx 差が揃うか」 を見るが、
|
|
112
|
+
* これは 3 node 以上が同 stack に載る時にしか判定されない。
|
|
113
|
+
*
|
|
114
|
+
* lane を 1 本だけ横に動かすと、 その lane の node が属する stack の cx 差が変わる。
|
|
115
|
+
* その stack に 3 node 以上あれば pitch のばらつきとして検出される
|
|
116
|
+
* (Codex review 実測 = 3 lane / 1 row で variance 200 > spec 40)。
|
|
117
|
+
*
|
|
118
|
+
* よって「動かす lane の node が属する stack のどれかに 3 node 以上ある」 lane の dx は捨てる。
|
|
119
|
+
* それ以外 (T 字型の疎な図など) では横 shift を効かせたまま label 回避ができる。
|
|
120
|
+
*
|
|
121
|
+
* @param dxByLane lane 単位に集約した dx
|
|
122
|
+
* @param nodes 全 node (lane / stack のみ参照)
|
|
123
|
+
* @returns 安全な dx だけを残した map
|
|
124
|
+
*/
|
|
125
|
+
function dropUnsafeLaneDx(
|
|
126
|
+
dxByLane: Map<string, number>,
|
|
127
|
+
nodes: readonly { lane: string; stack: number }[],
|
|
128
|
+
): Map<string, number> {
|
|
129
|
+
if (dxByLane.size === 0) return dxByLane;
|
|
130
|
+
|
|
131
|
+
const nodeCountByStack = new Map<number, number>();
|
|
132
|
+
for (const n of nodes) nodeCountByStack.set(n.stack, (nodeCountByStack.get(n.stack) ?? 0) + 1);
|
|
133
|
+
|
|
134
|
+
const crowdedLanes = new Set<string>();
|
|
135
|
+
for (const n of nodes) {
|
|
136
|
+
if ((nodeCountByStack.get(n.stack) ?? 0) >= 3) crowdedLanes.add(n.lane);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const out = new Map<string, number>();
|
|
140
|
+
for (const [lane, dx] of dxByLane) {
|
|
141
|
+
if (crowdedLanes.has(lane)) continue;
|
|
142
|
+
out.set(lane, dx);
|
|
143
|
+
}
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* chain 伝搬の shift を row / lane 単位に集約して node と lane に適用する (Issue #202)。
|
|
149
|
+
*
|
|
150
|
+
* 縦は `aggregateRowDy` (同 stack で共通)、 横は `aggregateLaneDx` (同 lane で共通)。
|
|
151
|
+
* layoutNodes が保証する 2 つの不変条件 (同 stack は同 cy / 同 lane は同 cx) を保ったまま、
|
|
152
|
+
* label 回避の押し出しを効かせる。
|
|
153
|
+
*
|
|
154
|
+
* lane 単位の dx は lane 矩形にも同じ量を適用する。 node の cx は lane 中心に固定される
|
|
155
|
+
* 前提なので、 node だけ動かすと lane からはみ出し `node-inside-lane` /
|
|
156
|
+
* `lane-cx-consistency` が発火する (Codex review 指摘)。
|
|
157
|
+
*
|
|
158
|
+
* **node も lane も 1 件も動かない場合は null を返す。** 呼び元はこの時 node 配列を差し替えず、
|
|
159
|
+
* 元の参照を保つ必要がある。 参照が変わると layout.ts が「node が動いた」 と判定して
|
|
160
|
+
* `layoutEdges` を再実行し、 chain 伝搬が確定させた label 位置 (labelX / labelY) を
|
|
161
|
+
* 捨ててしまう。 符号混在で集合を動かさない場合がまさにこの経路に該当する。
|
|
162
|
+
*
|
|
163
|
+
* @param nodes 適用対象の node
|
|
164
|
+
* @param lanes 適用対象の lane
|
|
165
|
+
* @param nodeShifts chain 伝搬が返した node 別 shift
|
|
166
|
+
* @returns 座標が変わった node / lane。 1 件も動かないなら null
|
|
167
|
+
*/
|
|
168
|
+
export function applyRowLevelNodeShifts<
|
|
169
|
+
N extends { id: string; stack: number; lane: string; cx: number; cy: number },
|
|
170
|
+
L extends { id: string; x: number },
|
|
171
|
+
>(
|
|
172
|
+
nodes: readonly N[],
|
|
173
|
+
lanes: readonly L[],
|
|
174
|
+
nodeShifts: ReadonlyMap<string, { dx: number; dy: number }>,
|
|
175
|
+
): { nodes: N[]; lanes: L[] } | null {
|
|
176
|
+
const dyByStack = aggregateRowDy(nodes, nodeShifts);
|
|
177
|
+
const dxByLane = dropUnsafeLaneDx(aggregateLaneDx(nodes, nodeShifts), nodes);
|
|
178
|
+
let moved = false;
|
|
179
|
+
const outNodes = nodes.map((n) => {
|
|
180
|
+
const dx = dxByLane.get(n.lane) ?? 0;
|
|
181
|
+
const dy = dyByStack.get(n.stack) ?? 0;
|
|
182
|
+
if (dx === 0 && dy === 0) return n;
|
|
183
|
+
moved = true;
|
|
184
|
+
return { ...n, cx: n.cx + dx, cy: n.cy + dy };
|
|
185
|
+
});
|
|
186
|
+
const outLanes = lanes.map((l) => {
|
|
187
|
+
const dx = dxByLane.get(l.id) ?? 0;
|
|
188
|
+
if (dx === 0) return l;
|
|
189
|
+
moved = true;
|
|
190
|
+
return { ...l, x: l.x + dx };
|
|
191
|
+
});
|
|
192
|
+
return moved ? { nodes: outNodes, lanes: outLanes } : null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Layout engine
|
|
197
|
+
*
|
|
198
|
+
* 規約 (SPEC.md §Layout engine 規約 と一致)。
|
|
199
|
+
* - lane が x 軸 column を決める
|
|
200
|
+
* - node は lane 内 stack 順で縦並び、 各 stack 間 gap 80px、 上端 80px
|
|
201
|
+
* - lane label は lane 上部 30px 高
|
|
202
|
+
* - contain: true の lane は内部 node 全部を 24px padding で囲む boundary
|
|
203
|
+
* - edge routing は orthogonal L 字 (起点 side → 中継 → 終点 side)、 左右 lane 跨ぎは S 字 bezier
|
|
204
|
+
* - edge label は path 中点から path 進行方向の垂直方向に 50-80px、 node bbox を避ける
|
|
205
|
+
* - viewBox は cdl が auto 計算 (右端 +80 / 下端 +80)
|
|
206
|
+
*/
|
|
207
|
+
export function layout(diag: CdlDiagram): LaidDiagram {
|
|
208
|
+
const lanes = layoutLanes(diag);
|
|
209
|
+
const rawNodes = layoutNodes(diag, lanes);
|
|
210
|
+
const expandedLanes = expandLanesForNodes(lanes, rawNodes);
|
|
211
|
+
// CAR-470 SSOT ... 隣接 lane 対を跨ぐ edge label の収納幅を engine 側で先手確保。
|
|
212
|
+
// 拡張後、 後続 lane は cumulative shift で連鎖 shift される。 lane.width は
|
|
213
|
+
// `expandLanesForNodes` 後の値のまま (node 収納 phase と gap 拡張 phase を分離)。
|
|
214
|
+
const gapExpandedLanes = expandLaneGapsForEdgeLabels(expandedLanes, rawNodes, diag.edges);
|
|
215
|
+
const nodes = rawNodes.map((n) => {
|
|
216
|
+
// posX / posY を明示した node は「user が固定した絶対座標」 なので lane 中心に引き戻さない。
|
|
217
|
+
//
|
|
218
|
+
// 本 map は gap 拡張で lane が動いた分を node に反映するためのものだが、 無条件に
|
|
219
|
+
// `lane.x + lane.width / 2` を代入していたため、 `layoutNodes` が尊重した posX が
|
|
220
|
+
// ここで捨てられていた。 結果 **全 diagram type で node の横位置を指定できず**、
|
|
221
|
+
// 「lane = actor」 の type (sequence / solidity / swimlane) 以外は自由配置が不可能だった。
|
|
222
|
+
//
|
|
223
|
+
// 実測 (修正前) = flow / state / topology のいずれも posX: 500 を書いて cx が変わらない。
|
|
224
|
+
if (n.posX !== undefined && n.posY !== undefined) return n;
|
|
225
|
+
const oldLane = lanes.find((l) => l.id === n.lane);
|
|
226
|
+
const newLane = gapExpandedLanes.find((l) => l.id === n.lane);
|
|
227
|
+
if (!oldLane || !newLane) return n;
|
|
228
|
+
return { ...n, cx: newLane.x + newLane.width / 2 };
|
|
229
|
+
});
|
|
230
|
+
const containedLanesPre = recomputeContainedLanes(gapExpandedLanes, nodes);
|
|
231
|
+
const edgesPre = layoutEdges(diag, nodes, containedLanesPre);
|
|
232
|
+
|
|
233
|
+
// 衝突 → 自動再配置 (同 kind=node 同士のみ、 sequence preset の lifeline 等は据え置き)。
|
|
234
|
+
// node の cy を shift した後は lane の contain bbox と edge routing も再計算が必要。
|
|
235
|
+
const bboxesPre = collectBBoxes(containedLanesPre, nodes, edgesPre);
|
|
236
|
+
const collisionsPre = detectCollisions(bboxesPre, edgesPre);
|
|
237
|
+
const nodesResolved = resolveOverlaps(nodes, collisionsPre);
|
|
238
|
+
const moved = nodesResolved !== nodes;
|
|
239
|
+
const containedLanes = moved ? recomputeContainedLanes(gapExpandedLanes, nodesResolved) : containedLanesPre;
|
|
240
|
+
const edgesAfterMove = moved ? layoutEdges(diag, nodesResolved, containedLanes) : edgesPre;
|
|
241
|
+
|
|
242
|
+
// post-pass (CAR-430 = SSOT 一本化) ... edges.ts routePath() が返す init 位置を SSOT とし、
|
|
243
|
+
// label 重複が生じた場合のみ chain 伝搬 shift (resolveEdgeLabelOverlapsWithChain) で解消。
|
|
244
|
+
//
|
|
245
|
+
// 旧実装 (CAR-430 以前) は post-pass 1 = shiftLabelsAwayFromNodes (16 方向 × 26 段階全探索) +
|
|
246
|
+
// post-pass 2 = resolveEdgeLabelOverlapsWithChain の 2 段構成で label 位置を再計算していた。
|
|
247
|
+
// 二重権限による予測困難な label 配置を解消するため、 CAR-430 で post-pass 1 を削除、
|
|
248
|
+
// chain 伝搬 shift を label 重複解消の単一 SSOT にした。
|
|
249
|
+
//
|
|
250
|
+
// fixedObstacles = node + lane-label bbox のみ (edge-path は resolveEdgeLabelOverlapsWithChain
|
|
251
|
+
// の設計 SSOT に従い chain shift 対象外)。 escape hatch = env `CDL_DISABLE_CHAIN_EDGE_LABEL=1` で
|
|
252
|
+
// OFF にすると edges.ts init 位置がそのまま最終 label 位置になる。
|
|
253
|
+
//
|
|
254
|
+
// minClearance = SSOT clearance の max (CLEARANCE_LABEL_LABEL=36 と CLEARANCE_NODE_LABEL=32)。
|
|
255
|
+
// 旧 default 16 では chain shift 後に near collision (32/36 base の閾値) が残る問題を
|
|
256
|
+
// 解消するため、 pair 別 clearance の worst case で shift 量を決める。
|
|
257
|
+
const chainShiftClearance = Math.max(CLEARANCE_LABEL_LABEL, CLEARANCE_NODE_LABEL);
|
|
258
|
+
// CAR-482 = chain 伝搬拡張版 (resolveEdgeLabelOverlapsWithChainAndPropagate) を採用、 label 移動先方向
|
|
259
|
+
// の node / lane-label も同方向同量 shift 連鎖。 既存 preset で label overlap 未発火なら shift 0 で
|
|
260
|
+
// 素通し (snapshot 100% 保持)。 escape hatch (CDL_DISABLE_CHAIN_EDGE_LABEL=1) は edges.ts init 位置維持。
|
|
261
|
+
//
|
|
262
|
+
// CAR-485 拡張 = label ↔ edge-path (別 edge) near-collision も検出、 label + 別 edge の path.d を
|
|
263
|
+
// 同方向連鎖 shift。 propagateResult.edges は label shift (labelX/labelY) + path shift (d 平行移動)
|
|
264
|
+
// 両方 apply 済 (applyEdgeLabelAndPathShifts helper) のため、 layout.ts 側追加処理は不要。
|
|
265
|
+
// path shift 情報は propagateResult.edgePathShifts で参照可能 (debug / test / future 用途)。
|
|
266
|
+
let edges: typeof edgesAfterMove;
|
|
267
|
+
let nodesFinal = nodesResolved;
|
|
268
|
+
let containedLanesFinal = containedLanes;
|
|
269
|
+
// lane 単位 dx を適用した後の lane 群。 chain 伝搬が lane を動かさなければ元のまま。
|
|
270
|
+
let laneShiftedBase: typeof gapExpandedLanes = gapExpandedLanes;
|
|
271
|
+
if (isChainEdgeLabelDisabled()) {
|
|
272
|
+
edges = edgesAfterMove;
|
|
273
|
+
} else {
|
|
274
|
+
const propagateResult = resolveEdgeLabelOverlapsWithChainAndPropagate(
|
|
275
|
+
edgesAfterMove,
|
|
276
|
+
collectBBoxes(containedLanes, nodesResolved, []).filter(
|
|
277
|
+
(b) => b.kind === "node" || b.kind === "lane-label",
|
|
278
|
+
),
|
|
279
|
+
chainShiftClearance,
|
|
280
|
+
);
|
|
281
|
+
edges = propagateResult.edges;
|
|
282
|
+
// node / lane-label chain 伝搬 shift を反映 (CAR-482)。
|
|
283
|
+
//
|
|
284
|
+
// 縦方向は row (stack) 単位で動かす (Issue #202、 集約規則は aggregateRowDy SSOT)。
|
|
285
|
+
//
|
|
286
|
+
// 縦 shift を撤去すると label から退いていた node が戻って clearance が悪化する
|
|
287
|
+
// (catalog 実測 = clearance 17 → 25 件)。 label 回避の効果は残したいので、
|
|
288
|
+
// 同 stack の node を同じ dy で一括移動して整列を保つ。
|
|
289
|
+
// 実測 = 全 axis 合計 39 → 26 件、 row-alignment は 6 → 0 件。
|
|
290
|
+
//
|
|
291
|
+
// 横方向 (dx) は lane 中心に紐づくため node ごとの値をそのまま使う。
|
|
292
|
+
if (propagateResult.nodeShifts.size > 0) {
|
|
293
|
+
// 1 件も動かない場合は null が返る。 その時 nodesFinal は nodesResolved の参照のままとなり、
|
|
294
|
+
// 下の `nodesFinal !== nodesResolved` 判定が false になって layoutEdges 再実行を回避する。
|
|
295
|
+
// これにより chain 伝搬が確定させた label 位置が捨てられない。
|
|
296
|
+
const shifted = applyRowLevelNodeShifts(nodesResolved, gapExpandedLanes, propagateResult.nodeShifts);
|
|
297
|
+
if (shifted) {
|
|
298
|
+
nodesFinal = shifted.nodes;
|
|
299
|
+
laneShiftedBase = shifted.lanes;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
// lane-label は独立 bbox として collect されるが、 現状 lane 自体の Y には影響しない
|
|
303
|
+
// (lane-label は render 時 lane.y + 8 offset で描画、 chain shift は future work)。
|
|
304
|
+
// node shift 発生時は contain lane bbox 再計算が必要。
|
|
305
|
+
if (nodesFinal !== nodesResolved) {
|
|
306
|
+
containedLanesFinal = recomputeContainedLanes(laneShiftedBase, nodesFinal);
|
|
307
|
+
// CAR-491 SSOT ... node が chain shift で移動した後 layoutEdges を再実行、 edge の起点/終点
|
|
308
|
+
// が shift 後 node 位置に追従するようにする。 旧実装は propagateResult.edges (shift 前 node
|
|
309
|
+
// 座標基準の path) をそのまま最終 edge として返却していたため、 topology ecs→rds のように
|
|
310
|
+
// rds を +108 world Y shift した後 vertical straight path の終点 Y=688 が rds top Y=796 に
|
|
311
|
+
// 到達せず 108 world gap の arrow marker 浮き症状が発生していた。
|
|
312
|
+
//
|
|
313
|
+
// 再実行後の chain 伝搬 label shift は失われるため trade-off がある ... 現状の SSOT は
|
|
314
|
+
// 「endpoint 追従 > label shift 保持」 = endpoint 切断は視覚的に致命 (arrow marker が空中
|
|
315
|
+
// 浮遊)、 label overlap は engine が最終段階で警告可能。 今後 label shift を再度保存する
|
|
316
|
+
// 必要が生じたら labelX/labelY を propagateResult 側から edgePathShifts と同 map に保存し、
|
|
317
|
+
// 再実行後の edges に merge する二段階 approach を検討する。
|
|
318
|
+
edges = layoutEdges(diag, nodesFinal, containedLanesFinal);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// 同じ 2 点を結ぶ複数の線を弓なりに分ける (dragon #941)。 label と path が確定した後に動かす =
|
|
323
|
+
// 確定前に判定すると、 後から動く label の位置を見て分離を諦める。 座標を明示した edge
|
|
324
|
+
// (canvas pivot) は「指定した直線が最終 path」 が公開契約なので対象から外す。
|
|
325
|
+
const positionedEdgeIds = new Set(
|
|
326
|
+
diag.edges
|
|
327
|
+
.filter((e) => e.posX1 !== undefined && e.posY1 !== undefined && e.posX2 !== undefined && e.posY2 !== undefined)
|
|
328
|
+
.map((e) => e.id),
|
|
329
|
+
);
|
|
330
|
+
bowSharedStraightPaths(edges, nodesFinal, positionedEdgeIds);
|
|
331
|
+
// 弧が確定してから、 同じ節の組を結ぶ辺の label を「兄弟の線を跨がない位置」 へ置き直す (#376)。
|
|
332
|
+
// 順序が逆だと、 直線を前提に置いた label を弧が貫く。 label を先に線から離すと、 今度は弧を
|
|
333
|
+
// 膨らませる判定が label を障害物と見なして弧が消える。
|
|
334
|
+
repositionParallelLabels(edges, nodesFinal);
|
|
335
|
+
|
|
336
|
+
const bboxes = collectBBoxes(containedLanesFinal, nodesFinal, edges);
|
|
337
|
+
const collisions = detectCollisions(bboxes, edges);
|
|
338
|
+
const nearCollisions = detectNearCollisions(bboxes, edges);
|
|
339
|
+
|
|
340
|
+
if (collisions.length > 0) {
|
|
341
|
+
for (const c of collisions) {
|
|
342
|
+
console.warn(
|
|
343
|
+
`[cdl layout] overlap: ${c.a.kind}#${c.a.id} ∩ ${c.b.kind}#${c.b.id} = ${Math.round(c.overlap_area)}px²`,
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (nearCollisions.length > 0) {
|
|
348
|
+
for (const c of nearCollisions) {
|
|
349
|
+
console.warn(
|
|
350
|
+
`[cdl layout] near: ${c.a.kind}#${c.a.id} ↔ ${c.b.kind}#${c.b.id} = ${Math.round(c.gap)}px (need ${c.required}px)`,
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const viewBox = computeViewBox(containedLanesFinal, nodesFinal, edges, bboxes, diag.viewport);
|
|
356
|
+
|
|
357
|
+
return {
|
|
358
|
+
id: diag.id,
|
|
359
|
+
topic: diag.topic,
|
|
360
|
+
viewBox,
|
|
361
|
+
lanes: containedLanesFinal,
|
|
362
|
+
nodes: nodesFinal,
|
|
363
|
+
edges,
|
|
364
|
+
states: diag.states,
|
|
365
|
+
phases: diag.phases,
|
|
366
|
+
bboxes,
|
|
367
|
+
collisions,
|
|
368
|
+
nearCollisions,
|
|
369
|
+
// 図全体の倍率は座標に掛けず、 描画側 (stage.tsx) に委ねる。
|
|
370
|
+
// 座標に掛けると衝突検出 / validation の閾値 (等倍前提) と基準がずれる。
|
|
371
|
+
diagramScale: normalizeDiagramScale(diag.viewport?.scale),
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/** 図全体の倍率を実用範囲に丸める。 1 / 未指定 / 不正値は undefined (= 倍率なしと同じ扱い)。 */
|
|
376
|
+
function normalizeDiagramScale(scale: number | undefined): number | undefined {
|
|
377
|
+
if (scale === undefined) return undefined;
|
|
378
|
+
if (!Number.isFinite(scale) || scale <= 0) return undefined;
|
|
379
|
+
const clamped = Math.min(8, Math.max(0.125, scale));
|
|
380
|
+
return Math.abs(clamped - 1) < 1e-6 ? undefined : clamped;
|
|
381
|
+
}
|