@cardenelabs/cdl 0.29.0 → 0.30.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 +54 -1
- package/dist/index.cjs +132 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +132 -12
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +132 -12
- package/dist/react.cjs.map +1 -1
- package/dist/react.js +132 -12
- package/dist/react.js.map +1 -1
- package/package.json +1 -1
- package/src/layout/edges.ts +167 -13
- package/src/layout.ts +4 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cardenelabs/cdl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.30.0",
|
|
4
4
|
"description": "CDL (Chainome Diagram Language). Mermaid-like declarative DSL that compiles to animated SVG diagrams. Built for blockchain / Solidity flows, generic enough for sequence / flow / state / ER / topology diagrams.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "cardene777",
|
package/src/layout/edges.ts
CHANGED
|
@@ -2999,12 +2999,110 @@ export function separateSharedStraightRuns(edges: LaidEdge[], nodes: readonly La
|
|
|
2999
2999
|
}
|
|
3000
3000
|
}
|
|
3001
3001
|
|
|
3002
|
+
/**
|
|
3003
|
+
* 同じ節の同じ辺から出る線のうち、 最長の 1 本を除いて最初の折れを手前へ引く (#651)。
|
|
3004
|
+
*
|
|
3005
|
+
* 起点は `fan-origin-single-point` のため動かさず、 最初の折れ直後の区間を平行移動する。
|
|
3006
|
+
* 1 本しか出ない辺は `detourStub` が決めた箱との中間位置をそのまま保つ。
|
|
3007
|
+
*/
|
|
3008
|
+
export function pullOutgoingFirstCorner(edges: LaidEdge[], nodes: readonly LaidNode[]): void {
|
|
3009
|
+
if (edges.length < 2) return;
|
|
3010
|
+
|
|
3011
|
+
const 束 = new Map<string, LaidEdge[]>();
|
|
3012
|
+
const 同じ組の数 = new Map<string, number>();
|
|
3013
|
+
for (const e of edges) {
|
|
3014
|
+
const key = `${e.from}::${e.fromSide}`;
|
|
3015
|
+
const list = 束.get(key) ?? [];
|
|
3016
|
+
list.push(e);
|
|
3017
|
+
束.set(key, list);
|
|
3018
|
+
const pair = `${e.from}->${e.to}`;
|
|
3019
|
+
同じ組の数.set(pair, (同じ組の数.get(pair) ?? 0) + 1);
|
|
3020
|
+
}
|
|
3021
|
+
|
|
3022
|
+
let 札を旧経路へ追従済み = false;
|
|
3023
|
+
for (const list of 束.values()) {
|
|
3024
|
+
if (list.length < 2) continue;
|
|
3025
|
+
const 並び = list
|
|
3026
|
+
.flatMap((e) => {
|
|
3027
|
+
const pts = pathVertices(e.d);
|
|
3028
|
+
if (!pts || pts.length < 2) return [];
|
|
3029
|
+
return [{ e, pts, 最初の長さ: Math.hypot(pts[1]!.x - pts[0]!.x, pts[1]!.y - pts[0]!.y) }];
|
|
3030
|
+
})
|
|
3031
|
+
.sort((a, b) => b.最初の長さ - a.最初の長さ);
|
|
3032
|
+
|
|
3033
|
+
// 最長の線を引いても共有区間は縮まらないので、 2 本目以降だけを動かす。
|
|
3034
|
+
for (const { e, pts, 最初の長さ } of 並び.slice(1)) {
|
|
3035
|
+
// 同じ 2 節を結ぶ線の分離は `bowSharedStraightPaths` の範囲。 迂回経路は重ねて残す。
|
|
3036
|
+
if ((同じ組の数.get(`${e.from}->${e.to}`) ?? 0) > 1) continue;
|
|
3037
|
+
if (pts.length < 3 || 最初の長さ <= EDGE_STUB_OUT + VERTICAL_DETOUR_CORNER) continue;
|
|
3038
|
+
const 横 = e.fromSide === "left" || e.fromSide === "right";
|
|
3039
|
+
if (!runIsAligned(pts, 0, 横 ? 1 : 0)) continue;
|
|
3040
|
+
|
|
3041
|
+
// 角丸の半径分を折れの位置に足し、起点からの直線部を EDGE_STUB_OUT だけ残す。
|
|
3042
|
+
const 移動量 = 最初の長さ - (EDGE_STUB_OUT + VERTICAL_DETOUR_CORNER);
|
|
3043
|
+
const ux = (pts[1]!.x - pts[0]!.x) / 最初の長さ;
|
|
3044
|
+
const uy = (pts[1]!.y - pts[0]!.y) / 最初の長さ;
|
|
3045
|
+
const 試し = pts.map((p, i) =>
|
|
3046
|
+
i === 1 || i === 2 ? { x: p.x - ux * 移動量, y: p.y - uy * 移動量 } : p,
|
|
3047
|
+
);
|
|
3048
|
+
const 試しD = verticesToPath(試し);
|
|
3049
|
+
const 試し区間 = flattenPathSegments(extractPathSegments(試しD));
|
|
3050
|
+
if (
|
|
3051
|
+
nodes.some(
|
|
3052
|
+
(n) => n.id !== e.from && n.id !== e.to && segmentsCrossRect(試し区間, nodeRect(n)),
|
|
3053
|
+
)
|
|
3054
|
+
) {
|
|
3055
|
+
continue;
|
|
3056
|
+
}
|
|
3057
|
+
|
|
3058
|
+
const 前 = e.d;
|
|
3059
|
+
const 前の交差 = countEdgeCrossings(edges).count;
|
|
3060
|
+
e.d = 試しD;
|
|
3061
|
+
if (countEdgeCrossings(edges).count > 前の交差) {
|
|
3062
|
+
e.d = 前;
|
|
3063
|
+
continue;
|
|
3064
|
+
}
|
|
3065
|
+
e.d = 前;
|
|
3066
|
+
|
|
3067
|
+
// 旧経路に重なっていた札を先に確定する。 経路を動かした後では重なりが消え、 札だけが
|
|
3068
|
+
// 元の位置に取り残される。
|
|
3069
|
+
if (!札を旧経路へ追従済み) {
|
|
3070
|
+
slideLabelsAlongOwnPath(edges, nodes);
|
|
3071
|
+
札を旧経路へ追従済み = true;
|
|
3072
|
+
}
|
|
3073
|
+
const labelBox = computeLabelBBoxWorld(e);
|
|
3074
|
+
const label = { x: labelBox.x + labelBox.w / 2, y: labelBox.y + labelBox.h / 2 };
|
|
3075
|
+
const 線分までの距離2 = (a: Vertex, b: Vertex): number => {
|
|
3076
|
+
const dx = b.x - a.x;
|
|
3077
|
+
const dy = b.y - a.y;
|
|
3078
|
+
const len2 = dx * dx + dy * dy;
|
|
3079
|
+
const t =
|
|
3080
|
+
len2 === 0
|
|
3081
|
+
? 0
|
|
3082
|
+
: Math.max(0, Math.min(1, ((label.x - a.x) * dx + (label.y - a.y) * dy) / len2));
|
|
3083
|
+
return (label.x - (a.x + dx * t)) ** 2 + (label.y - (a.y + dy * t)) ** 2;
|
|
3084
|
+
};
|
|
3085
|
+
const 動く区間への距離 = 線分までの距離2(pts[1]!, pts[2]!);
|
|
3086
|
+
const labelも動かす = pts
|
|
3087
|
+
.slice(0, -1)
|
|
3088
|
+
.every((p, i) => 線分までの距離2(p, pts[i + 1]!) >= 動く区間への距離 - 1e-6);
|
|
3089
|
+
e.d = 試しD;
|
|
3090
|
+
if (labelも動かす) {
|
|
3091
|
+
e.labelX -= ux * 移動量;
|
|
3092
|
+
e.labelY -= uy * 移動量;
|
|
3093
|
+
}
|
|
3094
|
+
}
|
|
3095
|
+
}
|
|
3096
|
+
}
|
|
3097
|
+
|
|
3002
3098
|
/** 同じ辺に入る線を離す間隔 (world unit)。 箱の行 1 つ分。 */
|
|
3003
3099
|
const ENDPOINT_FAN_STEP = 56;
|
|
3004
3100
|
/** これより詰まるなら離さない (world unit)。 離しても線どうしを見分けられない。 */
|
|
3005
3101
|
const ENDPOINT_FAN_MIN = 20;
|
|
3006
|
-
/**
|
|
3102
|
+
/** ずらすために最後の区間へ要る長さ (world unit)。 これより短い区間は動かさない。 */
|
|
3007
3103
|
const ENDPOINT_FAN_RUNUP = 40;
|
|
3104
|
+
/** これより短い重なりは動かさない (world unit)。 動かすとまっすぐな線に折れが増える。 */
|
|
3105
|
+
const MIXED_ROOT_OVERLAP_MIN = 24;
|
|
3008
3106
|
|
|
3009
3107
|
/**
|
|
3010
3108
|
* 同じ節の同じ辺に **入る** 線の終点を、 辺に沿って離す (#611)。
|
|
@@ -3039,6 +3137,26 @@ const ENDPOINT_FAN_RUNUP = 40;
|
|
|
3039
3137
|
export function fanIncomingEndpoints(edges: LaidEdge[], nodes: readonly LaidNode[]): void {
|
|
3040
3138
|
if (edges.length < 2) return;
|
|
3041
3139
|
const nmap = new Map(nodes.map((n) => [n.id, n]));
|
|
3140
|
+
const 平坦な区間 = new Map(edges.map((e) => [e.id, flattenPathSegments(extractPathSegments(e.d))] as const));
|
|
3141
|
+
|
|
3142
|
+
const 軸に平行な重なり長さ = (一方: LaidEdge, 他方: LaidEdge): number => {
|
|
3143
|
+
let 合計 = 0;
|
|
3144
|
+
for (const a of 平坦な区間.get(一方.id) ?? []) {
|
|
3145
|
+
const aは水平 = Math.abs(a.y2 - a.y1) <= 1;
|
|
3146
|
+
const aは垂直 = Math.abs(a.x2 - a.x1) <= 1;
|
|
3147
|
+
for (const b of 平坦な区間.get(他方.id) ?? []) {
|
|
3148
|
+
const bは水平 = Math.abs(b.y2 - b.y1) <= 1;
|
|
3149
|
+
const bは垂直 = Math.abs(b.x2 - b.x1) <= 1;
|
|
3150
|
+
if (aは水平 && bは水平 && Math.abs(a.y1 - b.y1) <= 1) {
|
|
3151
|
+
合計 += Math.max(0, Math.min(Math.max(a.x1, a.x2), Math.max(b.x1, b.x2)) - Math.max(Math.min(a.x1, a.x2), Math.min(b.x1, b.x2)));
|
|
3152
|
+
}
|
|
3153
|
+
if (aは垂直 && bは垂直 && Math.abs(a.x1 - b.x1) <= 1) {
|
|
3154
|
+
合計 += Math.max(0, Math.min(Math.max(a.y1, a.y2), Math.max(b.y1, b.y2)) - Math.max(Math.min(a.y1, a.y2), Math.min(b.y1, b.y2)));
|
|
3155
|
+
}
|
|
3156
|
+
}
|
|
3157
|
+
}
|
|
3158
|
+
return 合計;
|
|
3159
|
+
};
|
|
3042
3160
|
|
|
3043
3161
|
/** 同じ 2 点を結ぶ線の本数。 その組は弓が扱うので、 ここでは触らない。 */
|
|
3044
3162
|
const 同じ組の数 = new Map<string, number>();
|
|
@@ -3046,7 +3164,14 @@ export function fanIncomingEndpoints(edges: LaidEdge[], nodes: readonly LaidNode
|
|
|
3046
3164
|
|
|
3047
3165
|
/** 同じ節の同じ辺に入る線。 */
|
|
3048
3166
|
const 束 = new Map<string, LaidEdge[]>();
|
|
3167
|
+
const 出る線 = new Map<string, LaidEdge[]>();
|
|
3049
3168
|
for (const e of edges) {
|
|
3169
|
+
if (nmap.has(e.from)) {
|
|
3170
|
+
const key = `${e.from}::${e.fromSide}`;
|
|
3171
|
+
const list = 出る線.get(key) ?? [];
|
|
3172
|
+
list.push(e);
|
|
3173
|
+
出る線.set(key, list);
|
|
3174
|
+
}
|
|
3050
3175
|
if (!nmap.has(e.to)) continue;
|
|
3051
3176
|
// 同じ 2 点を結ぶ線は `bowSharedStraightPaths` が弓で分ける範囲。 `#941` は
|
|
3052
3177
|
// 「どの幅でも箱に当たるなら分離せず重なりを残す」 と決めており、 末尾で分けると
|
|
@@ -3058,36 +3183,62 @@ export function fanIncomingEndpoints(edges: LaidEdge[], nodes: readonly LaidNode
|
|
|
3058
3183
|
束.set(key, list);
|
|
3059
3184
|
}
|
|
3060
3185
|
|
|
3061
|
-
for (const [, list] of 束) {
|
|
3062
|
-
|
|
3186
|
+
for (const [key, list] of 束) {
|
|
3187
|
+
const 中央が塞がっている = (出る線.get(key) ?? []).some((出線) =>
|
|
3188
|
+
list.some((入線) =>
|
|
3189
|
+
!(入線.from === 出線.to && 入線.to === 出線.from)
|
|
3190
|
+
&& 軸に平行な重なり長さ(入線, 出線) >= MIXED_ROOT_OVERLAP_MIN
|
|
3191
|
+
)
|
|
3192
|
+
);
|
|
3193
|
+
if (list.length < 2 && !中央が塞がっている) continue;
|
|
3063
3194
|
const n = nmap.get(list[0]!.to)!;
|
|
3064
3195
|
const 横 = list[0]!.toSide === "left" || list[0]!.toSide === "right";
|
|
3065
3196
|
const 辺の長さ = 横 ? n.h : n.w;
|
|
3066
3197
|
// 4 隅に刺さらない範囲。 `arrow-endpoint-center` が見ているのと同じ余白を空ける。
|
|
3067
3198
|
const 使える幅 = 辺の長さ - 2 * ARROW_ENDPOINT_CENTER_TOL;
|
|
3068
3199
|
if (使える幅 <= 0) continue;
|
|
3069
|
-
const
|
|
3200
|
+
const 席の間 = 中央が塞がっている ? list.length : list.length - 1;
|
|
3201
|
+
const 間隔 = Math.min(ENDPOINT_FAN_STEP, 使える幅 / 席の間);
|
|
3070
3202
|
if (間隔 < ENDPOINT_FAN_MIN) continue;
|
|
3071
3203
|
|
|
3072
3204
|
// 来た高さの順に割り当てる。 上を走ってきた線を上の端点へ入れれば、 束の中で交差しない。
|
|
3073
3205
|
// 図の並び順で割り当てると、 上を走る線が下の端点へ回って必ず交差する。
|
|
3206
|
+
const 辺の中心 = 横 ? n.cy : n.cx;
|
|
3074
3207
|
const 並び = list
|
|
3075
|
-
.map((e) =>
|
|
3208
|
+
.map((e) => {
|
|
3209
|
+
const 区間 = extractPathSegments(e.d);
|
|
3210
|
+
const last = 区間[区間.length - 1];
|
|
3211
|
+
const 長い助走 = last === undefined ? 0 : Math.hypot(last.x2 - last.x1, last.y2 - last.y1);
|
|
3212
|
+
const 中心を補う = 中央が塞がっている || list.length === 2;
|
|
3213
|
+
const 深さ = 辺から離れた側の位置(e, 横) ?? (中心を補う ? 辺の中心 : 0);
|
|
3214
|
+
return { e, 長い助走, 深さ };
|
|
3215
|
+
})
|
|
3076
3216
|
.sort((a, b) => a.深さ - b.深さ || (a.e.id < b.e.id ? -1 : 1));
|
|
3077
3217
|
|
|
3078
3218
|
const 中心 = ((並び.length - 1) * 間隔) / 2;
|
|
3219
|
+
let 負側の席 = 0;
|
|
3220
|
+
let 正側の席 = 0;
|
|
3079
3221
|
for (let k = 0; k < 並び.length; k++) {
|
|
3080
|
-
const d =
|
|
3222
|
+
const d = 中央が塞がっている
|
|
3223
|
+
? 並び[k]!.深さ < 辺の中心
|
|
3224
|
+
? -(++負側の席 * 間隔)
|
|
3225
|
+
: ++正側の席 * 間隔
|
|
3226
|
+
: k * 間隔 - 中心;
|
|
3081
3227
|
if (Math.abs(d) < 0.001) continue;
|
|
3082
3228
|
const e = 並び[k]!.e;
|
|
3083
|
-
const 差替 = 末尾を辺に沿ってずらす(e, 横, d, nodes);
|
|
3084
|
-
if (差替 === null) continue;
|
|
3085
3229
|
// 交差が増えるなら戻す。 判定は軸と同じ数え方を使う = 独自の数え方は軸と食い違い、
|
|
3086
3230
|
// 軸が 3 件を数えている状況で 0 件と判定して通してしまった (実測)。
|
|
3087
3231
|
const 前 = e.d;
|
|
3088
3232
|
const 前の交差 = countEdgeCrossings(edges).count;
|
|
3089
|
-
|
|
3090
|
-
|
|
3233
|
+
const 長い助走 = 並び[k]!.長い助走;
|
|
3234
|
+
for (const 助走 of [長い助走, ENDPOINT_FAN_RUNUP]) {
|
|
3235
|
+
e.d = 前;
|
|
3236
|
+
const 差替 = 末尾を辺に沿ってずらす(e, 横, d, 助走, nodes);
|
|
3237
|
+
if (差替 === null) continue;
|
|
3238
|
+
e.d = 差替;
|
|
3239
|
+
if (countEdgeCrossings(edges).count <= 前の交差) break;
|
|
3240
|
+
e.d = 前;
|
|
3241
|
+
}
|
|
3091
3242
|
}
|
|
3092
3243
|
}
|
|
3093
3244
|
}
|
|
@@ -3121,6 +3272,7 @@ function 末尾を辺に沿ってずらす(
|
|
|
3121
3272
|
e: LaidEdge,
|
|
3122
3273
|
横: boolean,
|
|
3123
3274
|
d: number,
|
|
3275
|
+
助走: number,
|
|
3124
3276
|
nodes: readonly LaidNode[],
|
|
3125
3277
|
): string | null {
|
|
3126
3278
|
if ((e.d.match(/M/g) ?? []).length !== 1) return null;
|
|
@@ -3139,10 +3291,11 @@ function 末尾を辺に沿ってずらす(
|
|
|
3139
3291
|
const 長さ = Math.hypot(last.x2 - last.x1, last.y2 - last.y1);
|
|
3140
3292
|
if (長さ < ENDPOINT_FAN_RUNUP + 1) return null;
|
|
3141
3293
|
|
|
3142
|
-
//
|
|
3294
|
+
// 助走が最後の区間を越えると、 書き戻した直前の折れから短く戻る区間ができる。
|
|
3143
3295
|
const ux = (last.x1 - last.x2) / 長さ;
|
|
3144
3296
|
const uy = (last.y1 - last.y2) / 長さ;
|
|
3145
|
-
const
|
|
3297
|
+
const 丸めた助走 = Math.min(助走, 長さ);
|
|
3298
|
+
const 折れ1 = { x: last.x2 + ux * 丸めた助走, y: last.y2 + uy * 丸めた助走 };
|
|
3146
3299
|
const 折れ2 = 横 ? { x: 折れ1.x, y: 折れ1.y + d } : { x: 折れ1.x + d, y: 折れ1.y };
|
|
3147
3300
|
const 終点 = 横 ? { x: last.x2, y: last.y2 + d } : { x: last.x2 + d, y: last.y2 };
|
|
3148
3301
|
|
|
@@ -3169,7 +3322,8 @@ function 末尾を辺に沿ってずらす(
|
|
|
3169
3322
|
parts.push(`L ${s.x2} ${s.y2}`);
|
|
3170
3323
|
}
|
|
3171
3324
|
// 助走の折れは丸めない。 短い 2 折れなので、 丸めると切り口が区間からはみ出す。
|
|
3172
|
-
|
|
3325
|
+
const 折れ1は直前の折れ = Math.abs(折れ1.x - last.x1) <= 0.5 && Math.abs(折れ1.y - last.y1) <= 0.5;
|
|
3326
|
+
if (!折れ1は直前の折れ) parts.push(`L ${round1(折れ1.x)} ${round1(折れ1.y)}`);
|
|
3173
3327
|
parts.push(`L ${round1(折れ2.x)} ${round1(折れ2.y)}`);
|
|
3174
3328
|
parts.push(`L ${round1(終点.x)} ${round1(終点.y)}`);
|
|
3175
3329
|
return parts.join(" ");
|
package/src/layout.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
fanIncomingEndpoints,
|
|
16
16
|
hopEdgeCrossings,
|
|
17
17
|
layoutEdges,
|
|
18
|
+
pullOutgoingFirstCorner,
|
|
18
19
|
repositionParallelLabels,
|
|
19
20
|
separateSharedStraightRuns,
|
|
20
21
|
slideLabelsAlongOwnPath,
|
|
@@ -346,6 +347,9 @@ export function layout(diag: CdlDiagram): LaidDiagram {
|
|
|
346
347
|
// 弧が確定してから、 同じ節の組を結ぶ辺の label を「兄弟の線を跨がない位置」 へ置き直す (#376)。
|
|
347
348
|
// 順序が逆だと、 直線を前提に置いた label を弧が貫く。 label を先に線から離すと、 今度は弧を
|
|
348
349
|
// 膨らませる判定が label を障害物と見なして弧が消える。
|
|
350
|
+
// 同じ辺から出る線は、 最長の 1 本を除いて最初の直線部を 40 world まで引く (#651)。
|
|
351
|
+
// 経路の引き方を変えず、 確定後の最初の折れだけを動かす。
|
|
352
|
+
pullOutgoingFirstCorner(edges, nodesFinal);
|
|
349
353
|
// 同じ辺に入る線の終点を辺に沿って離す (#611)。 経路を引き終えてから当てる = 終点を先に
|
|
350
354
|
// 動かすと引き方の分岐が変わり、 迂回していた線が箱を貫通する (実測で 0 件が 7 件になった)。
|
|
351
355
|
fanIncomingEndpoints(edges, nodesFinal);
|