@cardenelabs/cdl 0.47.0 → 0.49.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 +57 -1
- package/dist/index.cjs +230 -105
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +230 -105
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +219 -101
- package/dist/react.cjs.map +1 -1
- package/dist/react.js +219 -101
- package/dist/react.js.map +1 -1
- package/package.json +1 -1
- package/src/layout/collisions.ts +293 -139
- package/src/layout/edges.ts +95 -23
- package/src/layout-with-validation.ts +4 -2
- package/src/visual-validate.ts +62 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cardenelabs/cdl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.49.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/collisions.ts
CHANGED
|
@@ -2212,57 +2212,17 @@ export function resolveEdgeLabelOverlapsWithChainAndPropagate(
|
|
|
2212
2212
|
};
|
|
2213
2213
|
}
|
|
2214
2214
|
|
|
2215
|
+
/** 札の逃がし先を測るのに使う、 線の区間と外接矩形。 同じ図の中で使い回す (#810)。 */
|
|
2216
|
+
interface 線の測り方 {
|
|
2217
|
+
readonly 区間: (id: string, d: string) => Seg[];
|
|
2218
|
+
readonly 遠い: (box: BBox, edgeId: string, d: string, 余裕: number) => boolean;
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2215
2221
|
/**
|
|
2216
|
-
*
|
|
2217
|
-
*
|
|
2218
|
-
* ## なぜ最終 pass が要るか
|
|
2219
|
-
*
|
|
2220
|
-
* `resolveEdgeLabelOverlapsWithChainAndPropagate` は経路を動かす 4 つの pass
|
|
2221
|
-
* (`bowSharedStraightPaths` / `pullOutgoingFirstCorner` / `fanIncomingEndpoints` /
|
|
2222
|
-
* `separateSharedStraightRuns`) の **前** に走る。 札の初期位置は経路から決まるため、
|
|
2223
|
-
* 後の pass が経路を動かすと札も一緒に動く。 動いた後を見る者が誰も居なかった。
|
|
2224
|
-
*
|
|
2225
|
-
* 0.30.0 で `pullOutgoingFirstCorner` (#651) と `fanIncomingEndpoints` (#649) が経路を
|
|
2226
|
-
* 大きく動かすようになり、 消費側の見本帳で札が 11 件動いた (実測 = 最大 217 world)。
|
|
2227
|
-
* 動いた先が箱の近くだと `clearance` 軸の要求 (箱と札で 32) を割ったまま残る。
|
|
2228
|
-
* 実測 = 0.29.0 で 0 件だった違反が 0.30.1 で 9 件。
|
|
2229
|
-
*
|
|
2230
|
-
* ## 何を動かすか
|
|
2231
|
-
*
|
|
2232
|
-
* **札だけを動かす**。 箱と経路は動かさない = ここで経路を動かすと、 直前の 4 pass が
|
|
2233
|
-
* 確定させた形 (根元の重なり分け / 起点の直進) を崩すことになる。
|
|
2234
|
-
*
|
|
2235
|
-
* 判定は門 (`detectNearCollisions`) と同じ `requiredNearClearance` ・ `computeGap` ・
|
|
2236
|
-
* `measurePathLabelGapSegs` を引く。 別の式で近似すると、 門は落ちるのに押し出しは動かない
|
|
2237
|
-
* 状態に戻る。
|
|
2238
|
-
*
|
|
2239
|
-
* ## 行き先を確かめてから動かす
|
|
2240
|
-
*
|
|
2241
|
-
* 一番安く済む軸へ出すだけだと、 上下や左右の箱に挟まれた札が往復して収束しない
|
|
2242
|
-
* (実測 = 箱 2 つの間で 16 下げて 4 上げるのを繰り返す)。
|
|
2243
|
-
* 軸ごとに行き先を作り、 **他のどの箱 ・ 札 ・ 線とも今より近づかない** 向きだけを採る。
|
|
2244
|
-
* どの向きも安全でなければ動かさない = 直せなかったことは門が引き続き報告する。
|
|
2245
|
-
*
|
|
2246
|
-
* ## 動かしすぎない
|
|
2247
|
-
*
|
|
2248
|
-
* 累積は `CHAIN_SHIFT_MAX_ACCUMULATED` で頭を打つ。 札は自分の線の近くに居ることが
|
|
2249
|
-
* 読みの前提で、 離しすぎるとどの線の札か判らなくなる。
|
|
2222
|
+
* 線の測り方を作る。 逃がし pass と、 門が「なぜ動かせなかったか」 を説明する側
|
|
2223
|
+
* (`札が動けない理由`) の両方が使う = 2 箇所で別に書くと、 片方だけ直してずれる。
|
|
2250
2224
|
*/
|
|
2251
|
-
|
|
2252
|
-
edges: LaidEdge[],
|
|
2253
|
-
obstacles: BBox[],
|
|
2254
|
-
maxIter = 8,
|
|
2255
|
-
): LaidEdge[] {
|
|
2256
|
-
if (edges.length === 0) return edges;
|
|
2257
|
-
|
|
2258
|
-
const labeledIds = new Set(
|
|
2259
|
-
edges.filter((e) => e.label?.trim() || e.sub?.trim()).map((e) => e.id),
|
|
2260
|
-
);
|
|
2261
|
-
if (labeledIds.size === 0) return edges;
|
|
2262
|
-
|
|
2263
|
-
const 箱たち = obstacles.filter((o) => o.kind === "node" || o.kind === "lane-label");
|
|
2264
|
-
const edgeById = new Map<string, LaidEdge>();
|
|
2265
|
-
for (const e of edges) edgeById.set(e.id, e);
|
|
2225
|
+
function 線の測り方を作る(): 線の測り方 {
|
|
2266
2226
|
/*
|
|
2267
2227
|
* 線の区間は **曲線を開いた形** で持つ (`detectCollisions` の札 branch と同じ)。
|
|
2268
2228
|
*
|
|
@@ -2313,12 +2273,287 @@ export function resolveLabelClearanceAfterPathPasses(
|
|
|
2313
2273
|
return Math.hypot(dx, dy) >= 余裕;
|
|
2314
2274
|
};
|
|
2315
2275
|
|
|
2276
|
+
return { 区間, 遠い };
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
/*
|
|
2280
|
+
* 動かす量の頭。 読みの上限は「自分の線から離れすぎない」 ことで、 それは `安全か` が
|
|
2281
|
+
* 距離そのもので見る。 ここは念のための頭で、 chain 伝搬の頭 (`CHAIN_SHIFT_MAX_ACCUMULATED`)
|
|
2282
|
+
* より緩くしてよい = この pass は札しか動かさず、 箱や経路を引きずらない。
|
|
2283
|
+
*/
|
|
2284
|
+
const 札の最大移動 = CHAIN_SHIFT_MAX_ACCUMULATED * 2;
|
|
2285
|
+
|
|
2286
|
+
/** 軸ごとの離れ量。 重なっている時は負になる */
|
|
2287
|
+
const 離れ量 = (a: BBox, b: BBox): { x: number; y: number } => ({
|
|
2288
|
+
x: Math.max(a.x - (b.x + b.w), b.x - (a.x + a.w)),
|
|
2289
|
+
y: Math.max(a.y - (b.y + b.h), b.y - (a.y + a.h)),
|
|
2290
|
+
});
|
|
2291
|
+
|
|
2292
|
+
const 動かした箱 = (box: BBox, axis: "x" | "y", sign: 1 | -1, amount: number): BBox => ({
|
|
2293
|
+
...box,
|
|
2294
|
+
x: box.x + (axis === "x" ? sign * amount : 0),
|
|
2295
|
+
y: box.y + (axis === "y" ? sign * amount : 0),
|
|
2296
|
+
});
|
|
2297
|
+
|
|
2298
|
+
/** 札の逃がし候補。 軸 ・ 向き ・ 量 */
|
|
2299
|
+
type 逃がし候補 = { axis: "x" | "y"; sign: 1 | -1; amount: number };
|
|
2300
|
+
|
|
2301
|
+
/** 札を 1 つ動かした先で、 いまより近づいてしまう相手 (#810)。 */
|
|
2302
|
+
export interface 札の移動を塞ぐ相手 {
|
|
2303
|
+
/** `種類:id`。 自分の線から離れすぎる時は `自分の線` */
|
|
2304
|
+
readonly 相手: string;
|
|
2305
|
+
/** 動かす前の隙間 */
|
|
2306
|
+
readonly 前: number;
|
|
2307
|
+
/** 動かした後の隙間 */
|
|
2308
|
+
readonly 後: number;
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
/** 札を動かす判定に使う、 図の中の相手 */
|
|
2312
|
+
interface 札の周り {
|
|
2313
|
+
readonly 箱たち: readonly BBox[];
|
|
2314
|
+
readonly labels: readonly BBox[];
|
|
2315
|
+
readonly edges: readonly LaidEdge[];
|
|
2316
|
+
readonly edgeById: ReadonlyMap<string, LaidEdge>;
|
|
2317
|
+
readonly 測り方: 線の測り方;
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2320
|
+
/**
|
|
2321
|
+
* 札 1 つを動かした先が、 いまより近づく相手を作らないか。 作るなら最初の 1 つを返す。
|
|
2322
|
+
*
|
|
2323
|
+
* 逃がし pass の `安全か` と、 門の説明 (`札が動けない理由`) が同じものを引く (#810)。
|
|
2324
|
+
* 2 箇所で別に書くと、 「pass は動かせないのに説明は動かせると言う」 ずれが生まれる。
|
|
2325
|
+
*/
|
|
2326
|
+
function 札の移動を塞ぐ相手を探す(
|
|
2327
|
+
周り: 札の周り,
|
|
2328
|
+
label: BBox,
|
|
2329
|
+
行き先: BBox,
|
|
2330
|
+
): 札の移動を塞ぐ相手 | null {
|
|
2331
|
+
const { 箱たち, labels, edges, edgeById, 測り方 } = 周り;
|
|
2332
|
+
for (const obs of 箱たち) {
|
|
2333
|
+
const 前 = computeGap(label, obs);
|
|
2334
|
+
const 後 = computeGap(行き先, obs);
|
|
2335
|
+
if (後 < Math.min(前, requiredNearClearance("edge-label", obs.kind))) return { 相手: `${obs.kind}:${obs.id}`, 前, 後 };
|
|
2336
|
+
}
|
|
2337
|
+
const 札の要求 = requiredNearClearance("edge-label", "edge-label");
|
|
2338
|
+
for (const 他 of labels) {
|
|
2339
|
+
if (他.id === label.id) continue;
|
|
2340
|
+
const 前 = computeGap(label, 他);
|
|
2341
|
+
const 後 = computeGap(行き先, 他);
|
|
2342
|
+
if (後 < Math.min(前, 札の要求)) return { 相手: `edge-label:${他.id}`, 前, 後 };
|
|
2343
|
+
}
|
|
2344
|
+
// 線との距離は重いので最後に見る。 ここまでで落ちる候補が大半。
|
|
2345
|
+
const 自分 = edgeById.get(label.id);
|
|
2346
|
+
const 自分の区間 = 自分 ? 測り方.区間(自分.id, 自分.d) : [];
|
|
2316
2347
|
/*
|
|
2317
|
-
*
|
|
2318
|
-
*
|
|
2319
|
-
* より緩くしてよい = この pass は札しか動かさず、 箱や経路を引きずらない。
|
|
2348
|
+
* **自分の線から離れすぎない**。 札は自分の線の近くに在ることが読みの前提で、
|
|
2349
|
+
* 離すと どの線の説明なのかが判らなくなる。 いま既に離れている札はそれ以上離さない。
|
|
2320
2350
|
*/
|
|
2321
|
-
|
|
2351
|
+
if (自分の区間.length > 0) {
|
|
2352
|
+
const 前 = measurePathLabelGapSegs(自分の区間, label);
|
|
2353
|
+
const 後 = measurePathLabelGapSegs(自分の区間, 行き先);
|
|
2354
|
+
if (後 > Math.max(前, DIST_LABEL_PATH_MAX)) return { 相手: "自分の線", 前, 後 };
|
|
2355
|
+
}
|
|
2356
|
+
for (const 相手 of edges) {
|
|
2357
|
+
if (相手.id === label.id) continue;
|
|
2358
|
+
// 動かす前も後も要求ぶん離れている線は、 測らずに落とす。
|
|
2359
|
+
if (測り方.遠い(label, 相手.id, 相手.d, LABEL_TO_PATH_CLEARANCE) && 測り方.遠い(行き先, 相手.id, 相手.d, LABEL_TO_PATH_CLEARANCE)) continue;
|
|
2360
|
+
const segs = segmentsExcludingOwnPath(測り方.区間(相手.id, 相手.d), 自分の区間);
|
|
2361
|
+
if (segs.length === 0) continue;
|
|
2362
|
+
const 前 = measurePathLabelGapSegs(segs, label);
|
|
2363
|
+
const 後 = measurePathLabelGapSegs(segs, 行き先);
|
|
2364
|
+
if (後 < Math.min(前, LABEL_TO_PATH_CLEARANCE)) return { 相手: `edge-path:${相手.id}`, 前, 後 };
|
|
2365
|
+
}
|
|
2366
|
+
return null;
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2369
|
+
/** 矩形どうしが要求を満たしていない時の、 軸ごとの逃がし候補 */
|
|
2370
|
+
function 矩形の候補(
|
|
2371
|
+
label: BBox,
|
|
2372
|
+
static箱: BBox,
|
|
2373
|
+
required: number,
|
|
2374
|
+
): 逃がし候補[] {
|
|
2375
|
+
const sep = 離れ量(label, static箱);
|
|
2376
|
+
const 向き = (axis: "x" | "y"): 1 | -1 => {
|
|
2377
|
+
const a = axis === "x" ? label.x + label.w / 2 : label.y + label.h / 2;
|
|
2378
|
+
const b = axis === "x" ? static箱.x + static箱.w / 2 : static箱.y + static箱.h / 2;
|
|
2379
|
+
return a >= b ? 1 : -1;
|
|
2380
|
+
};
|
|
2381
|
+
return [
|
|
2382
|
+
{ axis: "x" as const, sign: 向き("x"), amount: required - sep.x + NEAR_MISS_MARGIN },
|
|
2383
|
+
{ axis: "y" as const, sign: 向き("y"), amount: required - sep.y + NEAR_MISS_MARGIN },
|
|
2384
|
+
];
|
|
2385
|
+
}
|
|
2386
|
+
|
|
2387
|
+
/**
|
|
2388
|
+
* 札を軸の向きへ動かして、 線から `LABEL_TO_PATH_CLEARANCE` 離れるのに要る量。
|
|
2389
|
+
* 上限 (`札の最大移動`) まで伸ばしても満たせない向きは `null`。
|
|
2390
|
+
*/
|
|
2391
|
+
function 線から離れる量(
|
|
2392
|
+
segs: Seg[],
|
|
2393
|
+
label: BBox,
|
|
2394
|
+
gap: number,
|
|
2395
|
+
axis: "x" | "y",
|
|
2396
|
+
sign: 1 | -1,
|
|
2397
|
+
): number | null {
|
|
2398
|
+
const 最大移動 = 札の最大移動;
|
|
2399
|
+
const 満たす = (量: number): boolean =>
|
|
2400
|
+
measurePathLabelGapSegs(segs, 動かした箱(label, axis, sign, 量)) >= LABEL_TO_PATH_CLEARANCE;
|
|
2401
|
+
let lo = 0;
|
|
2402
|
+
let hi = Math.max(4, LABEL_TO_PATH_CLEARANCE - gap);
|
|
2403
|
+
// **上限そのものを試してから諦める**。 2 倍で伸ばした値が上限を跨いだ所で打ち切ると、
|
|
2404
|
+
// 上限内に満たす量があっても見つけられない (実測 = 83 で足りるのに 128 で諦めていた)。
|
|
2405
|
+
while (!満たす(hi)) {
|
|
2406
|
+
if (hi >= 最大移動) return null;
|
|
2407
|
+
lo = hi;
|
|
2408
|
+
hi = Math.min(hi * 2, 最大移動);
|
|
2409
|
+
}
|
|
2410
|
+
for (let k = 0; k < 12; k += 1) {
|
|
2411
|
+
const mid = (lo + hi) / 2;
|
|
2412
|
+
if (満たす(mid)) hi = mid;
|
|
2413
|
+
else lo = mid;
|
|
2414
|
+
}
|
|
2415
|
+
return hi + NEAR_MISS_MARGIN;
|
|
2416
|
+
}
|
|
2417
|
+
|
|
2418
|
+
/** 報告に出す向きの呼び名。 並べる順もこの順にする */
|
|
2419
|
+
const 向きの順: ReadonlyArray<{ axis: "x" | "y"; sign: 1 | -1; 名前: string }> = [
|
|
2420
|
+
{ axis: "y", sign: -1, 名前: "上" },
|
|
2421
|
+
{ axis: "y", sign: 1, 名前: "下" },
|
|
2422
|
+
{ axis: "x", sign: -1, 名前: "左" },
|
|
2423
|
+
{ axis: "x", sign: 1, 名前: "右" },
|
|
2424
|
+
];
|
|
2425
|
+
|
|
2426
|
+
/**
|
|
2427
|
+
* 門が `clearance` の違反を報告する時に、 札が **なぜ動けなかったか** を 1 行で返す (#810)。
|
|
2428
|
+
*
|
|
2429
|
+
* 違反の報告は「どの 2 つが何 px で、 要求が何 px か」 しか持たない。 読んだ人が次に知りたいのは
|
|
2430
|
+
* 「なぜ engine はこれを直せなかったのか」 で、 それを探すのに層を 3 つ読む必要があった。
|
|
2431
|
+
* 理由は逃がし pass の中で分かっている = 向きごとに行き先を作り、 塞ぐ相手が居る向きを捨てている。
|
|
2432
|
+
*
|
|
2433
|
+
* **最終の位置から測り直す**。 layout の途中の記録を運ぶと、 途中で逃がせたが別の pass で押し戻された
|
|
2434
|
+
* 形や、 走査を打ち切った形で、 古い理由が残る。 逃がし pass と同じ候補 ・ 同じ判定
|
|
2435
|
+
* (`札の移動を塞ぐ相手を探す`) を最終の位置で試せば、 報告と実物がずれない。
|
|
2436
|
+
*
|
|
2437
|
+
* どれか 1 つの向きでも安全なら `null` を返す = 動けるのに動いていない形について、
|
|
2438
|
+
* 「塞がれている」 と言うのは誤り。
|
|
2439
|
+
*
|
|
2440
|
+
* @param labelId 動かせなかった札の辺 id
|
|
2441
|
+
* @param 相手 違反の相手 (箱 / 縦列の題 / 別の札 / 別の線)
|
|
2442
|
+
* @param edges 図の全ての辺 (最終の位置)
|
|
2443
|
+
* @param obstacles 箱と縦列の題の矩形
|
|
2444
|
+
*/
|
|
2445
|
+
export function 札が動けない理由(
|
|
2446
|
+
labelId: string,
|
|
2447
|
+
相手: BBox,
|
|
2448
|
+
edges: readonly LaidEdge[],
|
|
2449
|
+
obstacles: readonly BBox[],
|
|
2450
|
+
): string | null {
|
|
2451
|
+
const edgeById = new Map(edges.map((e) => [e.id, e] as const));
|
|
2452
|
+
const 自分 = edgeById.get(labelId);
|
|
2453
|
+
if (!自分) return null;
|
|
2454
|
+
const labels: BBox[] = edges
|
|
2455
|
+
.filter((e) => e.label?.trim() || e.sub?.trim())
|
|
2456
|
+
.map((e) => ({ kind: "edge-label" as const, id: e.id, ...computeLabelBBoxWorld(e) }));
|
|
2457
|
+
const label = labels.find((l) => l.id === labelId);
|
|
2458
|
+
if (!label) return null;
|
|
2459
|
+
const 測り方 = 線の測り方を作る();
|
|
2460
|
+
const 周り: 札の周り = {
|
|
2461
|
+
箱たち: obstacles.filter((o) => o.kind === "node" || o.kind === "lane-label"),
|
|
2462
|
+
labels,
|
|
2463
|
+
edges,
|
|
2464
|
+
edgeById,
|
|
2465
|
+
測り方,
|
|
2466
|
+
};
|
|
2467
|
+
|
|
2468
|
+
// 逃がし pass と同じ候補を作る。 線が相手なら 4 つの向き、 矩形が相手なら離れる側の 2 つ。
|
|
2469
|
+
const 候補: Array<{ axis: "x" | "y"; sign: 1 | -1; amount: number | null }> = [];
|
|
2470
|
+
if (相手.kind === "edge-path") {
|
|
2471
|
+
const 線 = edgeById.get(相手.id);
|
|
2472
|
+
if (!線) return null;
|
|
2473
|
+
const segs = segmentsExcludingOwnPath(測り方.区間(線.id, 線.d), 測り方.区間(自分.id, 自分.d));
|
|
2474
|
+
if (segs.length === 0) return null;
|
|
2475
|
+
const gap = measurePathLabelGapSegs(segs, label);
|
|
2476
|
+
for (const 向き of 向きの順) {
|
|
2477
|
+
候補.push({ axis: 向き.axis, sign: 向き.sign, amount: 線から離れる量(segs, label, gap, 向き.axis, 向き.sign) });
|
|
2478
|
+
}
|
|
2479
|
+
} else {
|
|
2480
|
+
const required = requiredNearClearance("edge-label", 相手.kind);
|
|
2481
|
+
for (const c of 矩形の候補(label, 相手, required)) {
|
|
2482
|
+
// 既に離れている軸は候補にしない (逃がし pass も量 0 以下を飛ばす)
|
|
2483
|
+
if (c.amount > 0) 候補.push(c);
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
if (候補.length === 0) return null;
|
|
2487
|
+
|
|
2488
|
+
const 理由: Array<{ 順: number; 文: string }> = [];
|
|
2489
|
+
for (const c of 候補) {
|
|
2490
|
+
const 順 = 向きの順.findIndex((v) => v.axis === c.axis && v.sign === c.sign);
|
|
2491
|
+
const 名前 = 向きの順[順]!.名前;
|
|
2492
|
+
if (c.amount === null || c.amount > 札の最大移動) {
|
|
2493
|
+
理由.push({ 順, 文: `${名前}=上限 ${札の最大移動} を越える` });
|
|
2494
|
+
continue;
|
|
2495
|
+
}
|
|
2496
|
+
const 塞ぐ = 札の移動を塞ぐ相手を探す(周り, label, 動かした箱(label, c.axis, c.sign, c.amount));
|
|
2497
|
+
if (!塞ぐ) return null;
|
|
2498
|
+
理由.push({ 順, 文: `${名前}=${塞ぐ.相手} (${塞ぐ.前.toFixed(0)}→${塞ぐ.後.toFixed(0)})` });
|
|
2499
|
+
}
|
|
2500
|
+
return `逃げ場なし: ${理由.sort((a, b) => a.順 - b.順).map((r) => r.文).join(" / ")}`;
|
|
2501
|
+
}
|
|
2502
|
+
|
|
2503
|
+
/**
|
|
2504
|
+
* 経路を動かす pass の **後** に、 札だけを箱 ・ 札 ・ 線から離す (#659)。
|
|
2505
|
+
*
|
|
2506
|
+
* ## なぜ最終 pass が要るか
|
|
2507
|
+
*
|
|
2508
|
+
* `resolveEdgeLabelOverlapsWithChainAndPropagate` は経路を動かす 4 つの pass
|
|
2509
|
+
* (`bowSharedStraightPaths` / `pullOutgoingFirstCorner` / `fanIncomingEndpoints` /
|
|
2510
|
+
* `separateSharedStraightRuns`) の **前** に走る。 札の初期位置は経路から決まるため、
|
|
2511
|
+
* 後の pass が経路を動かすと札も一緒に動く。 動いた後を見る者が誰も居なかった。
|
|
2512
|
+
*
|
|
2513
|
+
* 0.30.0 で `pullOutgoingFirstCorner` (#651) と `fanIncomingEndpoints` (#649) が経路を
|
|
2514
|
+
* 大きく動かすようになり、 消費側の見本帳で札が 11 件動いた (実測 = 最大 217 world)。
|
|
2515
|
+
* 動いた先が箱の近くだと `clearance` 軸の要求 (箱と札で 32) を割ったまま残る。
|
|
2516
|
+
* 実測 = 0.29.0 で 0 件だった違反が 0.30.1 で 9 件。
|
|
2517
|
+
*
|
|
2518
|
+
* ## 何を動かすか
|
|
2519
|
+
*
|
|
2520
|
+
* **札だけを動かす**。 箱と経路は動かさない = ここで経路を動かすと、 直前の 4 pass が
|
|
2521
|
+
* 確定させた形 (根元の重なり分け / 起点の直進) を崩すことになる。
|
|
2522
|
+
*
|
|
2523
|
+
* 判定は門 (`detectNearCollisions`) と同じ `requiredNearClearance` ・ `computeGap` ・
|
|
2524
|
+
* `measurePathLabelGapSegs` を引く。 別の式で近似すると、 門は落ちるのに押し出しは動かない
|
|
2525
|
+
* 状態に戻る。
|
|
2526
|
+
*
|
|
2527
|
+
* ## 行き先を確かめてから動かす
|
|
2528
|
+
*
|
|
2529
|
+
* 一番安く済む軸へ出すだけだと、 上下や左右の箱に挟まれた札が往復して収束しない
|
|
2530
|
+
* (実測 = 箱 2 つの間で 16 下げて 4 上げるのを繰り返す)。
|
|
2531
|
+
* 軸ごとに行き先を作り、 **他のどの箱 ・ 札 ・ 線とも今より近づかない** 向きだけを採る。
|
|
2532
|
+
* どの向きも安全でなければ動かさない = 直せなかったことは門が引き続き報告する。
|
|
2533
|
+
*
|
|
2534
|
+
* ## 動かしすぎない
|
|
2535
|
+
*
|
|
2536
|
+
* 累積は `CHAIN_SHIFT_MAX_ACCUMULATED` で頭を打つ。 札は自分の線の近くに居ることが
|
|
2537
|
+
* 読みの前提で、 離しすぎるとどの線の札か判らなくなる。
|
|
2538
|
+
*/
|
|
2539
|
+
export function resolveLabelClearanceAfterPathPasses(
|
|
2540
|
+
edges: LaidEdge[],
|
|
2541
|
+
obstacles: BBox[],
|
|
2542
|
+
maxIter = 8,
|
|
2543
|
+
): LaidEdge[] {
|
|
2544
|
+
if (edges.length === 0) return edges;
|
|
2545
|
+
|
|
2546
|
+
const labeledIds = new Set(
|
|
2547
|
+
edges.filter((e) => e.label?.trim() || e.sub?.trim()).map((e) => e.id),
|
|
2548
|
+
);
|
|
2549
|
+
if (labeledIds.size === 0) return edges;
|
|
2550
|
+
|
|
2551
|
+
const 箱たち = obstacles.filter((o) => o.kind === "node" || o.kind === "lane-label");
|
|
2552
|
+
const edgeById = new Map<string, LaidEdge>();
|
|
2553
|
+
for (const e of edges) edgeById.set(e.id, e);
|
|
2554
|
+
const 測り方 = 線の測り方を作る();
|
|
2555
|
+
const { 区間, 遠い } = 測り方;
|
|
2556
|
+
const 最大移動 = 札の最大移動;
|
|
2322
2557
|
|
|
2323
2558
|
const shiftMap = new Map<string, { dx: number; dy: number }>();
|
|
2324
2559
|
const getShift = (id: string): { dx: number; dy: number } =>
|
|
@@ -2335,18 +2570,6 @@ export function resolveLabelClearanceAfterPathPasses(
|
|
|
2335
2570
|
return out;
|
|
2336
2571
|
};
|
|
2337
2572
|
|
|
2338
|
-
/** 軸ごとの離れ量。 重なっている時は負になる */
|
|
2339
|
-
const 離れ量 = (a: BBox, b: BBox): { x: number; y: number } => ({
|
|
2340
|
-
x: Math.max(a.x - (b.x + b.w), b.x - (a.x + a.w)),
|
|
2341
|
-
y: Math.max(a.y - (b.y + b.h), b.y - (a.y + a.h)),
|
|
2342
|
-
});
|
|
2343
|
-
|
|
2344
|
-
const 動かした箱 = (box: BBox, axis: "x" | "y", sign: 1 | -1, amount: number): BBox => ({
|
|
2345
|
-
...box,
|
|
2346
|
-
x: box.x + (axis === "x" ? sign * amount : 0),
|
|
2347
|
-
y: box.y + (axis === "y" ? sign * amount : 0),
|
|
2348
|
-
});
|
|
2349
|
-
|
|
2350
2573
|
for (let iter = 0; iter < maxIter; iter += 1) {
|
|
2351
2574
|
const labels = labelBBoxes();
|
|
2352
2575
|
const 札の要求 = requiredNearClearance("edge-label", "edge-label");
|
|
@@ -2360,47 +2583,13 @@ export function resolveLabelClearanceAfterPathPasses(
|
|
|
2360
2583
|
const 今回動かした = new Set<string>();
|
|
2361
2584
|
|
|
2362
2585
|
/** 札 1 つを動かした先が、 いまより近づく相手を作らないか */
|
|
2363
|
-
const 安全か = (label: BBox, 行き先: BBox): boolean =>
|
|
2364
|
-
|
|
2365
|
-
const 前 = computeGap(label, obs);
|
|
2366
|
-
const 後 = computeGap(行き先, obs);
|
|
2367
|
-
if (後 < Math.min(前, requiredNearClearance("edge-label", obs.kind))) return false;
|
|
2368
|
-
}
|
|
2369
|
-
for (const 他 of labels) {
|
|
2370
|
-
if (他.id === label.id) continue;
|
|
2371
|
-
const 前 = computeGap(label, 他);
|
|
2372
|
-
const 後 = computeGap(行き先, 他);
|
|
2373
|
-
if (後 < Math.min(前, 札の要求)) return false;
|
|
2374
|
-
}
|
|
2375
|
-
// 線との距離は重いので最後に見る。 ここまでで落ちる候補が大半。
|
|
2376
|
-
const 自分 = edgeById.get(label.id);
|
|
2377
|
-
const 自分の区間 = 自分 ? 区間(自分.id, 自分.d) : [];
|
|
2378
|
-
/*
|
|
2379
|
-
* **自分の線から離れすぎない**。 札は自分の線の近くに在ることが読みの前提で、
|
|
2380
|
-
* 離すと どの線の説明なのかが判らなくなる。 いま既に離れている札はそれ以上離さない。
|
|
2381
|
-
*/
|
|
2382
|
-
if (自分の区間.length > 0) {
|
|
2383
|
-
const 前 = measurePathLabelGapSegs(自分の区間, label);
|
|
2384
|
-
const 後 = measurePathLabelGapSegs(自分の区間, 行き先);
|
|
2385
|
-
if (後 > Math.max(前, DIST_LABEL_PATH_MAX)) return false;
|
|
2386
|
-
}
|
|
2387
|
-
for (const 相手 of edges) {
|
|
2388
|
-
if (相手.id === label.id) continue;
|
|
2389
|
-
// 動かす前も後も要求ぶん離れている線は、 測らずに落とす。
|
|
2390
|
-
if (遠い(label, 相手.id, 相手.d, LABEL_TO_PATH_CLEARANCE) && 遠い(行き先, 相手.id, 相手.d, LABEL_TO_PATH_CLEARANCE)) continue;
|
|
2391
|
-
const segs = segmentsExcludingOwnPath(区間(相手.id, 相手.d), 自分の区間);
|
|
2392
|
-
if (segs.length === 0) continue;
|
|
2393
|
-
const 前 = measurePathLabelGapSegs(segs, label);
|
|
2394
|
-
const 後 = measurePathLabelGapSegs(segs, 行き先);
|
|
2395
|
-
if (後 < Math.min(前, LABEL_TO_PATH_CLEARANCE)) return false;
|
|
2396
|
-
}
|
|
2397
|
-
return true;
|
|
2398
|
-
};
|
|
2586
|
+
const 安全か = (label: BBox, 行き先: BBox): boolean =>
|
|
2587
|
+
札の移動を塞ぐ相手を探す({ 箱たち, labels, edges, edgeById, 測り方 }, label, 行き先) === null;
|
|
2399
2588
|
|
|
2400
2589
|
/** 候補を安い順に試し、 行き先が安全な最初の 1 つを積む */
|
|
2401
2590
|
const 逃がす = (
|
|
2402
2591
|
label: BBox,
|
|
2403
|
-
候補:
|
|
2592
|
+
候補: 逃がし候補[],
|
|
2404
2593
|
): boolean => {
|
|
2405
2594
|
for (const c of 候補.slice().sort((a, b) => a.amount - b.amount)) {
|
|
2406
2595
|
if (c.amount <= 0) continue;
|
|
@@ -2417,24 +2606,6 @@ export function resolveLabelClearanceAfterPathPasses(
|
|
|
2417
2606
|
return false;
|
|
2418
2607
|
};
|
|
2419
2608
|
|
|
2420
|
-
/** 矩形どうしが要求を満たしていない時の、 軸ごとの逃がし候補 */
|
|
2421
|
-
const 矩形の候補 = (
|
|
2422
|
-
label: BBox,
|
|
2423
|
-
static箱: BBox,
|
|
2424
|
-
required: number,
|
|
2425
|
-
): Array<{ axis: "x" | "y"; sign: 1 | -1; amount: number }> => {
|
|
2426
|
-
const sep = 離れ量(label, static箱);
|
|
2427
|
-
const 向き = (axis: "x" | "y"): 1 | -1 => {
|
|
2428
|
-
const a = axis === "x" ? label.x + label.w / 2 : label.y + label.h / 2;
|
|
2429
|
-
const b = axis === "x" ? static箱.x + static箱.w / 2 : static箱.y + static箱.h / 2;
|
|
2430
|
-
return a >= b ? 1 : -1;
|
|
2431
|
-
};
|
|
2432
|
-
return [
|
|
2433
|
-
{ axis: "x" as const, sign: 向き("x"), amount: required - sep.x + NEAR_MISS_MARGIN },
|
|
2434
|
-
{ axis: "y" as const, sign: 向き("y"), amount: required - sep.y + NEAR_MISS_MARGIN },
|
|
2435
|
-
];
|
|
2436
|
-
};
|
|
2437
|
-
|
|
2438
2609
|
// 札 ↔ 箱 / 縦列の題。 障害物は動かさないので札だけを逃がす。
|
|
2439
2610
|
for (const label of labels) {
|
|
2440
2611
|
if (今回動かした.has(label.id)) continue;
|
|
@@ -2506,25 +2677,8 @@ export function resolveLabelClearanceAfterPathPasses(
|
|
|
2506
2677
|
* 動かす量を 2 倍ずつ伸ばして満たす所を見つけ、 二分で詰める。 測り方は門と同じ
|
|
2507
2678
|
* `measurePathLabelGapSegs` を使う。 上限まで伸ばしても満たせない向きは候補にしない。
|
|
2508
2679
|
*/
|
|
2509
|
-
const 必要量 = (axis: "x" | "y", sign: 1 | -1): number | null =>
|
|
2510
|
-
|
|
2511
|
-
measurePathLabelGapSegs(segs, 動かした箱(label, axis, sign, 量)) >= LABEL_TO_PATH_CLEARANCE;
|
|
2512
|
-
let lo = 0;
|
|
2513
|
-
let hi = Math.max(4, LABEL_TO_PATH_CLEARANCE - gap);
|
|
2514
|
-
// **上限そのものを試してから諦める**。 2 倍で伸ばした値が上限を跨いだ所で打ち切ると、
|
|
2515
|
-
// 上限内に満たす量があっても見つけられない (実測 = 83 で足りるのに 128 で諦めていた)。
|
|
2516
|
-
while (!満たす(hi)) {
|
|
2517
|
-
if (hi >= 最大移動) return null;
|
|
2518
|
-
lo = hi;
|
|
2519
|
-
hi = Math.min(hi * 2, 最大移動);
|
|
2520
|
-
}
|
|
2521
|
-
for (let k = 0; k < 12; k += 1) {
|
|
2522
|
-
const mid = (lo + hi) / 2;
|
|
2523
|
-
if (満たす(mid)) hi = mid;
|
|
2524
|
-
else lo = mid;
|
|
2525
|
-
}
|
|
2526
|
-
return hi + NEAR_MISS_MARGIN;
|
|
2527
|
-
};
|
|
2680
|
+
const 必要量 = (axis: "x" | "y", sign: 1 | -1): number | null =>
|
|
2681
|
+
線から離れる量(segs, label, gap, axis, sign);
|
|
2528
2682
|
const dx = 芯.x - 近い点.x;
|
|
2529
2683
|
const dy = 芯.y - 近い点.y;
|
|
2530
2684
|
const 候補: Array<{ axis: "x" | "y"; sign: 1 | -1; amount: number }> = [];
|
package/src/layout/edges.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
import {
|
|
8
8
|
extractPathSegments,
|
|
9
9
|
flattenPathSegments,
|
|
10
|
+
measurePathLabelGapSegs,
|
|
10
11
|
segmentsCrossRect,
|
|
11
12
|
segmentsExcludingOwnPath,
|
|
12
13
|
} from "./collisions";
|
|
@@ -2530,37 +2531,108 @@ function placeAvoidingSiblingPaths(args: {
|
|
|
2530
2531
|
|
|
2531
2532
|
// 動かさない置き方も候補に入れる (跨りが無いならこれが最善)。
|
|
2532
2533
|
consider([...wants]);
|
|
2534
|
+
|
|
2535
|
+
/**
|
|
2536
|
+
* 塊を「近すぎる線」 の向こうへ押し出すのに要る量 (#805)。 足りていれば 0。
|
|
2537
|
+
*
|
|
2538
|
+
* 兄弟の弧の帯 (`bands`) を越えるだけでは、 **帯の外を通る別の線** が残る。
|
|
2539
|
+
* 実測 = 4 本の弧の下を回る線が、 帯を越えた直後に置いた札の上端を 10 かすめた
|
|
2540
|
+
* (要求は `CLEARANCE_PATH_LABEL` = 14)。 貫いてはいないので `consider` の跨り判定では
|
|
2541
|
+
* 捨てられず、 その後の逃がし pass も列の上下が隣の札で塞がれていて動かせない。
|
|
2542
|
+
*
|
|
2543
|
+
* 押す量は探さずに測る = 邪魔をしている線を札の横幅で切った Y 範囲を取り、 その縁から
|
|
2544
|
+
* 要求ぶん離れる位置までの差を返す。 塊は間隔を保ったまま動かしたいので、 塊の中で
|
|
2545
|
+
* 最も要る量に合わせる。
|
|
2546
|
+
*/
|
|
2547
|
+
const 押す量 = (idx: number[], side: "above" | "below", pos: number[]): number => {
|
|
2548
|
+
let 要る = 0;
|
|
2549
|
+
for (const i of idx) {
|
|
2550
|
+
const box = boxes[i]!;
|
|
2551
|
+
const rect = { x: box.x, y: pos[i]! - box.h / 2, w: box.w, h: box.h };
|
|
2552
|
+
for (const probe of probeSegs[i]!) {
|
|
2553
|
+
if (measurePathLabelGapSegs(probe.segs, rect) >= CLEARANCE_PATH_LABEL) continue;
|
|
2554
|
+
let lo = Infinity;
|
|
2555
|
+
let hi = -Infinity;
|
|
2556
|
+
for (const seg of probe.segs) {
|
|
2557
|
+
const c = clipY(seg, box.x, box.x + box.w);
|
|
2558
|
+
if (!c) continue;
|
|
2559
|
+
lo = Math.min(lo, c.lo);
|
|
2560
|
+
hi = Math.max(hi, c.hi);
|
|
2561
|
+
}
|
|
2562
|
+
// 札の横幅の下に 1 本も入らない線は、 押しても隙間が変わらない。
|
|
2563
|
+
if (lo > hi) continue;
|
|
2564
|
+
const 行き先 =
|
|
2565
|
+
side === "above"
|
|
2566
|
+
? lo - box.h / 2 - CLEARANCE_PATH_LABEL
|
|
2567
|
+
: hi + box.h / 2 + CLEARANCE_PATH_LABEL;
|
|
2568
|
+
要る = Math.max(要る, side === "above" ? pos[i]! - 行き先 : 行き先 - pos[i]!);
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
return Math.max(0, 要る);
|
|
2572
|
+
};
|
|
2573
|
+
|
|
2533
2574
|
if (bands.some((b) => b !== null)) {
|
|
2534
2575
|
for (let split = 0; split <= order.length; split++) {
|
|
2535
2576
|
const above = order.slice(0, split);
|
|
2536
2577
|
const below = order.slice(split);
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
if (idx.length === 0) continue;
|
|
2543
|
-
const ys = spread(idx);
|
|
2544
|
-
// 帯に入らない位置まで塊ごと動かす。 必要な移動量が最大の label に合わせる。
|
|
2545
|
-
let shift = 0;
|
|
2546
|
-
for (let k = 0; k < idx.length; k++) {
|
|
2547
|
-
const band = bands[idx[k]!];
|
|
2548
|
-
if (!band) continue;
|
|
2549
|
-
shift =
|
|
2550
|
-
side === "above" ? Math.min(shift, band.lo - ys[k]!) : Math.max(shift, band.hi - ys[k]!);
|
|
2551
|
-
}
|
|
2552
|
-
for (let k = 0; k < idx.length; k++) out[idx[k]!] = ys[k]! + shift;
|
|
2553
|
-
}
|
|
2554
|
-
// 上側の末尾と下側の先頭が足りなければ、 下側を塊ごとさらに下げる。 帯を挟んでいても
|
|
2555
|
-
// pill どうしの間隔は要る (実測 = 帯を挟んで 100 しか空かず、 要求の 104 に届かない図)。
|
|
2556
|
-
if (above.length > 0 && below.length > 0) {
|
|
2578
|
+
/** 上側の末尾と下側の先頭が足りなければ、 下側を塊ごとさらに下げる。 */
|
|
2579
|
+
const 間を空ける = (out: number[]): void => {
|
|
2580
|
+
// 帯を挟んでいても pill どうしの間隔は要る
|
|
2581
|
+
// (実測 = 帯を挟んで 100 しか空かず、 要求の 104 に届かない図)。
|
|
2582
|
+
if (above.length === 0 || below.length === 0) return;
|
|
2557
2583
|
const lastIdx = above[above.length - 1]!;
|
|
2558
2584
|
const firstIdx = below[0]!;
|
|
2559
2585
|
const need = (sizes[lastIdx]! + sizes[firstIdx]!) / 2 + CLEARANCE_LABEL_LABEL;
|
|
2560
2586
|
const deficit = need - (out[firstIdx]! - out[lastIdx]!);
|
|
2561
|
-
if (deficit > 0) for (const i of below) out[i]!
|
|
2562
|
-
}
|
|
2563
|
-
|
|
2587
|
+
if (deficit > 0) for (const i of below) out[i] = out[i]! + deficit;
|
|
2588
|
+
};
|
|
2589
|
+
/** 兄弟の弧の帯を越える所まで塊ごと動かした置き方。 */
|
|
2590
|
+
const 帯を越える = (): number[] => {
|
|
2591
|
+
const out = new Array<number>(members.length);
|
|
2592
|
+
for (const [idx, side] of [
|
|
2593
|
+
[above, "above"],
|
|
2594
|
+
[below, "below"],
|
|
2595
|
+
] as const) {
|
|
2596
|
+
if (idx.length === 0) continue;
|
|
2597
|
+
const ys = spread(idx);
|
|
2598
|
+
// 帯に入らない位置まで塊ごと動かす。 必要な移動量が最大の label に合わせる。
|
|
2599
|
+
let shift = 0;
|
|
2600
|
+
for (let k = 0; k < idx.length; k++) {
|
|
2601
|
+
const band = bands[idx[k]!];
|
|
2602
|
+
if (!band) continue;
|
|
2603
|
+
shift =
|
|
2604
|
+
side === "above" ? Math.min(shift, band.lo - ys[k]!) : Math.max(shift, band.hi - ys[k]!);
|
|
2605
|
+
}
|
|
2606
|
+
for (let k = 0; k < idx.length; k++) out[idx[k]!] = ys[k]! + shift;
|
|
2607
|
+
}
|
|
2608
|
+
間を空ける(out);
|
|
2609
|
+
return out;
|
|
2610
|
+
};
|
|
2611
|
+
/**
|
|
2612
|
+
* 帯を越えた置き方を、 さらに「近すぎる線」 の向こうへ押し出した置き方。
|
|
2613
|
+
*
|
|
2614
|
+
* 押した先に別の線が居ることがあるので繰り返す。 **越えられない形では候補にしない**
|
|
2615
|
+
* (`null` を返す) = 線が札の上下を挟む図では、 押しても隙間が足りないまま位置だけが動く。
|
|
2616
|
+
* 跨り判定はその置き方を捨てられないので、 候補にする前にここで落とす。
|
|
2617
|
+
*
|
|
2618
|
+
* 押しは上側を上へ、 下側を下へしか動かさない = 上下の間隔は広がる一方なので、
|
|
2619
|
+
* ここで `間を空ける` を呼び直しても 1 度も効かない (変異試験で確認した)。
|
|
2620
|
+
*/
|
|
2621
|
+
const 押して越える = (): number[] | null => {
|
|
2622
|
+
const out = 帯を越える();
|
|
2623
|
+
for (let round = 0; round < 4; round += 1) {
|
|
2624
|
+
const a = 押す量(above, "above", out);
|
|
2625
|
+
const b = 押す量(below, "below", out);
|
|
2626
|
+
if (a === 0 && b === 0) return out;
|
|
2627
|
+
for (const i of above) out[i] = out[i]! - a;
|
|
2628
|
+
for (const i of below) out[i] = out[i]! + b;
|
|
2629
|
+
}
|
|
2630
|
+
return 押す量(above, "above", out) === 0 && 押す量(below, "below", out) === 0 ? out : null;
|
|
2631
|
+
};
|
|
2632
|
+
// 押す前と押した後の両方を候補にする = 押すと悪くなる図では押さない置き方が選ばれる。
|
|
2633
|
+
consider(帯を越える());
|
|
2634
|
+
const 押した = 押して越える();
|
|
2635
|
+
if (押した) consider(押した);
|
|
2564
2636
|
}
|
|
2565
2637
|
}
|
|
2566
2638
|
// 間隔だけを満たす置き方も候補に入れる (帯が無い / 帯を避ける置き方が全て捨てられた時)。
|
|
@@ -62,10 +62,12 @@ export function layoutWithValidation(
|
|
|
62
62
|
|
|
63
63
|
if (opts.emitConsole) {
|
|
64
64
|
for (const v of report.violations) {
|
|
65
|
+
// 理由は末尾に足す = `(diagram=...)` までの形を読む側を崩さない (#810)
|
|
66
|
+
const 理由 = v.reason !== undefined ? ` ${v.reason}` : "";
|
|
65
67
|
if (v.severity === "error") {
|
|
66
|
-
console.error(`[cdl geometry ${v.axis}] ${v.detail} (diagram=${v.diagramId})`);
|
|
68
|
+
console.error(`[cdl geometry ${v.axis}] ${v.detail} (diagram=${v.diagramId})${理由}`);
|
|
67
69
|
} else {
|
|
68
|
-
console.warn(`[cdl geometry ${v.axis}] ${v.detail} (diagram=${v.diagramId})`);
|
|
70
|
+
console.warn(`[cdl geometry ${v.axis}] ${v.detail} (diagram=${v.diagramId})${理由}`);
|
|
69
71
|
}
|
|
70
72
|
}
|
|
71
73
|
}
|