@cardenelabs/cdl 0.77.0 → 0.79.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 +55 -1
- package/dist/index.cjs +137 -40
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +137 -40
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +137 -40
- package/dist/react.cjs.map +1 -1
- package/dist/react.js +137 -40
- package/dist/react.js.map +1 -1
- package/package.json +1 -1
- package/src/kinds/shape-blockchain.tsx +13 -3
- package/src/layout/collisions.ts +185 -41
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cardenelabs/cdl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.79.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",
|
|
@@ -306,15 +306,25 @@ const 字の幅 = (text: string, size: number): number => 絵の中の字の幅(
|
|
|
306
306
|
* `data-cdl-role` の目印に使う識別子で、言語で変えると差し込みと目印が壊れる。 描く字は
|
|
307
307
|
* 辞書 (`src/lang.ts`) から引く。 16 進の値と数は言語に依らないのでここに残す。
|
|
308
308
|
*/
|
|
309
|
+
/**
|
|
310
|
+
* 5 行の既定値。
|
|
311
|
+
*
|
|
312
|
+
* **16 進の値の長さは打ち消しの上限から決める** (#885)。 行の値に使える幅は
|
|
313
|
+
* `240 - 28 - 名前の幅 - 8` で、名前も打ち消しで一緒に伸びる。 上限いっぱい (1.6 倍) まで
|
|
314
|
+
* 打ち消すと、いちばん長い名前 (`merkle`) の行に残るのは 118.6 しかない。
|
|
315
|
+
*
|
|
316
|
+
* #871 で打ち消しを入れるまでは 12 字 (`0xc1892f4a...`) が入っていたが、字が 1.6 倍に
|
|
317
|
+
* なった分だけ入る字数が減り、`0xc1892f…` に切れていた。 9 字なら 110.4 で収まる。
|
|
318
|
+
*/
|
|
309
319
|
const BLOCK_DEFAULT_FIELDS: readonly {
|
|
310
320
|
key: string;
|
|
311
321
|
label: CdlTextKey;
|
|
312
322
|
/** 言語に依らない既定値 (16 進の値と数) と、辞書から引く既定値 (単位を持つ値) のどちらか */
|
|
313
323
|
value: { text: string } | { 辞書: CdlTextKey };
|
|
314
324
|
}[] = [
|
|
315
|
-
{ key: "hash", label: "blockchainBlock.hash", value: { text: "
|
|
316
|
-
{ key: "prev", label: "blockchainBlock.prev", value: { text: "
|
|
317
|
-
{ key: "merkle", label: "blockchainBlock.merkle", value: { text: "
|
|
325
|
+
{ key: "hash", label: "blockchainBlock.hash", value: { text: "0xaf31..." } },
|
|
326
|
+
{ key: "prev", label: "blockchainBlock.prev", value: { text: "0x7b3f..." } },
|
|
327
|
+
{ key: "merkle", label: "blockchainBlock.merkle", value: { text: "0xc189..." } },
|
|
318
328
|
{ key: "nonce", label: "blockchainBlock.nonce", value: { text: "82,914" } },
|
|
319
329
|
{ key: "tx", label: "blockchainBlock.tx", value: { 辞書: "blockchainBlock.txCount" } },
|
|
320
330
|
];
|
package/src/layout/collisions.ts
CHANGED
|
@@ -74,41 +74,180 @@ export function collectBBoxes(lanes: LaidLane[], nodes: LaidNode[], edges: LaidE
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
for (const e of edges) {
|
|
77
|
-
const
|
|
78
|
-
if (
|
|
79
|
-
const xs = points.map((p) => p.x);
|
|
80
|
-
const ys = points.map((p) => p.y);
|
|
81
|
-
const minX = Math.min(...xs);
|
|
82
|
-
const maxX = Math.max(...xs);
|
|
83
|
-
const minY = Math.min(...ys);
|
|
84
|
-
const maxY = Math.max(...ys);
|
|
77
|
+
const box = pathBBox(e.d);
|
|
78
|
+
if (!box) continue;
|
|
85
79
|
out.push({
|
|
86
80
|
kind: "edge-path",
|
|
87
81
|
id: e.id,
|
|
88
|
-
x: minX - 2,
|
|
89
|
-
y: minY - 2,
|
|
90
|
-
w: maxX - minX + 4,
|
|
91
|
-
h: maxY - minY + 4,
|
|
82
|
+
x: box.minX - 2,
|
|
83
|
+
y: box.minY - 2,
|
|
84
|
+
w: box.maxX - box.minX + 4,
|
|
85
|
+
h: box.maxY - box.minY + 4,
|
|
92
86
|
});
|
|
93
87
|
}
|
|
94
88
|
|
|
95
89
|
return out;
|
|
96
90
|
}
|
|
97
91
|
|
|
98
|
-
/** SVG path
|
|
92
|
+
/** SVG path の命令が取る引数の数。 命令を区切る唯一の根拠 */
|
|
93
|
+
const PATH_ARITY: Record<string, number> = {
|
|
94
|
+
M: 2, L: 2, H: 1, V: 1, C: 6, S: 4, Q: 4, T: 2, A: 7, Z: 0,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/** 1 つの命令と、その命令だけが取る引数 */
|
|
98
|
+
type PathCommand = { cmd: string; args: number[] };
|
|
99
|
+
|
|
100
|
+
/** 小文字の命令は 1 つ前の点からの差分で書かれている */
|
|
101
|
+
function isRelative(cmd: string): boolean {
|
|
102
|
+
return cmd >= "a" && cmd <= "z";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 命令が持つ座標の位置を `[x の位置, y の位置]` の組で、出る順に返す。
|
|
107
|
+
*
|
|
108
|
+
* `A` は `rx ry 回転 大きい弧か 向き x y` の 7 個で、座標は末尾の 2 個だけ。
|
|
109
|
+
* 前の 5 個は半径と向きの指定なので、平行移動しても値が変わらない。
|
|
110
|
+
* `H` / `V` は片方しか持たないため -1 を置き、読む側が今の点で補う。
|
|
111
|
+
*/
|
|
112
|
+
function coordSlots(cmd: string): Array<[number, number]> {
|
|
113
|
+
switch (cmd.toUpperCase()) {
|
|
114
|
+
case "M":
|
|
115
|
+
case "L":
|
|
116
|
+
case "T":
|
|
117
|
+
return [[0, 1]];
|
|
118
|
+
case "H":
|
|
119
|
+
return [[0, -1]];
|
|
120
|
+
case "V":
|
|
121
|
+
return [[-1, 0]];
|
|
122
|
+
case "Q":
|
|
123
|
+
case "S":
|
|
124
|
+
return [[0, 1], [2, 3]];
|
|
125
|
+
case "C":
|
|
126
|
+
return [[0, 1], [2, 3], [4, 5]];
|
|
127
|
+
case "A":
|
|
128
|
+
return [[5, 6]];
|
|
129
|
+
default:
|
|
130
|
+
return [];
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* `d` を命令 1 つずつに切り出す。
|
|
136
|
+
*
|
|
137
|
+
* **引数の数を命令ごとに数えることが本関数の役目**。 文字を全部消して数を 2 つずつ組にする
|
|
138
|
+
* 読み方は、円弧 (`A`) の引数が 7 個 = 奇数のため組にする位置が 1 つずれ、その後ろの点が
|
|
139
|
+
* 全部ずれる (#872 実測 = 道筋のどこにも無い `(11,11)` と `(0,0)` が点として出て、
|
|
140
|
+
* 外接矩形が図の左上まで伸びた)。
|
|
141
|
+
*
|
|
142
|
+
* 同じ命令を続けて書く省略形 (`L 1 2 3 4` = `L 1 2 L 3 4`) も 1 つずつに開く。
|
|
143
|
+
* `M` の 2 組目以降は `L` になる (SVG の決まり)。
|
|
144
|
+
*
|
|
145
|
+
* 引数が足りない命令は読み飛ばす。 埋めると座標に undefined が入り、以降が全て NaN になって
|
|
146
|
+
* 判定が黙って対象外になる (#370 と同じ判断)。
|
|
147
|
+
*/
|
|
148
|
+
function parsePathCommands(d: string): PathCommand[] {
|
|
149
|
+
const out: PathCommand[] = [];
|
|
150
|
+
const commandRegex = /([MmLlHhVvCcSsQqTtAaZz])([^MmLlHhVvCcSsQqTtAaZz]*)/g;
|
|
151
|
+
let m: RegExpExecArray | null;
|
|
152
|
+
while ((m = commandRegex.exec(d)) !== null) {
|
|
153
|
+
const cmd = m[1]!;
|
|
154
|
+
const arity = PATH_ARITY[cmd.toUpperCase()] ?? 0;
|
|
155
|
+
if (arity === 0) {
|
|
156
|
+
out.push({ cmd, args: [] });
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
const nums = (m[2] ?? "").trim().split(/[\s,]+/).filter(Boolean).map(parseFloat);
|
|
160
|
+
for (let i = 0; i + arity <= nums.length; i += arity) {
|
|
161
|
+
const args = nums.slice(i, i + arity);
|
|
162
|
+
if (!args.every(Number.isFinite)) break;
|
|
163
|
+
const 繰り返し = i > 0;
|
|
164
|
+
const c = 繰り返し && cmd === "M" ? "L" : 繰り返し && cmd === "m" ? "l" : cmd;
|
|
165
|
+
out.push({ cmd: c, args });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return out;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* `d` を命令ごとに読み、座標点を出る順に返す。
|
|
173
|
+
*
|
|
174
|
+
* 返すのは **道筋の上に在る点** で、曲線の制御点も含む (外接矩形を安全側に出すため)。
|
|
175
|
+
* 円弧は終点だけを返す = 途中の膨らみは点として書かれていない。 膨らみを含めた外接矩形が
|
|
176
|
+
* 要るなら `pathBBox` を使う。
|
|
177
|
+
*
|
|
178
|
+
* 小文字の命令 (1 つ前の点からの差分) は絶対座標に直して返す。
|
|
179
|
+
*/
|
|
99
180
|
export function extractPathPoints(d: string): Array<{ x: number; y: number }> {
|
|
100
181
|
const out: Array<{ x: number; y: number }> = [];
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
182
|
+
let cur = { x: 0, y: 0 };
|
|
183
|
+
let started = false;
|
|
184
|
+
for (const { cmd, args } of parsePathCommands(d)) {
|
|
185
|
+
// 先頭の命令は小文字でも絶対座標として読む (SVG の決まり)
|
|
186
|
+
const 相対 = started && isRelative(cmd);
|
|
187
|
+
const 基点 = cur;
|
|
188
|
+
for (const [xi, yi] of coordSlots(cmd)) {
|
|
189
|
+
const x = xi >= 0 ? (相対 ? 基点.x + args[xi]! : args[xi]!) : 基点.x;
|
|
190
|
+
const y = yi >= 0 ? (相対 ? 基点.y + args[yi]! : args[yi]!) : 基点.y;
|
|
106
191
|
out.push({ x, y });
|
|
192
|
+
cur = { x, y };
|
|
107
193
|
}
|
|
194
|
+
started = true;
|
|
108
195
|
}
|
|
109
196
|
return out;
|
|
110
197
|
}
|
|
111
198
|
|
|
199
|
+
/**
|
|
200
|
+
* `d` の外接矩形を返す。 点が 1 つも無ければ `null`。
|
|
201
|
+
*
|
|
202
|
+
* `extractPathPoints` との違いは **円弧の膨らみを足すこと**。 円弧は終点しか点を持たないが、
|
|
203
|
+
* 実際に描かれる線は弦から膨らむ (経路の飛び越しは半径 11 の半円)。 終点だけで矩形を出すと
|
|
204
|
+
* 膨らんだ側が矩形の外に出て、本当の重なりを見落とす側に倒れる。
|
|
205
|
+
*
|
|
206
|
+
* 膨らみの上限は半径と弦の長さで決まる。 半径 `r` の円弧が長さ `L` の弦から離れる距離は、
|
|
207
|
+
* 短い側を通るなら `r - sqrt(r² - (L/2)²)`、長い側 (大きい弧) を通るなら
|
|
208
|
+
* `r + sqrt(r² - (L/2)²)`。 楕円は長い方の半径を使って安全側に倒す。
|
|
209
|
+
*/
|
|
210
|
+
export function pathBBox(
|
|
211
|
+
d: string,
|
|
212
|
+
): { minX: number; minY: number; maxX: number; maxY: number } | null {
|
|
213
|
+
let minX = Infinity;
|
|
214
|
+
let minY = Infinity;
|
|
215
|
+
let maxX = -Infinity;
|
|
216
|
+
let maxY = -Infinity;
|
|
217
|
+
let 点あり = false;
|
|
218
|
+
const 取る = (x: number, y: number): void => {
|
|
219
|
+
minX = Math.min(minX, x);
|
|
220
|
+
minY = Math.min(minY, y);
|
|
221
|
+
maxX = Math.max(maxX, x);
|
|
222
|
+
maxY = Math.max(maxY, y);
|
|
223
|
+
点あり = true;
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
let cur = { x: 0, y: 0 };
|
|
227
|
+
let started = false;
|
|
228
|
+
for (const { cmd, args } of parsePathCommands(d)) {
|
|
229
|
+
const 相対 = started && isRelative(cmd);
|
|
230
|
+
const 基点 = cur;
|
|
231
|
+
for (const [xi, yi] of coordSlots(cmd)) {
|
|
232
|
+
const x = xi >= 0 ? (相対 ? 基点.x + args[xi]! : args[xi]!) : 基点.x;
|
|
233
|
+
const y = yi >= 0 ? (相対 ? 基点.y + args[yi]! : args[yi]!) : 基点.y;
|
|
234
|
+
取る(x, y);
|
|
235
|
+
cur = { x, y };
|
|
236
|
+
}
|
|
237
|
+
if (cmd.toUpperCase() === "A") {
|
|
238
|
+
const r = Math.max(Math.abs(args[0]!), Math.abs(args[1]!));
|
|
239
|
+
const 弦 = Math.hypot(cur.x - 基点.x, cur.y - 基点.y);
|
|
240
|
+
const 半弦 = Math.min(弦 / 2, r);
|
|
241
|
+
const 余 = Math.sqrt(Math.max(0, r * r - 半弦 * 半弦));
|
|
242
|
+
const 膨らみ = args[3] !== 0 ? r + 余 : r - 余;
|
|
243
|
+
取る(Math.min(基点.x, cur.x) - 膨らみ, Math.min(基点.y, cur.y) - 膨らみ);
|
|
244
|
+
取る(Math.max(基点.x, cur.x) + 膨らみ, Math.max(基点.y, cur.y) + 膨らみ);
|
|
245
|
+
}
|
|
246
|
+
started = true;
|
|
247
|
+
}
|
|
248
|
+
return 点あり ? { minX, minY, maxX, maxY } : null;
|
|
249
|
+
}
|
|
250
|
+
|
|
112
251
|
/**
|
|
113
252
|
* SVG path d 文字列を (dx, dy) 平行移動する (CAR-485 導入)。
|
|
114
253
|
*
|
|
@@ -117,9 +256,9 @@ export function extractPathPoints(d: string): Array<{ x: number; y: number }> {
|
|
|
117
256
|
* (dx, dy) を加算する必要がある。 edges.ts の routePath() は M / L / Q コマンドで
|
|
118
257
|
* 全 coord を absolute 表記するため、 command tokenizer で数値 pair を舐めて加算する。
|
|
119
258
|
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
259
|
+
* 命令ごとの引数の数は `parsePathCommands` が持つ。 円弧 (`A`) は末尾 2 個だけが座標で、
|
|
260
|
+
* 半径と向きの指定は動かさない。 小文字の命令は 1 つ前の点からの差分なので、平行移動しても
|
|
261
|
+
* 値が変わらない = そのまま通す (先頭の絶対座標が動けば道筋全体が動く)。
|
|
123
262
|
*
|
|
124
263
|
* dx = 0 かつ dy = 0 は no-op (元 d を返す)。
|
|
125
264
|
*/
|
|
@@ -134,25 +273,35 @@ export function translatePathD(d: string, dx: number, dy: number): string {
|
|
|
134
273
|
* `idx` は path 先頭からの座標 pair の通し番号 (Q の control 点も 1 pair として数える)。
|
|
135
274
|
*/
|
|
136
275
|
function rebuildPathD(d: string, map: (x: number, y: number, idx: number) => [number, number]): string {
|
|
137
|
-
const commandRegex = /([MLCQZmlcqz])([^MLCQZmlcqz]*)/g;
|
|
138
276
|
let out = "";
|
|
139
277
|
let idx = 0;
|
|
140
|
-
let
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const
|
|
144
|
-
if (
|
|
278
|
+
let cur = { x: 0, y: 0 };
|
|
279
|
+
let started = false;
|
|
280
|
+
for (const { cmd, args } of parsePathCommands(d)) {
|
|
281
|
+
const slots = coordSlots(cmd);
|
|
282
|
+
if (slots.length === 0) {
|
|
145
283
|
out += cmd + " ";
|
|
284
|
+
started = true;
|
|
146
285
|
continue;
|
|
147
286
|
}
|
|
148
|
-
const
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
287
|
+
const 相対 = started && isRelative(cmd);
|
|
288
|
+
const 基点 = cur;
|
|
289
|
+
const 次の引数 = args.slice();
|
|
290
|
+
for (const [xi, yi] of slots) {
|
|
291
|
+
const x = xi >= 0 ? (相対 ? 基点.x + args[xi]! : args[xi]!) : 基点.x;
|
|
292
|
+
const y = yi >= 0 ? (相対 ? 基点.y + args[yi]! : args[yi]!) : 基点.y;
|
|
293
|
+
const [nx, ny] = map(x, y, idx);
|
|
294
|
+
if (!相対) {
|
|
295
|
+
if (xi >= 0) 次の引数[xi] = nx;
|
|
296
|
+
if (yi >= 0) 次の引数[yi] = ny;
|
|
297
|
+
}
|
|
298
|
+
// 位置の通し番号は `extractPathPoints` が返す点と 1 対 1 で揃える。
|
|
299
|
+
// 揃っていないと `translatePathDPreservingEndpoints` が別の点を固定する。
|
|
153
300
|
idx += 1;
|
|
301
|
+
cur = { x, y };
|
|
154
302
|
}
|
|
155
|
-
out += cmd + " " +
|
|
303
|
+
out += cmd + " " + 次の引数.join(" ") + " ";
|
|
304
|
+
started = true;
|
|
156
305
|
}
|
|
157
306
|
return out.trim();
|
|
158
307
|
}
|
|
@@ -163,17 +312,12 @@ function rebuildPathD(d: string, map: (x: number, y: number, idx: number) => [nu
|
|
|
163
312
|
* Q は `control` と `end` の 2 pair を持つ。 corner が無ければ両方 -1。
|
|
164
313
|
*/
|
|
165
314
|
function cornerBoundaryIndices(d: string): { firstCornerEnd: number; lastCornerStart: number } {
|
|
166
|
-
const commandRegex = /([MLCQZmlcqz])([^MLCQZmlcqz]*)/g;
|
|
167
315
|
let idx = 0;
|
|
168
316
|
let firstCornerEnd = -1;
|
|
169
317
|
let lastCornerStart = -1;
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const argsStr = (m[2] ?? "").trim();
|
|
174
|
-
if (argsStr === "" || cmd === "Z" || cmd === "z") continue;
|
|
175
|
-
const pairs = argsStr.split(/[\s,]+/).filter(Boolean).length >> 1;
|
|
176
|
-
if ((cmd === "Q" || cmd === "q") && pairs >= 2) {
|
|
318
|
+
for (const { cmd } of parsePathCommands(d)) {
|
|
319
|
+
const pairs = coordSlots(cmd).length;
|
|
320
|
+
if (cmd.toUpperCase() === "Q" && pairs >= 2) {
|
|
177
321
|
if (firstCornerEnd < 0) firstCornerEnd = idx + 1;
|
|
178
322
|
lastCornerStart = idx;
|
|
179
323
|
}
|