@cardenelabs/cdl 0.48.0 → 0.50.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 +40 -1
- package/dist/index.cjs +195 -99
- 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 +196 -100
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +184 -95
- package/dist/react.cjs.map +1 -1
- package/dist/react.js +185 -96
- package/dist/react.js.map +1 -1
- package/package.json +1 -1
- package/src/kinds/actor.tsx +2 -1
- package/src/kinds/event.tsx +3 -2
- package/src/kinds/function.tsx +2 -1
- package/src/kinds/generic.tsx +5 -4
- package/src/kinds/sequence-board.tsx +2 -1
- package/src/kinds/storage.tsx +4 -3
- package/src/layout/collisions.ts +293 -139
- package/src/layout-with-validation.ts +4 -2
- package/src/render/edges.tsx +2 -1
- package/src/render/mono-text.ts +44 -0
- 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.50.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/kinds/actor.tsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { 等幅の字 } from "../render/mono-text";
|
|
1
2
|
import type { JSX } from "react";
|
|
2
3
|
import type { LaidNode } from "../types";
|
|
3
4
|
import { nodeAccent } from "./node-tone";
|
|
@@ -84,7 +85,7 @@ export function ActorNode({ node, active, value }: { node: LaidNode; active: boo
|
|
|
84
85
|
fill="var(--cdl-text, #1a1f2a)"
|
|
85
86
|
style={{ fontFamily: "'JetBrains Mono', monospace" }}
|
|
86
87
|
>
|
|
87
|
-
{value}
|
|
88
|
+
{等幅の字(value, 42)}
|
|
88
89
|
</text>
|
|
89
90
|
)}
|
|
90
91
|
</g>
|
package/src/kinds/event.tsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { 等幅の字 } from "../render/mono-text";
|
|
1
2
|
import type { JSX } from "react";
|
|
2
3
|
import type { LaidNode } from "../types";
|
|
3
4
|
import { nodeAccent } from "./node-tone";
|
|
@@ -63,7 +64,7 @@ export function EventNode({
|
|
|
63
64
|
textAnchor={名前.anchor}
|
|
64
65
|
style={{ fontFamily: "'JetBrains Mono', monospace" }}
|
|
65
66
|
>
|
|
66
|
-
{node.title}
|
|
67
|
+
{等幅の字(node.title, 名前.size)}
|
|
67
68
|
</text>
|
|
68
69
|
{node.subtitle && (
|
|
69
70
|
<text
|
|
@@ -73,7 +74,7 @@ export function EventNode({
|
|
|
73
74
|
fill="var(--cdl-text-dim, #5a6270)"
|
|
74
75
|
style={{ fontFamily: "'JetBrains Mono', monospace" }}
|
|
75
76
|
>
|
|
76
|
-
{node.subtitle}
|
|
77
|
+
{等幅の字(node.subtitle, 21)}
|
|
77
78
|
</text>
|
|
78
79
|
)}
|
|
79
80
|
</g>
|
package/src/kinds/function.tsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { 等幅の字 } from "../render/mono-text";
|
|
1
2
|
import type { JSX } from "react";
|
|
2
3
|
import type { LaidNode } from "../types";
|
|
3
4
|
import { nodeAccent } from "./node-tone";
|
|
@@ -53,7 +54,7 @@ export function FunctionNode({ node, active }: { node: LaidNode; active: boolean
|
|
|
53
54
|
textAnchor={名前.anchor}
|
|
54
55
|
style={{ fontFamily: "'JetBrains Mono', monospace" }}
|
|
55
56
|
>
|
|
56
|
-
{node.title}
|
|
57
|
+
{等幅の字(node.title, 名前.size)}
|
|
57
58
|
</text>
|
|
58
59
|
{node.subtitle && (
|
|
59
60
|
<text x={26} y={126 - ずらし} fontSize={20} fill="var(--cdl-text-dim, #5a6270)">
|
package/src/kinds/generic.tsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { 等幅の字 } from "../render/mono-text";
|
|
1
2
|
import type { JSX } from "react";
|
|
2
3
|
import type { LaidNode, NodeKind } from "../types";
|
|
3
4
|
import { toneColor } from "./node-tone";
|
|
@@ -423,11 +424,11 @@ export function GenericNode({
|
|
|
423
424
|
</text>
|
|
424
425
|
)}
|
|
425
426
|
<text data-cdl-role="node-label" x={textX} y={titleY} fontSize={20} fontWeight={700} fill={paint.onSolid ? "var(--cdl-node-fill, #ffffff)" : "var(--cdl-text, #1a1f2a)"} textAnchor={textAnchor} dominantBaseline={dominantBaseline} style={node.kind === "function" || node.kind === "api" ? { fontFamily: "'JetBrains Mono', monospace" } : undefined}>
|
|
426
|
-
{node.title}
|
|
427
|
+
{node.kind === "function" || node.kind === "api" ? 等幅の字(node.title, 20) : node.title}
|
|
427
428
|
</text>
|
|
428
429
|
{node.subtitle && (
|
|
429
430
|
<text x={textX} y={subtitleY} fontSize={17} fill={paint.onSolid ? "var(--cdl-node-fill, #ffffff)" : "var(--cdl-text-dim, #5a6270)"} textAnchor={textAnchor} dominantBaseline={dominantBaseline} style={{ fontFamily: "'JetBrains Mono', monospace" }}>
|
|
430
|
-
{node.subtitle}
|
|
431
|
+
{等幅の字(node.subtitle, 17)}
|
|
431
432
|
</text>
|
|
432
433
|
)}
|
|
433
434
|
{resolvedValue && (
|
|
@@ -440,7 +441,7 @@ export function GenericNode({
|
|
|
440
441
|
fill="var(--cdl-text, #1a1f2a)"
|
|
441
442
|
style={{ fontFamily: "'JetBrains Mono', monospace" }}
|
|
442
443
|
>
|
|
443
|
-
{resolvedValue}
|
|
444
|
+
{等幅の字(resolvedValue, 32)}
|
|
444
445
|
</text>
|
|
445
446
|
)}
|
|
446
447
|
{node.rows && node.rows.length > 0 && (() => {
|
|
@@ -473,7 +474,7 @@ export function GenericNode({
|
|
|
473
474
|
fill="var(--cdl-text, #1a1f2a)"
|
|
474
475
|
style={{ fontFamily: "'JetBrains Mono', monospace" }}
|
|
475
476
|
>
|
|
476
|
-
{s.right}
|
|
477
|
+
{等幅の字(s.right, 21)}
|
|
477
478
|
</text>
|
|
478
479
|
</g>
|
|
479
480
|
))}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { 等幅の字 } from "../render/mono-text";
|
|
1
2
|
import type { JSX } from "react";
|
|
2
3
|
import type { LaidNode } from "../types";
|
|
3
4
|
import { interpolate } from "../render/utils";
|
|
@@ -144,7 +145,7 @@ export function SequenceBoardNode({
|
|
|
144
145
|
fill="var(--cdl-text, #1a1f2a)"
|
|
145
146
|
style={{ fontFamily: "'JetBrains Mono', monospace" }}
|
|
146
147
|
>
|
|
147
|
-
{a.name}
|
|
148
|
+
{等幅の字(a.name, SEQ_NAME)}
|
|
148
149
|
</text>
|
|
149
150
|
{a.subtitle && (
|
|
150
151
|
<text
|
package/src/kinds/storage.tsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { 等幅の字 } from "../render/mono-text";
|
|
1
2
|
import type { JSX } from "react";
|
|
2
3
|
import type { LaidNode, RowMark } from "../types";
|
|
3
4
|
import { nodeAccent } from "./node-tone";
|
|
@@ -233,7 +234,7 @@ export function StorageNode({
|
|
|
233
234
|
textAnchor={名前.anchor}
|
|
234
235
|
style={{ fontFamily: "'JetBrains Mono', monospace" }}
|
|
235
236
|
>
|
|
236
|
-
{node.title}
|
|
237
|
+
{等幅の字(node.title, 名前.size)}
|
|
237
238
|
</text>
|
|
238
239
|
{/*
|
|
239
240
|
表の区切り線 2 本 (`#419`)。
|
|
@@ -323,7 +324,7 @@ export function StorageNode({
|
|
|
323
324
|
fill="var(--cdl-text, #1a1f2a)"
|
|
324
325
|
style={{ fontFamily: "'JetBrains Mono', monospace" }}
|
|
325
326
|
>
|
|
326
|
-
{s.left}
|
|
327
|
+
{等幅の字(s.left, 26)}
|
|
327
328
|
</text>
|
|
328
329
|
{/*
|
|
329
330
|
型は **読み手が探すものではない**ので、右端で揃えて薄く引く。 名前の右に続けて
|
|
@@ -347,7 +348,7 @@ export function StorageNode({
|
|
|
347
348
|
}
|
|
348
349
|
style={{ fontFamily: "'JetBrains Mono', monospace" }}
|
|
349
350
|
>
|
|
350
|
-
{s.right}
|
|
351
|
+
{等幅の字(s.right, 印で分ける ? ROW_TYPE_FONT : 26)}
|
|
351
352
|
</text>
|
|
352
353
|
</g>
|
|
353
354
|
);
|
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 }> = [];
|
|
@@ -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
|
}
|
package/src/render/edges.tsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { 等幅の字 } from "./mono-text";
|
|
1
2
|
import type { JSX } from "react";
|
|
2
3
|
import { computeLabelBBoxWorld, hasRenderedLabel, LABEL_PILL_PAD_X } from "../layout/spec";
|
|
3
4
|
import type { EdgeRole, LaidEdge, Tone } from "../types";
|
|
@@ -387,7 +388,7 @@ export function CdlEdgeLabelView({
|
|
|
387
388
|
opacity={labelTextOpacity("sub")}
|
|
388
389
|
style={{ fontFamily: fontFamilyAttr("sub") }}
|
|
389
390
|
>
|
|
390
|
-
{edge.sub}
|
|
391
|
+
{等幅の字(edge.sub, EDGE_LABEL_TEXT.sub.fontSize)}
|
|
391
392
|
</text>
|
|
392
393
|
)}
|
|
393
394
|
</g>
|