@cardenelabs/cdl 0.22.0 → 0.23.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 +74 -1
- package/dist/index.cjs +6928 -6410
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +61 -1
- package/dist/index.d.ts +61 -1
- package/dist/index.js +6927 -6411
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +22839 -22436
- package/dist/react.cjs.map +1 -1
- package/dist/react.js +22839 -22436
- package/dist/react.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +4 -0
- package/src/layout/edges.ts +627 -0
- package/src/layout/order.ts +201 -0
- package/src/layout/spec.ts +1 -0
- package/src/layout.ts +25 -1
- package/src/presets.ts +5 -12
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 関係から lane と stack の並びを決める (#599)。
|
|
3
|
+
*
|
|
4
|
+
* cdl は交差を `countEdgeCrossings` で数えるだけで、減らす手段を持っていなかった。
|
|
5
|
+
* 書き手が lane / stack を手で指定する仕様のため、交差が出ても engine 側に打つ手がない。
|
|
6
|
+
* 6 表 5 関係の図を人が並べた実測で交差 3 箇所、本関数の並びでは 0 箇所になる。
|
|
7
|
+
*
|
|
8
|
+
* ## 3 段で決める
|
|
9
|
+
*
|
|
10
|
+
* | 段 | やること |
|
|
11
|
+
* |---|---|
|
|
12
|
+
* | 1 | 親からの深さで lane 番号を決める (最長路) |
|
|
13
|
+
* | 2 | lane を跨ぐ関係に中継点を挟み、通り道を確保する |
|
|
14
|
+
* | 3 | 各 lane の並びを中央値法で往復させ、交差を減らす |
|
|
15
|
+
*
|
|
16
|
+
* ## `compile` からは呼ばない
|
|
17
|
+
*
|
|
18
|
+
* 書き手が明示した lane / stack を engine が黙って動かすと、既存の全図の配置が変わる。
|
|
19
|
+
* 本関数は入力を受けて割当を返すだけで、呼ぶかどうかは上流が決める。
|
|
20
|
+
*
|
|
21
|
+
* ## 中継点を挟む理由
|
|
22
|
+
*
|
|
23
|
+
* lane を 2 つ以上跨ぐ関係は、途中の lane で場所を取らないと他の節点と重なる経路になる。
|
|
24
|
+
* 中継点を置くと、その lane の並べ替えに参加して通り道が確保される。
|
|
25
|
+
* 返す結果に中継点は含まない (呼出側が知る必要のない内部の足場)。
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/** 並べたい節点。 大きさは見ないので id だけでよい */
|
|
29
|
+
export type OrderNode = { id: string };
|
|
30
|
+
|
|
31
|
+
/** 親から子への関係。 向きが lane の深さを決める */
|
|
32
|
+
export type OrderEdge = { from: string; to: string };
|
|
33
|
+
|
|
34
|
+
/** 決まった並び。 lane が列、 stack が列の中の位置 */
|
|
35
|
+
export type OrderResult = { id: string; lane: number; stack: number };
|
|
36
|
+
|
|
37
|
+
/** 中央値法を往復させる回数。 4 往復で並びが落ち着くことを test で確かめている */
|
|
38
|
+
const SWEEPS = 4;
|
|
39
|
+
|
|
40
|
+
/** 中継点かどうかは id の接頭辞で分ける。 呼出側の id と衝突しない形にする */
|
|
41
|
+
const RELAY = "__relay:";
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 親からの深さを決める。
|
|
45
|
+
*
|
|
46
|
+
* 環状の関係 (a→b→c→a) があると最長路は定義できないため、節点数を上限に打ち切る。
|
|
47
|
+
* 打ち切った時点の深さをそのまま使う = 環の中では順序が付かないが、結果は返る。
|
|
48
|
+
*/
|
|
49
|
+
function ranks(nodes: readonly OrderNode[], edges: readonly OrderEdge[]): Map<string, number> {
|
|
50
|
+
const rank = new Map<string, number>();
|
|
51
|
+
for (const n of nodes) rank.set(n.id, 0);
|
|
52
|
+
for (let pass = 0; pass < nodes.length; pass += 1) {
|
|
53
|
+
let 動いた = false;
|
|
54
|
+
for (const e of edges) {
|
|
55
|
+
if (e.from === e.to) continue;
|
|
56
|
+
const a = rank.get(e.from);
|
|
57
|
+
const b = rank.get(e.to);
|
|
58
|
+
if (a === undefined || b === undefined) continue;
|
|
59
|
+
if (b < a + 1) {
|
|
60
|
+
rank.set(e.to, a + 1);
|
|
61
|
+
動いた = true;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (!動いた) break;
|
|
65
|
+
}
|
|
66
|
+
return rank;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** 中央値。 参照先が無い節点は -1 を返し、並べ替えの対象から外す */
|
|
70
|
+
function median(values: readonly number[]): number {
|
|
71
|
+
if (values.length === 0) return -1;
|
|
72
|
+
const v = [...values].sort((a, b) => a - b);
|
|
73
|
+
return v[Math.floor((v.length - 1) / 2)] ?? -1;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 関係から lane と stack を決める。
|
|
78
|
+
*
|
|
79
|
+
* 同じ入力からは常に同じ並びが返る (乱数を使わず、同点は入力順で解く)。
|
|
80
|
+
*
|
|
81
|
+
* @param nodes 並べたい節点
|
|
82
|
+
* @param edges 親から子への関係。 自己参照は深さに影響しない
|
|
83
|
+
* @returns 節点ごとの lane と stack。 入力に無い id は返らない
|
|
84
|
+
*/
|
|
85
|
+
export function orderNodes(nodes: readonly OrderNode[], edges: readonly OrderEdge[]): OrderResult[] {
|
|
86
|
+
if (nodes.length === 0) return [];
|
|
87
|
+
const ある = new Set(nodes.map((n) => n.id));
|
|
88
|
+
const 有効 = edges.filter((e) => ある.has(e.from) && ある.has(e.to));
|
|
89
|
+
|
|
90
|
+
const rank = ranks(nodes, 有効);
|
|
91
|
+
|
|
92
|
+
// 中継点を挟む。 lane を 2 つ以上跨ぐ関係だけが対象
|
|
93
|
+
type 節 = { id: string; lane: number };
|
|
94
|
+
const 節点: 節[] = nodes.map((n) => ({ id: n.id, lane: rank.get(n.id) ?? 0 }));
|
|
95
|
+
const 辺: OrderEdge[] = [];
|
|
96
|
+
有効.forEach((e, i) => {
|
|
97
|
+
if (e.from === e.to) return;
|
|
98
|
+
const 始 = rank.get(e.from) ?? 0;
|
|
99
|
+
const 終 = rank.get(e.to) ?? 0;
|
|
100
|
+
let 前 = e.from;
|
|
101
|
+
for (let s = 始 + 1; s < 終; s += 1) {
|
|
102
|
+
const id = `${RELAY}${i}:${s}`;
|
|
103
|
+
節点.push({ id, lane: s });
|
|
104
|
+
辺.push({ from: 前, to: id });
|
|
105
|
+
前 = id;
|
|
106
|
+
}
|
|
107
|
+
辺.push({ from: 前, to: e.to });
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// lane ごとに並べる。 初期の並びは入力順
|
|
111
|
+
const 深さ = Math.max(...節点.map((n) => n.lane)) + 1;
|
|
112
|
+
const 列: 節[][] = Array.from({ length: 深さ }, () => []);
|
|
113
|
+
for (const n of 節点) 列[n.lane]?.push(n);
|
|
114
|
+
|
|
115
|
+
const 位置 = (): Map<string, number> => {
|
|
116
|
+
const m = new Map<string, number>();
|
|
117
|
+
for (const 並 of 列) 並.forEach((n, i) => m.set(n.id, i));
|
|
118
|
+
return m;
|
|
119
|
+
};
|
|
120
|
+
const 親 = new Map<string, string[]>();
|
|
121
|
+
const 子 = new Map<string, string[]>();
|
|
122
|
+
const 足す = (m: Map<string, string[]>, key: string, value: string): void => {
|
|
123
|
+
const 既 = m.get(key);
|
|
124
|
+
if (既) 既.push(value);
|
|
125
|
+
else m.set(key, [value]);
|
|
126
|
+
};
|
|
127
|
+
for (const e of 辺) {
|
|
128
|
+
足す(親, e.to, e.from);
|
|
129
|
+
足す(子, e.from, e.to);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
for (let pass = 0; pass < SWEEPS; pass += 1) {
|
|
133
|
+
const pos = 位置();
|
|
134
|
+
const 下向き = pass % 2 === 0;
|
|
135
|
+
const 順 = 下向き ? [...列.keys()] : [...列.keys()].reverse();
|
|
136
|
+
for (const s of 順) {
|
|
137
|
+
if ((下向き && s === 0) || (!下向き && s === 列.length - 1)) continue;
|
|
138
|
+
const 並 = 列[s];
|
|
139
|
+
if (!並) continue;
|
|
140
|
+
const 元順 = new Map(並.map((n, i) => [n.id, i]));
|
|
141
|
+
const 重み = new Map(
|
|
142
|
+
並.map((n) => {
|
|
143
|
+
const 相手 = (下向き ? 親.get(n.id) : 子.get(n.id)) ?? [];
|
|
144
|
+
const m = median(相手.map((id) => pos.get(id) ?? -1).filter((v) => v >= 0));
|
|
145
|
+
return [n.id, m];
|
|
146
|
+
}),
|
|
147
|
+
);
|
|
148
|
+
並.sort((a, b) => {
|
|
149
|
+
const A = 重み.get(a.id) ?? -1;
|
|
150
|
+
const B = 重み.get(b.id) ?? -1;
|
|
151
|
+
// 参照先を持たない節点は動かさない (元の位置を保つ)
|
|
152
|
+
if (A < 0 && B < 0) return (元順.get(a.id) ?? 0) - (元順.get(b.id) ?? 0);
|
|
153
|
+
if (A < 0) return 1;
|
|
154
|
+
if (B < 0) return -1;
|
|
155
|
+
if (A !== B) return A - B;
|
|
156
|
+
return (元順.get(a.id) ?? 0) - (元順.get(b.id) ?? 0);
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// 中継点を落とし、残りに stack を振り直す
|
|
162
|
+
const 結果: OrderResult[] = [];
|
|
163
|
+
列.forEach((並, lane) => {
|
|
164
|
+
let stack = 0;
|
|
165
|
+
for (const n of 並) {
|
|
166
|
+
if (n.id.startsWith(RELAY)) continue;
|
|
167
|
+
結果.push({ id: n.id, lane, stack });
|
|
168
|
+
stack += 1;
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
return 結果;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* 並びの上で交差の数を数える。
|
|
176
|
+
*
|
|
177
|
+
* 節点の実寸を見ず、lane と stack だけで数える近似。
|
|
178
|
+
* 同じ lane 間を渡る 2 本が、始点と終点の上下関係を入れ替えていれば 1 交差と数える。
|
|
179
|
+
* 配置を比べる用途にはこれで足り、実寸を要する判定は `countEdgeCrossings` が担う。
|
|
180
|
+
*/
|
|
181
|
+
export function countOrderCrossings(order: readonly OrderResult[], edges: readonly OrderEdge[]): number {
|
|
182
|
+
const 場所 = new Map(order.map((o) => [o.id, o]));
|
|
183
|
+
const 渡り = edges
|
|
184
|
+
.filter((e) => e.from !== e.to)
|
|
185
|
+
.map((e) => ({ a: 場所.get(e.from), b: 場所.get(e.to) }))
|
|
186
|
+
.filter((e): e is { a: OrderResult; b: OrderResult } => e.a !== undefined && e.b !== undefined);
|
|
187
|
+
|
|
188
|
+
let 数 = 0;
|
|
189
|
+
for (let i = 0; i < 渡り.length; i += 1) {
|
|
190
|
+
for (let j = i + 1; j < 渡り.length; j += 1) {
|
|
191
|
+
const x = 渡り[i];
|
|
192
|
+
const y = 渡り[j];
|
|
193
|
+
if (!x || !y) continue;
|
|
194
|
+
// 同じ lane から同じ lane へ渡る 2 本だけを比べる
|
|
195
|
+
if (x.a.lane !== y.a.lane || x.b.lane !== y.b.lane) continue;
|
|
196
|
+
const 上下 = (x.a.stack - y.a.stack) * (x.b.stack - y.b.stack);
|
|
197
|
+
if (上下 < 0) 数 += 1;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return 数;
|
|
201
|
+
}
|
package/src/layout/spec.ts
CHANGED
|
@@ -251,6 +251,7 @@ export function genericTitleBaselineY(kind: string | undefined, h: number, hasRo
|
|
|
251
251
|
export const ROW_TOP_STORAGE = 152;
|
|
252
252
|
export const ROW_PITCH_STORAGE = 56;
|
|
253
253
|
|
|
254
|
+
|
|
254
255
|
/** `kinds/generic.tsx` の 1 行目 baseline と行送り。 */
|
|
255
256
|
export const ROW_TOP_GENERIC = 100;
|
|
256
257
|
export const ROW_PITCH_GENERIC = 28;
|
package/src/layout.ts
CHANGED
|
@@ -10,7 +10,15 @@ import {
|
|
|
10
10
|
resolveEdgeLabelOverlapsWithChainAndPropagate,
|
|
11
11
|
resolveOverlaps,
|
|
12
12
|
} from "./layout/collisions";
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
bowSharedStraightPaths,
|
|
15
|
+
fanIncomingEndpoints,
|
|
16
|
+
hopEdgeCrossings,
|
|
17
|
+
layoutEdges,
|
|
18
|
+
repositionParallelLabels,
|
|
19
|
+
separateSharedStraightRuns,
|
|
20
|
+
slideLabelsAlongOwnPath,
|
|
21
|
+
} from "./layout/edges";
|
|
14
22
|
import { applyFooterShapeDrops } from "./layout/footer-shape";
|
|
15
23
|
import {
|
|
16
24
|
expandLaneGapsForEdgeLabels,
|
|
@@ -338,7 +346,23 @@ export function layout(diag: CdlDiagram): LaidDiagram {
|
|
|
338
346
|
// 弧が確定してから、 同じ節の組を結ぶ辺の label を「兄弟の線を跨がない位置」 へ置き直す (#376)。
|
|
339
347
|
// 順序が逆だと、 直線を前提に置いた label を弧が貫く。 label を先に線から離すと、 今度は弧を
|
|
340
348
|
// 膨らませる判定が label を障害物と見なして弧が消える。
|
|
349
|
+
// 同じ辺に入る線の終点を辺に沿って離す (#611)。 経路を引き終えてから当てる = 終点を先に
|
|
350
|
+
// 動かすと引き方の分岐が変わり、 迂回していた線が箱を貫通する (実測で 0 件が 7 件になった)。
|
|
351
|
+
fanIncomingEndpoints(edges, nodesFinal);
|
|
352
|
+
// 同じ通り道を並んで走る区間をずらす (#610)。 弓の後に当てる = 弓で分かれた線を数え直さない。
|
|
353
|
+
// 弧より先に当てる = ずらした後の形で交点を数えないと、 消えた交点に弧が残る。
|
|
354
|
+
//
|
|
355
|
+
// 終点を離した **後** に当てる。 先に当てると、 終点を離したことで新しくできた並走が
|
|
356
|
+
// 残る (実測で ER 4 表に長さ 95 の並走が戻った)。
|
|
357
|
+
separateSharedStraightRuns(edges, nodesFinal);
|
|
358
|
+
// 交わる所で上を通る側に弧を差し込む (#608)。 経路が確定してから当てる = 弓や迂回で
|
|
359
|
+
// 形が変わった後の交点を見る。 札を置く前に当てるので、 札は弧を含んだ経路の上に載る。
|
|
360
|
+
hopEdgeCrossings(edges);
|
|
341
361
|
repositionParallelLabels(edges, nodesFinal);
|
|
362
|
+
// 縦にずらすだけでは逃げ場が無い札を、 自分の線に沿って動かす (#601)。 ここまでの
|
|
363
|
+
// 置き直しは全て縦方向で、 線に沿って動かす経路が無かった。 いま被っている札だけを
|
|
364
|
+
// 対象にするので、 落ち着いている図の座標は動かない。
|
|
365
|
+
slideLabelsAlongOwnPath(edges, nodesFinal);
|
|
342
366
|
|
|
343
367
|
const bboxes = collectBBoxes(containedLanesFinal, nodesFinal, edges);
|
|
344
368
|
const collisions = detectCollisions(bboxes, edges);
|
package/src/presets.ts
CHANGED
|
@@ -794,8 +794,9 @@ export function er(preset: ErPreset): ErBuilder {
|
|
|
794
794
|
const api: ErBuilder = {
|
|
795
795
|
entity(e) {
|
|
796
796
|
const laneId = `lane-${e.id}`;
|
|
797
|
-
//
|
|
798
|
-
//
|
|
797
|
+
// 鍵の群を上にまとめる (#578)。 群の区切りは **行頭の印** が持つ = 鍵は名前に下線が付く。
|
|
798
|
+
// 空の行で開ける形は #606 でやめた = 下線と重複した 2 つ目の手掛かりで、
|
|
799
|
+
// 代わりに行の間隔を不揃いにしていた (鍵と値を持つ箱だけ境目が広がる)
|
|
799
800
|
const 鍵 = (e.columns ?? []).filter((c) => c.pk === true);
|
|
800
801
|
const 値 = (e.columns ?? []).filter((c) => c.pk !== true);
|
|
801
802
|
const 行: string[] = [];
|
|
@@ -809,10 +810,6 @@ export function er(preset: ErPreset): ErBuilder {
|
|
|
809
810
|
});
|
|
810
811
|
};
|
|
811
812
|
鍵.forEach(足す);
|
|
812
|
-
if (鍵.length > 0 && 値.length > 0) {
|
|
813
|
-
行.push("");
|
|
814
|
-
印.push(null);
|
|
815
|
-
}
|
|
816
813
|
値.forEach(足す);
|
|
817
814
|
const 設計の形 = 行.length > 0;
|
|
818
815
|
const rows = 設計の形 ? 行 : (e.rows ?? []);
|
|
@@ -1442,8 +1439,8 @@ export function classDiagram(preset: ClassDiagramPreset): ClassDiagramBuilder {
|
|
|
1442
1439
|
return api;
|
|
1443
1440
|
},
|
|
1444
1441
|
build() {
|
|
1445
|
-
// 行頭の印で持ち物と振る舞いを分ける (#578)。
|
|
1446
|
-
//
|
|
1442
|
+
// 行頭の印で持ち物と振る舞いを分ける (#578)。 持ち物は四角、振る舞いは山形なので、
|
|
1443
|
+
// 群の区切りは行ごとに読める。 空の行で開ける形は #606 でやめた
|
|
1447
1444
|
const 中身 = 箱.map((c) => {
|
|
1448
1445
|
const rows: string[] = [];
|
|
1449
1446
|
const rowMarks: (RowMark | null)[] = [];
|
|
@@ -1451,10 +1448,6 @@ export function classDiagram(preset: ClassDiagramPreset): ClassDiagramBuilder {
|
|
|
1451
1448
|
rows.push(stripAccess(a));
|
|
1452
1449
|
rowMarks.push({ shape: "square", filled: isPublic(a) });
|
|
1453
1450
|
}
|
|
1454
|
-
if ((c.attributes?.length ?? 0) > 0 && (c.methods?.length ?? 0) > 0) {
|
|
1455
|
-
rows.push("");
|
|
1456
|
-
rowMarks.push(null);
|
|
1457
|
-
}
|
|
1458
1451
|
for (const m of c.methods ?? []) {
|
|
1459
1452
|
rows.push(stripAccess(m));
|
|
1460
1453
|
rowMarks.push({ shape: "chevron", filled: isPublic(m) });
|