@cardenelabs/dragon 0.12.0 → 0.14.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/README.md +86 -58
- package/dist/index.cjs +10485 -6260
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +945 -4
- package/dist/index.d.ts +945 -4
- package/dist/index.js +10486 -6261
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/color.ts +31 -6
- package/src/compile.ts +724 -196
- package/src/input-size.ts +21 -1
- package/src/json-parser.ts +695 -2
- package/src/schemas/diagram.json +1341 -3
- package/src/types.ts +130 -1
- package/src/v05/input-table.generated.ts +136 -0
- package/src/v05/parser-types.ts +21 -0
- package/src/v05/parser.ts +1237 -26
- package/src/v05/readout-table.generated.ts +1088 -0
package/src/compile.ts
CHANGED
|
@@ -9,16 +9,44 @@
|
|
|
9
9
|
* v0.2 ... 6 preset 全対応 (sequence / flow / swimlane / er / state / topology)
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import type {
|
|
13
|
-
|
|
12
|
+
import type {
|
|
13
|
+
DslActor,
|
|
14
|
+
DslDocument,
|
|
15
|
+
DslLane,
|
|
16
|
+
DslPhase,
|
|
17
|
+
DslStep,
|
|
18
|
+
DslEventBinding,
|
|
19
|
+
DslValue,
|
|
20
|
+
PresetType,
|
|
21
|
+
} from "./types";
|
|
22
|
+
import type {
|
|
23
|
+
CdlDiagram,
|
|
24
|
+
CdlEdge,
|
|
25
|
+
ErRelationCardinality,
|
|
26
|
+
FormulaAst,
|
|
27
|
+
LaidDiagram,
|
|
28
|
+
} from "@cardenelabs/cdl";
|
|
14
29
|
import {
|
|
15
|
-
sequence,
|
|
16
|
-
|
|
17
|
-
|
|
30
|
+
sequence,
|
|
31
|
+
flow,
|
|
32
|
+
swimlane,
|
|
33
|
+
er,
|
|
34
|
+
stateMachine,
|
|
35
|
+
topology,
|
|
36
|
+
diagram,
|
|
37
|
+
layout,
|
|
38
|
+
rendersRows,
|
|
39
|
+
requiredRowsHeight,
|
|
40
|
+
requiredRowsWidth,
|
|
41
|
+
NODE_KINDS,
|
|
42
|
+
applyDerivedValues,
|
|
43
|
+
parseFormula,
|
|
44
|
+
extractIdentifiers,
|
|
45
|
+
inputDefaultValue,
|
|
18
46
|
} from "@cardenelabs/cdl";
|
|
19
47
|
import { parseFocusEntry } from "./focus";
|
|
20
48
|
import { DRAW_TARGETS } from "./v05/parser";
|
|
21
|
-
import { isColorValue, stripExternalPaint } from "./color";
|
|
49
|
+
import { isColorValue, pointsOutside, stripExternalPaint } from "./color";
|
|
22
50
|
import {
|
|
23
51
|
MAX_INPUT_ELEMENTS,
|
|
24
52
|
countDiagramElements,
|
|
@@ -96,7 +124,11 @@ export type CompileNotice = {
|
|
|
96
124
|
// 起点から描く動きを持たない図種で段に `draw:` を書いた (#1312)
|
|
97
125
|
| "draw-not-honored"
|
|
98
126
|
// `draw:` の語がその図種と食い違う (`type: bar` に `draw: pie`、 #1314)
|
|
99
|
-
| "draw-target-mismatch"
|
|
127
|
+
| "draw-target-mismatch"
|
|
128
|
+
// 式が、どこにも書かれていない名前を読んだ (#1391)
|
|
129
|
+
| "formula-unresolved"
|
|
130
|
+
// 出来事が指す相手が図に無い (#1393)
|
|
131
|
+
| "event-target-missing";
|
|
100
132
|
/** 対象の名前。 光らせる相手なら書かれた指定そのまま */
|
|
101
133
|
actor: string;
|
|
102
134
|
/** 書かれていた行 */
|
|
@@ -279,12 +311,150 @@ export function compileToCdl(doc: DslDocument, opts?: CompileToCdlOpts): CdlDiag
|
|
|
279
311
|
// 扱いが式形と揃う。 時計の状態と段の補間もここで足す
|
|
280
312
|
const 畳んだきっかけ = foldValueTriggers(merged, doc, opts?.onNotice);
|
|
281
313
|
attachDerivedValues(merged, doc, opts?.onNotice, inheritedDerivedSourceLines, 畳んだきっかけ);
|
|
314
|
+
// 値を見せる部品を図に載せる (#1374)。 parts が持つ部品は merge 済みなので残したまま足す。
|
|
315
|
+
// **外部参照を落とす前に載せる**。 後から足すと readout の color / colors だけが出口の検査を
|
|
316
|
+
// 迂回し、 `url(https://...)` がそのまま SVG の paint 属性へ届く。
|
|
317
|
+
if (doc.readouts && doc.readouts.length > 0) {
|
|
318
|
+
// 出口の paint 検査は diagram を直接書き換える。 doc の object を共有すると、検査が
|
|
319
|
+
// compileToCdl の入力まで書き換えて入力不変性を壊すため、nested field も含めて写す。
|
|
320
|
+
const ownReadouts = doc.readouts.map(
|
|
321
|
+
(readout) => deepRewriteStrings(readout, (value) => value) as typeof readout,
|
|
322
|
+
);
|
|
323
|
+
merged.readouts = [...(merged.readouts ?? []), ...ownReadouts];
|
|
324
|
+
}
|
|
325
|
+
/*
|
|
326
|
+
* 読む人が動かすつまみを図に載せる (#1389)。
|
|
327
|
+
*
|
|
328
|
+
* 部品と同じく写して載せる = 出口の paint 検査が diagram を直接書き換えるため、doc の
|
|
329
|
+
* object を共有すると `compileToCdl` の入力まで書き換わって入力不変性が壊れる。
|
|
330
|
+
*/
|
|
331
|
+
if (doc.inputs && doc.inputs.length > 0) {
|
|
332
|
+
const ownInputs = doc.inputs.map(
|
|
333
|
+
(input) => deepRewriteStrings(input, (value) => value) as typeof input,
|
|
334
|
+
);
|
|
335
|
+
merged.inputs = [...(merged.inputs ?? []), ...ownInputs];
|
|
336
|
+
}
|
|
337
|
+
/*
|
|
338
|
+
* つまみの値から決まる値を図に載せる (#1391)。
|
|
339
|
+
*
|
|
340
|
+
* 部品やつまみと同じく写して載せる。 式の文字列は色を塗る位置に届かないが、
|
|
341
|
+
* 入力を書き換えない形を 3 経路で揃える方が読み手に説明しやすい。
|
|
342
|
+
*/
|
|
343
|
+
if (doc.formulas && doc.formulas.length > 0) {
|
|
344
|
+
const ownFormulas = doc.formulas.map((formula) => ({
|
|
345
|
+
id: formula.id,
|
|
346
|
+
expression: formula.expression,
|
|
347
|
+
line: formula.pos?.line ?? 0,
|
|
348
|
+
}));
|
|
349
|
+
/*
|
|
350
|
+
* 式が読む名前が、どこにも書かれていないことを知らせる (#1391)。
|
|
351
|
+
*
|
|
352
|
+
* engine が読める名前は、つまみと **先に宣言した式** だけ。綴り違い、状態、
|
|
353
|
+
* `values:`、後から宣言する式を渡すと、描画時に未定義参照として例外になる。
|
|
354
|
+
*
|
|
355
|
+
* **解けない式は図へ載せない**。 engine は未定義参照や input/formula の同名衝突を
|
|
356
|
+
* runtime error にするため、残すと図全体が描けない。式以外の図は出し、知らせを返す。
|
|
357
|
+
*/
|
|
358
|
+
const つまみ = new Map((merged.inputs ?? []).map((input) => [input.id, input]));
|
|
359
|
+
const 先に書かれた式 = new Set((merged.formulas ?? []).map((formula) => formula.id));
|
|
360
|
+
const 載せる式 = [...(merged.formulas ?? [])];
|
|
361
|
+
for (const formula of ownFormulas) {
|
|
362
|
+
if (つまみ.has(formula.id) || 先に書かれた式.has(formula.id)) {
|
|
363
|
+
opts?.onNotice?.({
|
|
364
|
+
kind: "formula-unresolved",
|
|
365
|
+
actor: formula.id,
|
|
366
|
+
line: formula.line,
|
|
367
|
+
message: `式 "${formula.id}" の名前が、先に書かれたつまみまたは式と重なっています`,
|
|
368
|
+
hint: "`inputs:` と `formulas:` では重ならない名前を使う",
|
|
369
|
+
});
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
let 名前たち: Set<string>;
|
|
373
|
+
try {
|
|
374
|
+
名前たち = extractIdentifiers(parseFormula(formula.expression));
|
|
375
|
+
} catch {
|
|
376
|
+
// 読めない式は記法の読み取りが既に知らせている。 ここで二重に出さない
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
let 解けない = false;
|
|
380
|
+
for (const 名 of 名前たち) {
|
|
381
|
+
if (先に書かれた式.has(名)) continue;
|
|
382
|
+
const input = つまみ.get(名);
|
|
383
|
+
if (input) {
|
|
384
|
+
const 初期値 = inputDefaultValue(input);
|
|
385
|
+
if (typeof 初期値 === "number" || typeof 初期値 === "boolean") continue;
|
|
386
|
+
解けない = true;
|
|
387
|
+
opts?.onNotice?.({
|
|
388
|
+
kind: "formula-unresolved",
|
|
389
|
+
actor: formula.id,
|
|
390
|
+
line: formula.line,
|
|
391
|
+
message: `式 "${formula.id}" が、数でも真偽でもないつまみ "${名}" を読んでいます`,
|
|
392
|
+
hint: "式が読めるつまみは slider / number / stepper / timeline / toggle",
|
|
393
|
+
});
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
解けない = true;
|
|
397
|
+
opts?.onNotice?.({
|
|
398
|
+
kind: "formula-unresolved",
|
|
399
|
+
actor: formula.id,
|
|
400
|
+
line: formula.line,
|
|
401
|
+
message: `式 "${formula.id}" が、つまみまたは先に書かれた式ではない名前 "${名}" を読んでいます`,
|
|
402
|
+
hint: "`inputs:` に書くか、参照される式をこの式より前に書く",
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
if (解けない) continue;
|
|
406
|
+
載せる式.push({ id: formula.id, expression: formula.expression });
|
|
407
|
+
先に書かれた式.add(formula.id);
|
|
408
|
+
}
|
|
409
|
+
if (載せる式.length > 0) merged.formulas = 載せる式;
|
|
410
|
+
else delete merged.formulas;
|
|
411
|
+
}
|
|
412
|
+
/*
|
|
413
|
+
* 押下などの出来事で動く仕掛けを図に載せる (#1393)。
|
|
414
|
+
*
|
|
415
|
+
* **相手の名前を識別子へ直す**。 記法は識別子を書けないため名前で指す。 指す先が
|
|
416
|
+
* 見つからない形は載せずに知らせる = 描画側は知らない識別子を黙って無視するため、
|
|
417
|
+
* 残すと「書いたのに押しても何も起きない」 が手掛かりなしで起きる。
|
|
418
|
+
*
|
|
419
|
+
* 識別子は書いた順に `evt-1` から振る (組み立て API と同じ)。
|
|
420
|
+
*/
|
|
421
|
+
if (doc.events && doc.events.length > 0) {
|
|
422
|
+
const 載せる: NonNullable<CdlDiagram["eventBindings"]> = [...(merged.eventBindings ?? [])];
|
|
423
|
+
for (const e of doc.events) {
|
|
424
|
+
const 相手 = 出来事の相手を解く(merged, doc, e);
|
|
425
|
+
if (相手 === undefined) {
|
|
426
|
+
opts?.onNotice?.({
|
|
427
|
+
kind: "event-target-missing",
|
|
428
|
+
actor: e.handlerId,
|
|
429
|
+
line: e.pos.line,
|
|
430
|
+
message: `出来事 "${e.handlerId}" が指す相手が見つかりません`,
|
|
431
|
+
hint: "box は箱の名前、 lane は縦列の名前、 arrow は `A -> B` で書く",
|
|
432
|
+
});
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
載せる.push({
|
|
436
|
+
id: `evt-${載せる.length + 1}`,
|
|
437
|
+
event: e.event,
|
|
438
|
+
target: 相手,
|
|
439
|
+
handlerId: e.handlerId,
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
if (載せる.length > 0) merged.eventBindings = 載せる;
|
|
443
|
+
}
|
|
444
|
+
// 巻き上げに応じて進む値を図に載せる (#1393)。 相手を持たないのでそのまま写す
|
|
445
|
+
if (doc.scrolls && doc.scrolls.length > 0) {
|
|
446
|
+
merged.scrollTriggers = [
|
|
447
|
+
...(merged.scrollTriggers ?? []),
|
|
448
|
+
...doc.scrolls.map((x) => ({ ...x })),
|
|
449
|
+
];
|
|
450
|
+
}
|
|
282
451
|
// 図の外を指す値を、 色を塗る位置から落とす (#1004)。
|
|
283
452
|
//
|
|
284
453
|
// 入口ごとに塞ぐ形は採らない。 状態の上書き / phase が入れる値 / 画面が直接書く背景色 /
|
|
285
454
|
// 埋め込んだ JSON / states / values と入口が複数あり、 1 つ見落とすと穴が残る。
|
|
286
455
|
// **図への追加を全て終えた後**、 出口で 1 度だけ見る。 この後に状態を足すと検査を迂回する。
|
|
287
|
-
|
|
456
|
+
const 外した部品の配色 = stripExternalReadoutPalettes(merged);
|
|
457
|
+
for (const dropped of [...stripExternalPaint(merged), ...外した部品の配色]) {
|
|
288
458
|
opts?.onNotice?.({
|
|
289
459
|
kind: "external-paint-dropped",
|
|
290
460
|
actor: dropped.path,
|
|
@@ -299,6 +469,29 @@ export function compileToCdl(doc: DslDocument, opts?: CompileToCdlOpts): CdlDiag
|
|
|
299
469
|
return merged;
|
|
300
470
|
}
|
|
301
471
|
|
|
472
|
+
/**
|
|
473
|
+
* readout の配色配列から外部参照を落とす (#1374)。
|
|
474
|
+
*
|
|
475
|
+
* 共通の `stripExternalPaint` は `color` / `fill` のような key を見るが、 `colors` の中へ
|
|
476
|
+
* 入ると配列要素には key が無い。 `heat-cell` が公開した配色だけはここで要素ごとに閉じる。
|
|
477
|
+
*/
|
|
478
|
+
function stripExternalReadoutPalettes(diagram: CdlDiagram): Array<{ path: string; value: string }> {
|
|
479
|
+
const stripped: Array<{ path: string; value: string }> = [];
|
|
480
|
+
for (let i = 0; i < (diagram.readouts?.length ?? 0); i += 1) {
|
|
481
|
+
const readout = diagram.readouts?.[i] as { colors?: readonly string[] } | undefined;
|
|
482
|
+
if (!readout?.colors) continue;
|
|
483
|
+
const colors = [...readout.colors];
|
|
484
|
+
for (let j = 0; j < colors.length; j += 1) {
|
|
485
|
+
const color = colors[j];
|
|
486
|
+
if (color === undefined || !pointsOutside(color)) continue;
|
|
487
|
+
colors[j] = "none";
|
|
488
|
+
stripped.push({ path: `readouts[${i}].colors[${j}]`, value: color });
|
|
489
|
+
}
|
|
490
|
+
readout.colors = colors;
|
|
491
|
+
}
|
|
492
|
+
return stripped;
|
|
493
|
+
}
|
|
494
|
+
|
|
302
495
|
/**
|
|
303
496
|
* 書いた状態 (`states:`) を図に載せる (#1162)。
|
|
304
497
|
*
|
|
@@ -907,7 +1100,9 @@ function disambiguateActorIds(
|
|
|
907
1100
|
}
|
|
908
1101
|
const 新しい名前 = new Map<string, string>();
|
|
909
1102
|
// 名前で並べてから配る = 書いた順に依らない
|
|
910
|
-
for (const a of [...作り替える].sort((x, y) =>
|
|
1103
|
+
for (const a of [...作り替える].sort((x, y) =>
|
|
1104
|
+
x.name < y.name ? -1 : x.name > y.name ? 1 : 0,
|
|
1105
|
+
)) {
|
|
911
1106
|
const 尾 = 名前の尾(a.name);
|
|
912
1107
|
// **id の長さの上限のぶん、 元の名前を先に切る** (Round 1 の指摘)。 切らないと尾が
|
|
913
1108
|
// 64 字で落ちて、 同じ頭を持つ長い名前どうしが元のまま重なる
|
|
@@ -935,6 +1130,26 @@ function disambiguateActorIds(
|
|
|
935
1130
|
const to = 直す(s.to);
|
|
936
1131
|
return from === s.from && to === s.to ? s : { ...s, from, to };
|
|
937
1132
|
}),
|
|
1133
|
+
events: doc.events?.map((event) => {
|
|
1134
|
+
const target = event.target;
|
|
1135
|
+
if (target.kind === "node") {
|
|
1136
|
+
const name = 直す(target.name);
|
|
1137
|
+
return name === target.name ? event : { ...event, target: { ...target, name } };
|
|
1138
|
+
}
|
|
1139
|
+
if (target.kind === "edge") {
|
|
1140
|
+
const from = 直す(target.from);
|
|
1141
|
+
const to = 直す(target.to);
|
|
1142
|
+
return from === target.from && to === target.to
|
|
1143
|
+
? event
|
|
1144
|
+
: { ...event, target: { ...target, from, to } };
|
|
1145
|
+
}
|
|
1146
|
+
// sequence 系の縦列は登場人物から作るため、同じ名前の読み替えが必要。
|
|
1147
|
+
if (target.kind === "lane" && (doc.type === "sequence" || doc.type === "solidity")) {
|
|
1148
|
+
const name = 直す(target.name);
|
|
1149
|
+
return name === target.name ? event : { ...event, target: { ...target, name } };
|
|
1150
|
+
}
|
|
1151
|
+
return event;
|
|
1152
|
+
}),
|
|
938
1153
|
};
|
|
939
1154
|
// 光らせる指定と位置の基準も名前で書くので、 同じ表で直す
|
|
940
1155
|
if (次.animate) {
|
|
@@ -1453,10 +1668,7 @@ function applyCanvasPivotPositions(diagram: CdlDiagram, doc: DslDocument): void
|
|
|
1453
1668
|
for (const [subKey, override] of Object.entries(actor.nodes)) {
|
|
1454
1669
|
if (override.posX === undefined || override.posY === undefined) continue;
|
|
1455
1670
|
for (const node of diagram.nodes) {
|
|
1456
|
-
if (
|
|
1457
|
-
node.id === `${aliasSlug}-${subKey}` ||
|
|
1458
|
-
node.id === `${subKey}-${aliasSlug}`
|
|
1459
|
-
) {
|
|
1671
|
+
if (node.id === `${aliasSlug}-${subKey}` || node.id === `${subKey}-${aliasSlug}`) {
|
|
1460
1672
|
node.posX = override.posX;
|
|
1461
1673
|
node.posY = override.posY;
|
|
1462
1674
|
if (override.posW !== undefined) node.posW = override.posW;
|
|
@@ -1684,8 +1896,6 @@ export function partTargetScale(
|
|
|
1684
1896
|
return partScaleFactor(part, targetW, targetH, undefined);
|
|
1685
1897
|
}
|
|
1686
1898
|
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
1899
|
/**
|
|
1690
1900
|
* パーツ 1 個が図の上で占める外接矩形。
|
|
1691
1901
|
*
|
|
@@ -2009,8 +2219,7 @@ export function partsGridCenters(
|
|
|
2009
2219
|
if (items.length === 0) return out;
|
|
2010
2220
|
// 公開している関数なので、 呼出側が渡す値を入口で閉じる。 数でない箱の数や桁溢れを
|
|
2011
2221
|
// そのまま計算に入れると、 描けない座標を返すことになる
|
|
2012
|
-
const safeCount =
|
|
2013
|
-
Number.isSafeInteger(baseNodeCount) && baseNodeCount >= 0 ? baseNodeCount : 0;
|
|
2222
|
+
const safeCount = Number.isSafeInteger(baseNodeCount) && baseNodeCount >= 0 ? baseNodeCount : 0;
|
|
2014
2223
|
const top = safeCount * STACK_PITCH + PARTS_GAP * 2;
|
|
2015
2224
|
// 同じ名前が 2 度来たら先の方だけを見る。 後の分を残すと、 どちらを指したか決められない
|
|
2016
2225
|
// まま列の送り幅にも影響する
|
|
@@ -2212,9 +2421,7 @@ function partBoxes(
|
|
|
2212
2421
|
const t = partTargetSize(part, a.posW, a.posH, a.scale);
|
|
2213
2422
|
const size = partExtent(part, t.w, t.h);
|
|
2214
2423
|
const placed =
|
|
2215
|
-
a.posX !== undefined && a.posY !== undefined
|
|
2216
|
-
? { cx: a.posX, cy: a.posY }
|
|
2217
|
-
: grid.get(a.name);
|
|
2424
|
+
a.posX !== undefined && a.posY !== undefined ? { cx: a.posX, cy: a.posY } : grid.get(a.name);
|
|
2218
2425
|
// 相対で書いた分はここでは決まらない (解決側が後で埋める)
|
|
2219
2426
|
if (!placed) continue;
|
|
2220
2427
|
// 渡す座標は段の中心。 矩形の中心はそこからずれる
|
|
@@ -2234,68 +2441,68 @@ function cleanupPlaceholderActor(
|
|
|
2234
2441
|
doc: DslDocument,
|
|
2235
2442
|
a: { name: string; lane?: string },
|
|
2236
2443
|
): void {
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
}
|
|
2245
|
-
}
|
|
2246
|
-
const ownedNodeIds = new Set<string>();
|
|
2247
|
-
for (const n of target.nodes) {
|
|
2248
|
-
if (ownedLaneIds.has(n.lane)) ownedNodeIds.add(n.id);
|
|
2444
|
+
const aliasSlug = slugify(a.name);
|
|
2445
|
+
const ownedLaneIds = new Set<string>();
|
|
2446
|
+
if (doc.type === "sequence" || doc.type === "solidity") {
|
|
2447
|
+
for (const l of target.lanes) {
|
|
2448
|
+
// 明示 lane mapping (a.lane) 先は part の張替え先で actor 専用 lane ではないため除外
|
|
2449
|
+
if (a.lane !== undefined && l.id === a.lane) continue;
|
|
2450
|
+
if (l.label === a.name) ownedLaneIds.add(l.id);
|
|
2249
2451
|
}
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2452
|
+
}
|
|
2453
|
+
const ownedNodeIds = new Set<string>();
|
|
2454
|
+
for (const n of target.nodes) {
|
|
2455
|
+
if (ownedLaneIds.has(n.lane)) ownedNodeIds.add(n.id);
|
|
2456
|
+
}
|
|
2457
|
+
// actor 専用 lane を引き当てられない経路 (flow / topology 等の共有 lane preset) は従来どおり dragon
|
|
2458
|
+
// slug の prefix match に fallback する。 これらは 1 actor = 1 node (id = slug) の生成規則。
|
|
2459
|
+
const matchesAliasSlug = (id: string): boolean => {
|
|
2460
|
+
if (id === aliasSlug) return true;
|
|
2461
|
+
if (id.startsWith(`${aliasSlug}-`)) return true;
|
|
2462
|
+
// sequence step anchor = `s{N}-{aliasSlug}` pattern
|
|
2463
|
+
if (/^s\d+-/.test(id) && id.endsWith(`-${aliasSlug}`)) return true;
|
|
2464
|
+
return false;
|
|
2465
|
+
};
|
|
2466
|
+
const relatedToActor = (id: string): boolean =>
|
|
2467
|
+
ownedLaneIds.size > 0 ? ownedNodeIds.has(id) : matchesAliasSlug(id);
|
|
2468
|
+
target.nodes = target.nodes.filter((n) => !relatedToActor(n.id));
|
|
2469
|
+
// edge も同経路で削除 (parts actor に接続していた flow を除去、 parts merge 後の flow は user が
|
|
2470
|
+
// 別途書く経路になる)。 削除した edge の id は phase.activate に残ると dangling 参照になるため回収する。
|
|
2471
|
+
const removedEdgeIds = new Set<string>();
|
|
2472
|
+
target.edges = target.edges.filter((e) => {
|
|
2473
|
+
const drop = relatedToActor(e.from) || relatedToActor(e.to);
|
|
2474
|
+
if (drop) removedEdgeIds.add(e.id);
|
|
2475
|
+
return !drop;
|
|
2476
|
+
});
|
|
2477
|
+
// lane も削除 = sequence preset は parts actor 用に lane (id = aliasSlug、 label = actor 名) を
|
|
2478
|
+
// 生成する。 node/edge だけ消して lane を残すと、 merge 後の part 側 lane (label = alias) と 2 本が
|
|
2479
|
+
// 同じ label を lane-label として描画し二重表示になる (actor ラベル二重表示 bug の root cause)。
|
|
2480
|
+
//
|
|
2481
|
+
// 削除は seq-like preset (sequence / solidity = compileSequence 経由) に限定する。 これらは
|
|
2482
|
+
// 1 actor = 1 lane (lane.label === a.name、 lane.id は actor 名の slug) の生成規則が成立し、
|
|
2483
|
+
// parts actor 用 lane を安全に削除できる。 他 preset (flow / topology / class / pie 等) は複数
|
|
2484
|
+
// actor が共有 lane (id = "main" 等) を参照するため、 一致 lane を消すと通常 actor の node が
|
|
2485
|
+
// 削除済 lane を参照する不正 diagram になる (cc-codex MAJOR 指摘)。
|
|
2486
|
+
//
|
|
2487
|
+
// leftover lane の特定は lane.label === a.name を第一に使う。 seq-like preset は非 animate 経路
|
|
2488
|
+
// (cdl preset の slugify) と animate 経路 (dragon の slugify) で lane.id の slug 規則が異なり
|
|
2489
|
+
// (`_`/全角の扱い等)、 aliasSlug (dragon slugify) と lane.id が不一致になる actor 名がある。 lane.label
|
|
2490
|
+
// は両経路とも a.name 生値なので slug 差の影響を受けず確実に一致する。 id === aliasSlug は
|
|
2491
|
+
// label 未設定 preset への fallback (exact match のみ、 prefix は false match risk のため付けない)。
|
|
2492
|
+
if (doc.type === "sequence" || doc.type === "solidity") {
|
|
2493
|
+
target.lanes = target.lanes.filter((l) => {
|
|
2494
|
+
// 明示 lane mapping (a.lane) 先は part の張替え先なので保持する。
|
|
2495
|
+
if (a.lane !== undefined && l.id === a.lane) return true;
|
|
2496
|
+
if (l.label === a.name) return false;
|
|
2497
|
+
if (l.id === aliasSlug) return false;
|
|
2498
|
+
return true;
|
|
2269
2499
|
});
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
// parts actor 用 lane を安全に削除できる。 他 preset (flow / topology / class / pie 等) は複数
|
|
2277
|
-
// actor が共有 lane (id = "main" 等) を参照するため、 一致 lane を消すと通常 actor の node が
|
|
2278
|
-
// 削除済 lane を参照する不正 diagram になる (cc-codex MAJOR 指摘)。
|
|
2279
|
-
//
|
|
2280
|
-
// leftover lane の特定は lane.label === a.name を第一に使う。 seq-like preset は非 animate 経路
|
|
2281
|
-
// (cdl preset の slugify) と animate 経路 (dragon の slugify) で lane.id の slug 規則が異なり
|
|
2282
|
-
// (`_`/全角の扱い等)、 aliasSlug (dragon slugify) と lane.id が不一致になる actor 名がある。 lane.label
|
|
2283
|
-
// は両経路とも a.name 生値なので slug 差の影響を受けず確実に一致する。 id === aliasSlug は
|
|
2284
|
-
// label 未設定 preset への fallback (exact match のみ、 prefix は false match risk のため付けない)。
|
|
2285
|
-
if (doc.type === "sequence" || doc.type === "solidity") {
|
|
2286
|
-
target.lanes = target.lanes.filter((l) => {
|
|
2287
|
-
// 明示 lane mapping (a.lane) 先は part の張替え先なので保持する。
|
|
2288
|
-
if (a.lane !== undefined && l.id === a.lane) return true;
|
|
2289
|
-
if (l.label === a.name) return false;
|
|
2290
|
-
if (l.id === aliasSlug) return false;
|
|
2291
|
-
return true;
|
|
2292
|
-
});
|
|
2293
|
-
}
|
|
2294
|
-
// 削除された node / edge を activate 参照している既存 phase の cleanup (node 削除と同じ判定経路
|
|
2295
|
-
// = 取りこぼすと存在しない id が activate に残り dangling 参照になる、 #873)
|
|
2296
|
-
for (const phase of target.phases) {
|
|
2297
|
-
phase.activate = phase.activate.filter((id) => !relatedToActor(id) && !removedEdgeIds.has(id));
|
|
2298
|
-
}
|
|
2500
|
+
}
|
|
2501
|
+
// 削除された node / edge を activate 参照している既存 phase の cleanup (node 削除と同じ判定経路
|
|
2502
|
+
// = 取りこぼすと存在しない id が activate に残り dangling 参照になる、 #873)
|
|
2503
|
+
for (const phase of target.phases) {
|
|
2504
|
+
phase.activate = phase.activate.filter((id) => !relatedToActor(id) && !removedEdgeIds.has(id));
|
|
2505
|
+
}
|
|
2299
2506
|
}
|
|
2300
2507
|
|
|
2301
2508
|
/**
|
|
@@ -2362,7 +2569,9 @@ function mergePartsFromActors(
|
|
|
2362
2569
|
const part = found;
|
|
2363
2570
|
if (!part) {
|
|
2364
2571
|
if (typeof console !== "undefined" && console.warn) {
|
|
2365
|
-
console.warn(
|
|
2572
|
+
console.warn(
|
|
2573
|
+
`[dragon] parts kind "${partId}" not found in partsCatalog (actor: ${actor.name})`,
|
|
2574
|
+
);
|
|
2366
2575
|
}
|
|
2367
2576
|
continue;
|
|
2368
2577
|
}
|
|
@@ -2450,7 +2659,8 @@ function resolveStateOverride(
|
|
|
2450
2659
|
override: number | string | boolean | undefined,
|
|
2451
2660
|
): { initial: number | string; rejected: boolean } {
|
|
2452
2661
|
if (override === undefined) return { initial: original, rejected: false };
|
|
2453
|
-
if (isColorValue(original) && !isColorValue(override))
|
|
2662
|
+
if (isColorValue(original) && !isColorValue(override))
|
|
2663
|
+
return { initial: original, rejected: true };
|
|
2454
2664
|
return { initial: override as number | string, rejected: false };
|
|
2455
2665
|
}
|
|
2456
2666
|
|
|
@@ -2615,9 +2825,8 @@ function mergePartIntoDiagram(
|
|
|
2615
2825
|
// directive で廃止 (2026-07-21)。 重なりは user の意図位置を優先し、 手動移動で回避する経路。
|
|
2616
2826
|
// 未指定 (座標なし fallback) 時のみ existingMax + gap で右外配置 (通常経路は drop/click で座標を渡す)。
|
|
2617
2827
|
const PARTS_LANE_GAP = 300;
|
|
2618
|
-
const existingLaneMaxX =
|
|
2619
|
-
? Math.max(...target.lanes.map((l) => (l.x ?? 0) + l.width))
|
|
2620
|
-
: 0;
|
|
2828
|
+
const existingLaneMaxX =
|
|
2829
|
+
target.lanes.length > 0 ? Math.max(...target.lanes.map((l) => (l.x ?? 0) + l.width)) : 0;
|
|
2621
2830
|
// parts 全体 resize (I2 forensic): user が SE handle drag で targetW/H 指定 = actor.posW/H。
|
|
2622
2831
|
// scale 基準は part 全体の bbox 幅 (全 lane の最左端〜最右端) にする。 lane[0] 幅だけを基準にすると
|
|
2623
2832
|
// multi-lane part (複数 lane を横に並べた part) で全体幅を過小評価し、 非先頭 lane の node が自 lane
|
|
@@ -2649,9 +2858,10 @@ function mergePartIntoDiagram(
|
|
|
2649
2858
|
// 変換し、 lane.x = mapLaneX(元 lane 左端) にすることで全 lane / 全 node が一貫して drop 座標を中心に
|
|
2650
2859
|
// scale 配置される (cc-codex #879 の mapPartX と同じ発想を lane push まで前倒し、 #880 root fix)。
|
|
2651
2860
|
const partOrigBboxCenterX = partMinLaneX + partsBboxW / 2;
|
|
2652
|
-
const dropCenterX =
|
|
2653
|
-
|
|
2654
|
-
|
|
2861
|
+
const dropCenterX =
|
|
2862
|
+
offsetX !== undefined
|
|
2863
|
+
? offsetX
|
|
2864
|
+
: existingLaneMaxX + PARTS_LANE_GAP + (partsBboxW * laneScaleX) / 2;
|
|
2655
2865
|
const mapLaneX = (x: number): number => (x - partOrigBboxCenterX) * laneScaleX + dropCenterX;
|
|
2656
2866
|
|
|
2657
2867
|
for (const laneOrig of part.lanes) {
|
|
@@ -2688,9 +2898,10 @@ function mergePartIntoDiagram(
|
|
|
2688
2898
|
const shouldForcePos = offsetX !== undefined || offsetY !== undefined;
|
|
2689
2899
|
// target 側の現在 max stack + isolation offset で parts node の stack を shift、
|
|
2690
2900
|
// sequence の rowH 計算と完全分離 (D2 fix、 posX/posY 明示との 2 段防御)。
|
|
2691
|
-
const targetMaxStack =
|
|
2692
|
-
|
|
2693
|
-
|
|
2901
|
+
const targetMaxStack =
|
|
2902
|
+
shouldForcePos && target.nodes.length > 0
|
|
2903
|
+
? Math.max(...target.nodes.map((n) => n.stack ?? 0))
|
|
2904
|
+
: 0;
|
|
2694
2905
|
const stackShiftBase = shouldForcePos ? targetMaxStack + STACK_ISOLATION_OFFSET : 0;
|
|
2695
2906
|
// parts 全体 resize scale (I2 forensic 対応): targetW / targetH 指定時、 parts の元 total size
|
|
2696
2907
|
// に対する比率 = scale 係数、 全 sub-node の w / h + cx / cy 相対位置に scale 反映。
|
|
@@ -2758,8 +2969,10 @@ function mergePartIntoDiagram(
|
|
|
2758
2969
|
// (cc-codex #879 Round 2/3 MAJOR + #880)。 mapLaneX は part bbox 中心 → drop 座標の scale 変換で、
|
|
2759
2970
|
// lane / node / 明示 posX / auto-layout の全経路がこの 1 式を共有するため、 lane.x != 0 でも
|
|
2760
2971
|
// multi-lane でも node 中心と自 lane 中心が一致する。
|
|
2761
|
-
let nodePosX: number | undefined =
|
|
2762
|
-
|
|
2972
|
+
let nodePosX: number | undefined =
|
|
2973
|
+
nodeOrig.posX !== undefined ? mapLaneX(nodeOrig.posX) : undefined;
|
|
2974
|
+
let nodePosY: number | undefined =
|
|
2975
|
+
nodeOrig.posY !== undefined ? nodeOrig.posY + (offsetY ?? 0) : undefined;
|
|
2763
2976
|
if (shouldForcePos && nodePosX === undefined) {
|
|
2764
2977
|
// posX を持たない node は所属 lane の中央 (auto layout の cx 相当) を同じ mapLaneX で変換する。
|
|
2765
2978
|
const geom = partLaneGeom.get(nodeOrig.lane) ?? { x: 0, w: 320 };
|
|
@@ -2775,12 +2988,10 @@ function mergePartIntoDiagram(
|
|
|
2775
2988
|
// 座標が非有限になって図が描けない (実測 = 箱の中心が NaN になった)
|
|
2776
2989
|
const rawNodeW = nodeOrig.w !== undefined ? positiveOr(nodeOrig.w, 200) : undefined;
|
|
2777
2990
|
const rawNodeH = nodeOrig.h !== undefined ? positiveOr(nodeOrig.h, 200) : undefined;
|
|
2778
|
-
const nodeW =
|
|
2779
|
-
? rawNodeW * scaleX
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
? rawNodeH * scaleY
|
|
2783
|
-
: rawNodeH;
|
|
2991
|
+
const nodeW =
|
|
2992
|
+
rawNodeW !== undefined && (scaleX !== 1 || scaleY !== 1) ? rawNodeW * scaleX : rawNodeW;
|
|
2993
|
+
const nodeH =
|
|
2994
|
+
rawNodeH !== undefined && (scaleX !== 1 || scaleY !== 1) ? rawNodeH * scaleY : rawNodeH;
|
|
2784
2995
|
target.nodes.push({
|
|
2785
2996
|
...nodeOrig,
|
|
2786
2997
|
id: prefix(nodeOrig.id),
|
|
@@ -2800,7 +3011,10 @@ function mergePartIntoDiagram(
|
|
|
2800
3011
|
|
|
2801
3012
|
// state merge = id prefix + initial override
|
|
2802
3013
|
for (const stateOrig of part.states) {
|
|
2803
|
-
const { initial, rejected } = resolveStateOverride(
|
|
3014
|
+
const { initial, rejected } = resolveStateOverride(
|
|
3015
|
+
stateOrig.initial,
|
|
3016
|
+
stateOverride[stateOrig.id],
|
|
3017
|
+
);
|
|
2804
3018
|
if (rejected) {
|
|
2805
3019
|
onNotice?.({
|
|
2806
3020
|
kind: "state-override-rejected",
|
|
@@ -2844,7 +3058,10 @@ function mergePartIntoDiagram(
|
|
|
2844
3058
|
if (part.readouts && part.readouts.length > 0) {
|
|
2845
3059
|
if (!target.readouts) target.readouts = [];
|
|
2846
3060
|
for (const readoutOrig of part.readouts) {
|
|
2847
|
-
const rewritten = deepRewriteStrings(
|
|
3061
|
+
const rewritten = deepRewriteStrings(
|
|
3062
|
+
readoutOrig as unknown,
|
|
3063
|
+
rewriteTemplate,
|
|
3064
|
+
) as CdlDiagram["readouts"] extends readonly (infer R)[] ? R : never;
|
|
2848
3065
|
// id は shape 全 walk で rewrite されないので個別に prefix
|
|
2849
3066
|
target.readouts.push({
|
|
2850
3067
|
...(rewritten as { id: string }),
|
|
@@ -2882,8 +3099,14 @@ function mergePartIntoDiagram(
|
|
|
2882
3099
|
const partPhase = part.phases[i]!;
|
|
2883
3100
|
targetPhase.duration = Math.max(targetPhase.duration, partPhase.duration);
|
|
2884
3101
|
targetPhase.activate = [...targetPhase.activate, ...partPhase.activate.map(prefix)];
|
|
2885
|
-
targetPhase.tweens = [
|
|
2886
|
-
|
|
3102
|
+
targetPhase.tweens = [
|
|
3103
|
+
...targetPhase.tweens,
|
|
3104
|
+
...partPhase.tweens.map((t) => ({ ...t, stateId: valuePrefix(t.stateId) })),
|
|
3105
|
+
];
|
|
3106
|
+
targetPhase.sets = [
|
|
3107
|
+
...targetPhase.sets,
|
|
3108
|
+
...partPhase.sets.map((s) => ({ ...s, stateId: valuePrefix(s.stateId) })),
|
|
3109
|
+
];
|
|
2887
3110
|
}
|
|
2888
3111
|
// parts phase 余剰は append (target より parts が長い場合)
|
|
2889
3112
|
for (let i = commonLen; i < partsLen; i++) {
|
|
@@ -2978,6 +3201,49 @@ function applyEdgeInlineOptions(
|
|
|
2978
3201
|
});
|
|
2979
3202
|
}
|
|
2980
3203
|
|
|
3204
|
+
/**
|
|
3205
|
+
* 出来事が指す相手を、図の識別子へ直す (#1393)。 見つからなければ `undefined`。
|
|
3206
|
+
*
|
|
3207
|
+
* 箱と縦列は **名前から作った識別子** と **描かれる題** の両方で探す。 記法は名前で書き、
|
|
3208
|
+
* 図の識別子はそこから作られるが、見本を重ねた図など識別子が名前と揃わない形もある。
|
|
3209
|
+
* 矢印は両端の名前から識別子を作って突き合わせる。
|
|
3210
|
+
*/
|
|
3211
|
+
function 出来事の相手を解く(
|
|
3212
|
+
diagram: CdlDiagram,
|
|
3213
|
+
doc: DslDocument,
|
|
3214
|
+
e: DslEventBinding,
|
|
3215
|
+
): NonNullable<CdlDiagram["eventBindings"]>[number]["target"] | undefined {
|
|
3216
|
+
if (e.target.kind === "diagram") return { kind: "diagram" };
|
|
3217
|
+
if (e.target.kind === "edge") {
|
|
3218
|
+
const from = slugify(e.target.from);
|
|
3219
|
+
const to = slugify(e.target.to);
|
|
3220
|
+
let 矢印: CdlEdge | undefined;
|
|
3221
|
+
if (doc.type === "sequence" || doc.type === "solidity") {
|
|
3222
|
+
const stepIdx = doc.flow.findIndex(
|
|
3223
|
+
(step) => slugify(step.from) === from && slugify(step.to) === to,
|
|
3224
|
+
);
|
|
3225
|
+
if (stepIdx >= 0) {
|
|
3226
|
+
矢印 = diagram.edges.find(
|
|
3227
|
+
(x) =>
|
|
3228
|
+
(x.from === `s${stepIdx}-${from}` || x.from === from) &&
|
|
3229
|
+
(x.to === `s${stepIdx}-${to}` || x.from === x.to),
|
|
3230
|
+
);
|
|
3231
|
+
}
|
|
3232
|
+
} else {
|
|
3233
|
+
矢印 = diagram.edges.find((x) => x.from === from && x.to === to);
|
|
3234
|
+
}
|
|
3235
|
+
return 矢印 ? { kind: "edge", id: 矢印.id } : undefined;
|
|
3236
|
+
}
|
|
3237
|
+
const 名 = e.target.name;
|
|
3238
|
+
const slug = slugify(名);
|
|
3239
|
+
if (e.target.kind === "node") {
|
|
3240
|
+
const 箱 = diagram.nodes.find((n) => n.id === slug || n.id === 名 || n.title === 名);
|
|
3241
|
+
return 箱 ? { kind: "node", id: 箱.id } : undefined;
|
|
3242
|
+
}
|
|
3243
|
+
const 列 = (diagram.lanes ?? []).find((l) => l.id === slug || l.id === 名 || l.label === 名);
|
|
3244
|
+
return 列 ? { kind: "lane", id: 列.id } : undefined;
|
|
3245
|
+
}
|
|
3246
|
+
|
|
2981
3247
|
/** 本文に書いた矢印の指定を、対応が取れた矢印へ書き写す。 対応の取り方は呼出側が決める。 */
|
|
2982
3248
|
function 矢印へ書き写す(target: CdlEdge, s: DslStep, doc: DslDocument): void {
|
|
2983
3249
|
// **書いた補足が勝つ** (#1275)。 ここで写さないと 2 つ落ちる。 静止した `type: flow` は
|
|
@@ -2993,14 +3259,19 @@ function 矢印へ書き写す(target: CdlEdge, s: DslStep, doc: DslDocument): v
|
|
|
2993
3259
|
target.cardinality = s.cardinality;
|
|
2994
3260
|
// ER preset の場合 label に "(1:N)" 形式で併記 (既に含まれていればスキップ)
|
|
2995
3261
|
if (doc.type === "er" && !target.label.includes(s.cardinality)) {
|
|
2996
|
-
target.label = target.label
|
|
2997
|
-
? `${target.label} (${s.cardinality})`
|
|
2998
|
-
: `(${s.cardinality})`;
|
|
3262
|
+
target.label = target.label ? `${target.label} (${s.cardinality})` : `(${s.cardinality})`;
|
|
2999
3263
|
}
|
|
3000
3264
|
}
|
|
3265
|
+
// 矢印がどの辺から出るか (#1385)。 書かなければ描画側が自動で選ぶ
|
|
3266
|
+
if (s.side !== undefined) target.side = s.side;
|
|
3001
3267
|
if (s.labelOffsetX !== undefined) target.labelOffsetX = s.labelOffsetX;
|
|
3002
3268
|
if (s.labelOffsetY !== undefined) target.labelOffsetY = s.labelOffsetY;
|
|
3003
3269
|
if (s.overlay !== undefined) target.overlay = s.overlay;
|
|
3270
|
+
// 値に追随する 3 欄 (#1396)。 太さ / 色 / 破線の位置が値に合わせて動く。
|
|
3271
|
+
// 通り道そのものは動かないので、配置計算と重なり解消には影響しない
|
|
3272
|
+
if (s.widthBind !== undefined) target.widthBind = s.widthBind;
|
|
3273
|
+
if (s.strokeBind !== undefined) target.strokeBind = s.strokeBind;
|
|
3274
|
+
if (s.dashOffsetBind !== undefined) target.dashOffsetBind = s.dashOffsetBind;
|
|
3004
3275
|
}
|
|
3005
3276
|
|
|
3006
3277
|
/**
|
|
@@ -3090,7 +3361,8 @@ function 式に書く数(n: number): string {
|
|
|
3090
3361
|
const decimalAt = whole.length + Number(exponentText);
|
|
3091
3362
|
let expanded: string;
|
|
3092
3363
|
if (decimalAt <= 0) expanded = `0.${"0".repeat(-decimalAt)}${digits}`;
|
|
3093
|
-
else if (decimalAt >= digits.length)
|
|
3364
|
+
else if (decimalAt >= digits.length)
|
|
3365
|
+
expanded = `${digits}${"0".repeat(decimalAt - digits.length)}`;
|
|
3094
3366
|
else expanded = `${digits.slice(0, decimalAt)}.${digits.slice(decimalAt)}`;
|
|
3095
3367
|
return negative ? `-${expanded}` : expanded;
|
|
3096
3368
|
}
|
|
@@ -3109,13 +3381,20 @@ type 動く区間 = { 段: number; start: number; end: number; dur: number; from
|
|
|
3109
3381
|
function 境目を通る時刻(区間: 動く区間, op: string, 境目: number): number | null {
|
|
3110
3382
|
const 満たす = (x: number): boolean => {
|
|
3111
3383
|
switch (op) {
|
|
3112
|
-
case ">=":
|
|
3113
|
-
|
|
3114
|
-
case "
|
|
3115
|
-
|
|
3116
|
-
case "
|
|
3117
|
-
|
|
3118
|
-
|
|
3384
|
+
case ">=":
|
|
3385
|
+
return x >= 境目;
|
|
3386
|
+
case ">":
|
|
3387
|
+
return x > 境目;
|
|
3388
|
+
case "<=":
|
|
3389
|
+
return x <= 境目;
|
|
3390
|
+
case "<":
|
|
3391
|
+
return x < 境目;
|
|
3392
|
+
case "==":
|
|
3393
|
+
return x === 境目;
|
|
3394
|
+
case "!=":
|
|
3395
|
+
return x !== 境目;
|
|
3396
|
+
default:
|
|
3397
|
+
return false;
|
|
3119
3398
|
}
|
|
3120
3399
|
};
|
|
3121
3400
|
if (満たす(区間.from)) return 区間.start;
|
|
@@ -3196,7 +3475,13 @@ function foldValueTriggers(
|
|
|
3196
3475
|
const 解決中 = new Set<string>();
|
|
3197
3476
|
|
|
3198
3477
|
const 知らせる = (v: DslValue, message: string, hint: string): void => {
|
|
3199
|
-
onNotice?.({
|
|
3478
|
+
onNotice?.({
|
|
3479
|
+
kind: "value-trigger-unresolved",
|
|
3480
|
+
actor: v.name,
|
|
3481
|
+
line: v.pos?.line ?? 0,
|
|
3482
|
+
message,
|
|
3483
|
+
hint,
|
|
3484
|
+
});
|
|
3200
3485
|
};
|
|
3201
3486
|
|
|
3202
3487
|
const 解く = (name: string): 動く区間 | null => {
|
|
@@ -3207,7 +3492,11 @@ function foldValueTriggers(
|
|
|
3207
3492
|
if (!v || !v.trigger) return null;
|
|
3208
3493
|
if (解決中.has(name)) {
|
|
3209
3494
|
解けない.add(name);
|
|
3210
|
-
知らせる(
|
|
3495
|
+
知らせる(
|
|
3496
|
+
v,
|
|
3497
|
+
`"${name}" のきっかけが一周しています`,
|
|
3498
|
+
"どれか 1 つを `trigger: step ...` に変える",
|
|
3499
|
+
);
|
|
3211
3500
|
return null;
|
|
3212
3501
|
}
|
|
3213
3502
|
解決中.add(name);
|
|
@@ -3231,7 +3520,11 @@ function foldValueTriggers(
|
|
|
3231
3520
|
if (trigger.kind === "step") {
|
|
3232
3521
|
const idx = 段の番号.get(trigger.step);
|
|
3233
3522
|
if (idx === undefined) {
|
|
3234
|
-
知らせる(
|
|
3523
|
+
知らせる(
|
|
3524
|
+
v,
|
|
3525
|
+
`"${trigger.step}" という段がありません`,
|
|
3526
|
+
"`animation:` にその名前の段を書くか、 段の名前に合わせる",
|
|
3527
|
+
);
|
|
3235
3528
|
return null;
|
|
3236
3529
|
}
|
|
3237
3530
|
段 = idx;
|
|
@@ -3287,7 +3580,8 @@ function foldValueTriggers(
|
|
|
3287
3580
|
段ごとの時計.set(段, 名前);
|
|
3288
3581
|
diagram.states.push({ id: 名前, initial: 0 });
|
|
3289
3582
|
const 段の中身 = diagram.phases[段];
|
|
3290
|
-
if (段の中身)
|
|
3583
|
+
if (段の中身)
|
|
3584
|
+
段の中身.tweens = [...段の中身.tweens, { stateId: 名前, from: 0, to: 段の中身.duration }];
|
|
3291
3585
|
return 名前;
|
|
3292
3586
|
};
|
|
3293
3587
|
|
|
@@ -3555,9 +3849,15 @@ const VALUE_NOTICE_HINT: Readonly<Record<string, string>> = {
|
|
|
3555
3849
|
* **今は記法からも到達する** (一覧へ戻す作業が要らなかったのはこのため)。
|
|
3556
3850
|
*/
|
|
3557
3851
|
const SINGLE_BOX_KINDS: ReadonlySet<string> = new Set([
|
|
3558
|
-
"chart-pie",
|
|
3559
|
-
"
|
|
3560
|
-
"
|
|
3852
|
+
"chart-pie",
|
|
3853
|
+
"chart-line",
|
|
3854
|
+
"chart-bar",
|
|
3855
|
+
"gantt-timeline",
|
|
3856
|
+
"mind-map",
|
|
3857
|
+
"funnel-stages",
|
|
3858
|
+
"quadrant-matrix",
|
|
3859
|
+
"tree-hierarchy",
|
|
3860
|
+
"journey-map",
|
|
3561
3861
|
]);
|
|
3562
3862
|
|
|
3563
3863
|
/** 記法の種別を描画の種別に直す。 描けない種別のままなら `undefined`。 */
|
|
@@ -3666,14 +3966,36 @@ const LABEL_MIN_H: Readonly<Record<string, number>> = {
|
|
|
3666
3966
|
*/
|
|
3667
3967
|
const LABEL_NEVER_FITS: ReadonlySet<string> = new Set([
|
|
3668
3968
|
// 左右にはみ出す 24 種。 横幅は高さで変わらないため直らない
|
|
3669
|
-
"shape-api-gateway",
|
|
3670
|
-
"shape-
|
|
3671
|
-
"shape-
|
|
3672
|
-
"shape-
|
|
3673
|
-
"shape-
|
|
3674
|
-
"shape-
|
|
3969
|
+
"shape-api-gateway",
|
|
3970
|
+
"shape-atm",
|
|
3971
|
+
"shape-auditor",
|
|
3972
|
+
"shape-bank",
|
|
3973
|
+
"shape-bitcoin-chain",
|
|
3974
|
+
"shape-blockchain",
|
|
3975
|
+
"shape-blockchain-block",
|
|
3976
|
+
"shape-blockchain-node",
|
|
3977
|
+
"shape-brokerage",
|
|
3978
|
+
"shape-code-block",
|
|
3979
|
+
"shape-customer-service",
|
|
3980
|
+
"shape-ethereum-chain",
|
|
3981
|
+
"shape-hexagon",
|
|
3982
|
+
"shape-kanban-card",
|
|
3983
|
+
"shape-lawyer",
|
|
3984
|
+
"shape-network-node",
|
|
3985
|
+
"shape-nft",
|
|
3986
|
+
"shape-notary",
|
|
3987
|
+
"shape-regulator",
|
|
3988
|
+
"shape-satellite",
|
|
3989
|
+
"shape-smart-contract",
|
|
3990
|
+
"shape-terminal",
|
|
3991
|
+
"shape-trader",
|
|
3992
|
+
"shape-trust-bank",
|
|
3675
3993
|
// 下のはみ出しが高さに依らない 6 種
|
|
3676
|
-
"shape-cylinder",
|
|
3994
|
+
"shape-cylinder",
|
|
3995
|
+
"shape-diamond",
|
|
3996
|
+
"shape-file",
|
|
3997
|
+
"shape-folder",
|
|
3998
|
+
"shape-mobile-device",
|
|
3677
3999
|
"shape-stack",
|
|
3678
4000
|
// 上へ出る絵が名札の大きさでは読めない。 `#1067` では「上は何ともぶつからない」 として残したが、
|
|
3679
4001
|
// 実際には絵が小さく潰れて名前と重なり、 横に並べた時も 1 本だけ頭が浮く (user 実機確認)
|
|
@@ -3979,6 +4301,7 @@ function compileGantt(doc: DslDocument): CdlDiagram {
|
|
|
3979
4301
|
const 目盛りなし: string[] = [];
|
|
3980
4302
|
const タスク: {
|
|
3981
4303
|
name: string;
|
|
4304
|
+
title: string;
|
|
3982
4305
|
label: string;
|
|
3983
4306
|
tone?: DslDocument["actors"][number]["tone"];
|
|
3984
4307
|
owner?: string;
|
|
@@ -3994,6 +4317,7 @@ function compileGantt(doc: DslDocument): CdlDiagram {
|
|
|
3994
4317
|
// 色は帯にそのまま渡す。 箱が 1 つになっても、 書いた色が消えないようにする
|
|
3995
4318
|
タスク.push({
|
|
3996
4319
|
name: a.name,
|
|
4320
|
+
title: 箱の題(a),
|
|
3997
4321
|
label,
|
|
3998
4322
|
...(a.tone !== undefined ? { tone: a.tone } : {}),
|
|
3999
4323
|
...(a.owner !== undefined ? { owner: a.owner } : {}),
|
|
@@ -4020,7 +4344,12 @@ function compileGantt(doc: DslDocument): CdlDiagram {
|
|
|
4020
4344
|
}
|
|
4021
4345
|
依存元.set(s.to, s.from);
|
|
4022
4346
|
// 帯の依存は「どちらが先か」 だけを持つ。 矢印に書いた文字や色は描けないので伝える
|
|
4023
|
-
if (
|
|
4347
|
+
if (
|
|
4348
|
+
(s.label ?? "") !== "" ||
|
|
4349
|
+
(s.sub ?? "") !== "" ||
|
|
4350
|
+
s.tone !== undefined ||
|
|
4351
|
+
s.style !== undefined
|
|
4352
|
+
) {
|
|
4024
4353
|
装飾つき.push(`${s.from} -> ${s.to}`);
|
|
4025
4354
|
}
|
|
4026
4355
|
}
|
|
@@ -4059,7 +4388,7 @@ function compileGantt(doc: DslDocument): CdlDiagram {
|
|
|
4059
4388
|
const 終わり = 終わる位置(t.end, idx, 目盛り, t.name, 逆向きを伝える, doc);
|
|
4060
4389
|
return {
|
|
4061
4390
|
id: slugify(t.name) || t.name,
|
|
4062
|
-
title: t.
|
|
4391
|
+
title: t.title,
|
|
4063
4392
|
startIdx: idx,
|
|
4064
4393
|
endIdx: 終わり.idx,
|
|
4065
4394
|
startLabel: t.label,
|
|
@@ -4110,7 +4439,7 @@ function compileClass(doc: DslDocument): CdlDiagram {
|
|
|
4110
4439
|
lane: laneId,
|
|
4111
4440
|
stack: 0,
|
|
4112
4441
|
kind: "storage",
|
|
4113
|
-
title: a
|
|
4442
|
+
title: 箱の題(a),
|
|
4114
4443
|
w: CLASS_W,
|
|
4115
4444
|
});
|
|
4116
4445
|
});
|
|
@@ -4121,6 +4450,7 @@ function compileClass(doc: DslDocument): CdlDiagram {
|
|
|
4121
4450
|
b.edge(fromId, toId, {
|
|
4122
4451
|
label: s.label,
|
|
4123
4452
|
...(s.sub ? { sub: s.sub } : {}),
|
|
4453
|
+
...(s.side ? { side: s.side } : {}),
|
|
4124
4454
|
...(s.tone ? { tone: s.tone } : {}),
|
|
4125
4455
|
...(s.style ? { style: s.style } : {}),
|
|
4126
4456
|
});
|
|
@@ -4185,7 +4515,6 @@ function parseChartValue(raw: string | undefined): number | string | null {
|
|
|
4185
4515
|
return parseBoundValue(raw);
|
|
4186
4516
|
}
|
|
4187
4517
|
|
|
4188
|
-
|
|
4189
4518
|
/** 状態を読む欄が指している名前 (`{v.sum}` なら `v`)。 欄でなければ null */
|
|
4190
4519
|
function 参照する名前(value: number | string | null): string | null {
|
|
4191
4520
|
if (typeof value !== "string") return null;
|
|
@@ -4251,7 +4580,7 @@ function compileValueChart(
|
|
|
4251
4580
|
continue;
|
|
4252
4581
|
}
|
|
4253
4582
|
// 色はそのまま渡す。 箱が 1 つになっても、 書いた色が消えないようにする
|
|
4254
|
-
data.push({ label: a
|
|
4583
|
+
data.push({ label: 箱の題(a), value, ...(a.tone !== undefined ? { tone: a.tone } : {}) });
|
|
4255
4584
|
}
|
|
4256
4585
|
// 案内の言葉は型ごとに変える。 共通化した時に `pie` の「割合 / 円 / 45%」 が「値 / 図 / 45」 に
|
|
4257
4586
|
// 薄まり、 既存の案内が後退した (review 指摘)。 何を書けばよいかは型ごとに違う
|
|
@@ -4313,7 +4642,6 @@ function compileValueChart(
|
|
|
4313
4642
|
return b.build();
|
|
4314
4643
|
}
|
|
4315
4644
|
|
|
4316
|
-
|
|
4317
4645
|
/**
|
|
4318
4646
|
* 図表 4 種の組立て (#1154 段 2 / 段 3)。
|
|
4319
4647
|
*
|
|
@@ -4415,7 +4743,11 @@ const 区画 = new Map<string, "topLeft" | "topRight" | "bottomLeft" | "bottomRi
|
|
|
4415
4743
|
*/
|
|
4416
4744
|
function 図表の欄から参照できる名前(
|
|
4417
4745
|
doc: DslDocument,
|
|
4418
|
-
欄: {
|
|
4746
|
+
欄: {
|
|
4747
|
+
読めるか: (v: number | string) => boolean;
|
|
4748
|
+
補間で壊れるか: boolean;
|
|
4749
|
+
自動の値を許すか: boolean;
|
|
4750
|
+
},
|
|
4419
4751
|
): Set<string> {
|
|
4420
4752
|
// 同じ名前を 2 回宣言した時は後ろが効く。 描画側が後の宣言を有効値として扱うため、
|
|
4421
4753
|
// 前の宣言で判定すると「読めると判定したのに読めない値が入る」 状態になる (実測)
|
|
@@ -4536,7 +4868,7 @@ function compileFunnel(doc: DslDocument, onNotice?: (n: CompileNotice) => void):
|
|
|
4536
4868
|
未宣言.push(a.name);
|
|
4537
4869
|
continue;
|
|
4538
4870
|
}
|
|
4539
|
-
data.push({ id: slugify(a.name), title: a
|
|
4871
|
+
data.push({ id: slugify(a.name), title: 箱の題(a), count: v });
|
|
4540
4872
|
}
|
|
4541
4873
|
if (未宣言.length > 0) {
|
|
4542
4874
|
const m3 = `type: funnel で数にならない値を参照した項目があります (段に載せません): ${未宣言.join(", ")}。 \`states:\` にその名前を数で書いてください`;
|
|
@@ -4551,11 +4883,23 @@ function compileFunnel(doc: DslDocument, onNotice?: (n: CompileNotice) => void):
|
|
|
4551
4883
|
// 矢印は描けない。 書かれていたら伝える (黙って捨てると「書いたのに効かない」 が残る)
|
|
4552
4884
|
if (doc.flow.length > 0) {
|
|
4553
4885
|
const m2 = `type: funnel では矢印を描けません (${doc.flow.length} 本を無視しました)。 関係を描くなら type: flow を使ってください`;
|
|
4554
|
-
onNotice?.({
|
|
4886
|
+
onNotice?.({
|
|
4887
|
+
kind: "chart-edge-dropped",
|
|
4888
|
+
actor: doc.flow[0]?.from ?? "",
|
|
4889
|
+
line: doc.flow[0]?.pos?.line ?? 0,
|
|
4890
|
+
message: m2,
|
|
4891
|
+
});
|
|
4555
4892
|
if (typeof console !== "undefined" && console.warn) console.warn(`[dragon] ${m2}`);
|
|
4556
4893
|
}
|
|
4557
4894
|
b.node(`${slugify(doc.title) || "funnel"}-chart`, {
|
|
4558
|
-
lane: "chart",
|
|
4895
|
+
lane: "chart",
|
|
4896
|
+
stack: 0,
|
|
4897
|
+
kind: "funnel-stages",
|
|
4898
|
+
title: doc.title,
|
|
4899
|
+
...図の小見出し(doc),
|
|
4900
|
+
w: W,
|
|
4901
|
+
h: H,
|
|
4902
|
+
funnelData: data,
|
|
4559
4903
|
});
|
|
4560
4904
|
return b.build();
|
|
4561
4905
|
}
|
|
@@ -4582,12 +4926,20 @@ function 矢印から親を決める(
|
|
|
4582
4926
|
const 子 = slugify(f.to);
|
|
4583
4927
|
const 親名 = slugify(f.from);
|
|
4584
4928
|
if (!名前.has(親名)) {
|
|
4585
|
-
伝える(
|
|
4929
|
+
伝える(
|
|
4930
|
+
f.from,
|
|
4931
|
+
`type: ${図種} で書いていない名前を親にしています: ${f.from} -> ${f.to}`,
|
|
4932
|
+
f.pos?.line ?? 0,
|
|
4933
|
+
);
|
|
4586
4934
|
continue;
|
|
4587
4935
|
}
|
|
4588
4936
|
// 子の側も見る。 書いていない名前への矢印は、 黙って捨てると図から関係が消える
|
|
4589
4937
|
if (!名前.has(子)) {
|
|
4590
|
-
伝える(
|
|
4938
|
+
伝える(
|
|
4939
|
+
f.to,
|
|
4940
|
+
`type: ${図種} で書いていない名前を子にしています: ${f.from} -> ${f.to}`,
|
|
4941
|
+
f.pos?.line ?? 0,
|
|
4942
|
+
);
|
|
4591
4943
|
continue;
|
|
4592
4944
|
}
|
|
4593
4945
|
if (子 === 親名) {
|
|
@@ -4596,7 +4948,11 @@ function 矢印から親を決める(
|
|
|
4596
4948
|
}
|
|
4597
4949
|
const 既存 = 親.get(子);
|
|
4598
4950
|
if (既存 !== undefined && 既存 !== 親名) {
|
|
4599
|
-
伝える(
|
|
4951
|
+
伝える(
|
|
4952
|
+
f.to,
|
|
4953
|
+
`type: ${図種} で ${f.to} に親が 2 つあります (後の ${f.from} は使いません)`,
|
|
4954
|
+
f.pos?.line ?? 0,
|
|
4955
|
+
);
|
|
4600
4956
|
continue;
|
|
4601
4957
|
}
|
|
4602
4958
|
親.set(子, 親名);
|
|
@@ -4638,7 +4994,10 @@ function compileTree(doc: DslDocument, onNotice?: (n: CompileNotice) => void): C
|
|
|
4638
4994
|
};
|
|
4639
4995
|
for (const [k, 群] of slug別) {
|
|
4640
4996
|
if (群.length > 1) {
|
|
4641
|
-
伝える(
|
|
4997
|
+
伝える(
|
|
4998
|
+
群[0]!,
|
|
4999
|
+
`type: tree で ${群.join(" / ")} が同じ id (${k}) になります。 名前を変えてください`,
|
|
5000
|
+
);
|
|
4642
5001
|
}
|
|
4643
5002
|
}
|
|
4644
5003
|
const 親 = 矢印から親を決める(doc, "tree", 名前, 伝える);
|
|
@@ -4650,7 +5009,14 @@ function compileTree(doc: DslDocument, onNotice?: (n: CompileNotice) => void): C
|
|
|
4650
5009
|
return { id, ...放射に出す文字(a), ...(p3 !== undefined ? { parent: p3 } : {}) };
|
|
4651
5010
|
});
|
|
4652
5011
|
b.node(`${slugify(doc.title) || "tree"}-chart`, {
|
|
4653
|
-
lane: "chart",
|
|
5012
|
+
lane: "chart",
|
|
5013
|
+
stack: 0,
|
|
5014
|
+
kind: "tree-hierarchy",
|
|
5015
|
+
title: doc.title,
|
|
5016
|
+
...図の小見出し(doc),
|
|
5017
|
+
w: W,
|
|
5018
|
+
h: H,
|
|
5019
|
+
treeData: data,
|
|
4654
5020
|
});
|
|
4655
5021
|
return b.build();
|
|
4656
5022
|
}
|
|
@@ -4677,19 +5043,25 @@ function 道筋の欄(a: DslActor): { touchpoint?: string; opportunity?: string
|
|
|
4677
5043
|
* `type: mind` では伝えない = `compileMind` が描けない欄をまとめて 1 件で伝えており、
|
|
4678
5044
|
* そこに 2 つとも入っている (`放射で描けない欄`)。 二重に伝えない (#1246 と同じ扱い)。
|
|
4679
5045
|
*/
|
|
4680
|
-
function reportChartFieldsNotHonored(
|
|
5046
|
+
function reportChartFieldsNotHonored(
|
|
5047
|
+
doc: DslDocument,
|
|
5048
|
+
onNotice?: (n: CompileNotice) => void,
|
|
5049
|
+
): void {
|
|
4681
5050
|
if (!onNotice) return;
|
|
4682
5051
|
if (doc.type === "mind") return;
|
|
4683
5052
|
for (const a of doc.actors) {
|
|
4684
5053
|
if (a.partId !== undefined) continue;
|
|
4685
|
-
const 道筋 =
|
|
4686
|
-
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
|
|
4690
|
-
|
|
4691
|
-
|
|
4692
|
-
|
|
5054
|
+
const 道筋 =
|
|
5055
|
+
doc.type === "journey"
|
|
5056
|
+
? []
|
|
5057
|
+
: [
|
|
5058
|
+
...(a.touchpoint !== undefined ? ["touchpoint"] : []),
|
|
5059
|
+
...(a.opportunity !== undefined ? ["opportunity"] : []),
|
|
5060
|
+
];
|
|
5061
|
+
const 工程 =
|
|
5062
|
+
doc.type === "gantt"
|
|
5063
|
+
? []
|
|
5064
|
+
: [...(a.owner !== undefined ? ["owner"] : []), ...(a.end !== undefined ? ["end"] : [])];
|
|
4693
5065
|
if (道筋.length > 0) {
|
|
4694
5066
|
onNotice({
|
|
4695
5067
|
kind: "chart-value-unreadable",
|
|
@@ -4727,7 +5099,7 @@ function compileJourney(doc: DslDocument, onNotice?: (n: CompileNotice) => void)
|
|
|
4727
5099
|
読めない.push(a.name);
|
|
4728
5100
|
continue;
|
|
4729
5101
|
}
|
|
4730
|
-
data.push({ id: slugify(a.name), title: a
|
|
5102
|
+
data.push({ id: slugify(a.name), title: 箱の題(a), emotion: 語 as never, ...道筋の欄(a) });
|
|
4731
5103
|
continue;
|
|
4732
5104
|
}
|
|
4733
5105
|
const e = 気持ち.get(語);
|
|
@@ -4735,7 +5107,7 @@ function compileJourney(doc: DslDocument, onNotice?: (n: CompileNotice) => void)
|
|
|
4735
5107
|
読めない.push(a.name);
|
|
4736
5108
|
continue;
|
|
4737
5109
|
}
|
|
4738
|
-
data.push({ id: slugify(a.name), title: a
|
|
5110
|
+
data.push({ id: slugify(a.name), title: 箱の題(a), emotion: e, ...道筋の欄(a) });
|
|
4739
5111
|
}
|
|
4740
5112
|
if (読めない.length > 0) {
|
|
4741
5113
|
const m = `type: journey で気持ちを読めない項目があります (道筋に載せません): ${読めない.join(", ")}。 \`- 登録: "不満"\` の形で、 ${[...気持ち.keys()].join(" / ")} のどれかを書いてください`;
|
|
@@ -4745,11 +5117,23 @@ function compileJourney(doc: DslDocument, onNotice?: (n: CompileNotice) => void)
|
|
|
4745
5117
|
// 矢印は描けない。 書かれていたら伝える (黙って捨てると「書いたのに効かない」 が残る)
|
|
4746
5118
|
if (doc.flow.length > 0) {
|
|
4747
5119
|
const m2 = `type: journey では矢印を描けません (${doc.flow.length} 本を無視しました)。 関係を描くなら type: flow を使ってください`;
|
|
4748
|
-
onNotice?.({
|
|
5120
|
+
onNotice?.({
|
|
5121
|
+
kind: "chart-edge-dropped",
|
|
5122
|
+
actor: doc.flow[0]?.from ?? "",
|
|
5123
|
+
line: doc.flow[0]?.pos?.line ?? 0,
|
|
5124
|
+
message: m2,
|
|
5125
|
+
});
|
|
4749
5126
|
if (typeof console !== "undefined" && console.warn) console.warn(`[dragon] ${m2}`);
|
|
4750
5127
|
}
|
|
4751
5128
|
b.node(`${slugify(doc.title) || "journey"}-chart`, {
|
|
4752
|
-
lane: "chart",
|
|
5129
|
+
lane: "chart",
|
|
5130
|
+
stack: 0,
|
|
5131
|
+
kind: "journey-map",
|
|
5132
|
+
title: doc.title,
|
|
5133
|
+
...図の小見出し(doc),
|
|
5134
|
+
w: W,
|
|
5135
|
+
h: H,
|
|
5136
|
+
journeyData: data,
|
|
4753
5137
|
});
|
|
4754
5138
|
return b.build();
|
|
4755
5139
|
}
|
|
@@ -4828,7 +5212,7 @@ function compileQuadrant(doc: DslDocument, onNotice?: (n: CompileNotice) => void
|
|
|
4828
5212
|
読めない.push(a.name);
|
|
4829
5213
|
continue;
|
|
4830
5214
|
}
|
|
4831
|
-
items.push({ id: slugify(a.name), title: a
|
|
5215
|
+
items.push({ id: slugify(a.name), title: 箱の題(a), quadrant: 語 as never });
|
|
4832
5216
|
continue;
|
|
4833
5217
|
}
|
|
4834
5218
|
const q = 区画.get(語);
|
|
@@ -4836,7 +5220,7 @@ function compileQuadrant(doc: DslDocument, onNotice?: (n: CompileNotice) => void
|
|
|
4836
5220
|
読めない.push(a.name);
|
|
4837
5221
|
continue;
|
|
4838
5222
|
}
|
|
4839
|
-
items.push({ id: slugify(a.name), title: a
|
|
5223
|
+
items.push({ id: slugify(a.name), title: 箱の題(a), quadrant: q });
|
|
4840
5224
|
}
|
|
4841
5225
|
if (読めない.length > 0) {
|
|
4842
5226
|
const m = `type: quadrant で区画を読めない項目があります (図に載せません): ${読めない.join(", ")}。 \`- 重複削除: "左上"\` の形で、 ${[...区画.keys()].join(" / ")} のどれかを書いてください`;
|
|
@@ -4846,11 +5230,22 @@ function compileQuadrant(doc: DslDocument, onNotice?: (n: CompileNotice) => void
|
|
|
4846
5230
|
// 矢印は描けない。 書かれていたら伝える (黙って捨てると「書いたのに効かない」 が残る)
|
|
4847
5231
|
if (doc.flow.length > 0) {
|
|
4848
5232
|
const m2 = `type: quadrant では矢印を描けません (${doc.flow.length} 本を無視しました)。 関係を描くなら type: flow を使ってください`;
|
|
4849
|
-
onNotice?.({
|
|
5233
|
+
onNotice?.({
|
|
5234
|
+
kind: "chart-edge-dropped",
|
|
5235
|
+
actor: doc.flow[0]?.from ?? "",
|
|
5236
|
+
line: doc.flow[0]?.pos?.line ?? 0,
|
|
5237
|
+
message: m2,
|
|
5238
|
+
});
|
|
4850
5239
|
if (typeof console !== "undefined" && console.warn) console.warn(`[dragon] ${m2}`);
|
|
4851
5240
|
}
|
|
4852
5241
|
b.node(`${slugify(doc.title) || "quadrant"}-chart`, {
|
|
4853
|
-
lane: "chart",
|
|
5242
|
+
lane: "chart",
|
|
5243
|
+
stack: 0,
|
|
5244
|
+
kind: "quadrant-matrix",
|
|
5245
|
+
title: doc.title,
|
|
5246
|
+
...図の小見出し(doc),
|
|
5247
|
+
w: W,
|
|
5248
|
+
h: H,
|
|
4854
5249
|
quadrantData: { ...軸と区画の名前(doc), items },
|
|
4855
5250
|
});
|
|
4856
5251
|
return b.build();
|
|
@@ -4879,7 +5274,10 @@ function 段を読み取る(subtitle: string | undefined): { 段: number; 説明
|
|
|
4879
5274
|
const m = 元.match(/^L([123])(?![0-9A-Za-z])/i);
|
|
4880
5275
|
if (m === null) return { 段: 1, 説明: subtitle };
|
|
4881
5276
|
// 目印と、 その直後の区切り (`:` / 全角コロン / 空白) を落とす
|
|
4882
|
-
const 残り =
|
|
5277
|
+
const 残り = 元
|
|
5278
|
+
.slice(m[0].length)
|
|
5279
|
+
.replace(/^[::\s]+/, "")
|
|
5280
|
+
.trim();
|
|
4883
5281
|
return { 段: Number(m[1]), 説明: 残り === "" ? undefined : 残り };
|
|
4884
5282
|
}
|
|
4885
5283
|
|
|
@@ -4943,6 +5341,7 @@ function compileC4(doc: DslDocument): CdlDiagram {
|
|
|
4943
5341
|
b.edge(fromId, toId, {
|
|
4944
5342
|
label: s.label,
|
|
4945
5343
|
...(s.sub ? { sub: s.sub } : {}),
|
|
5344
|
+
...(s.side ? { side: s.side } : {}),
|
|
4946
5345
|
...(s.tone ? { tone: s.tone } : {}),
|
|
4947
5346
|
...(s.style ? { style: s.style } : {}),
|
|
4948
5347
|
});
|
|
@@ -4998,6 +5397,8 @@ function compileC4(doc: DslDocument): CdlDiagram {
|
|
|
4998
5397
|
type 放射で描ける欄 =
|
|
4999
5398
|
/** 中心の名前 / 枝の名前になる */
|
|
5000
5399
|
| "name"
|
|
5400
|
+
/** 名前と分けて中心 / 枝に出す題。 書かなければ名前を出す */
|
|
5401
|
+
| "title"
|
|
5001
5402
|
/**
|
|
5002
5403
|
* 名前とは分けて補足に出す 2 欄。
|
|
5003
5404
|
*
|
|
@@ -5018,6 +5419,16 @@ type 放射で描ける欄 =
|
|
|
5018
5419
|
/** 本文の行番号。 知らせに載せるために使う */
|
|
5019
5420
|
| "pos";
|
|
5020
5421
|
|
|
5422
|
+
/**
|
|
5423
|
+
* 箱に出す題 (#1381)。 書いていなければ名前をそのまま使う。
|
|
5424
|
+
*
|
|
5425
|
+
* 名前は図の中で 1 つに決まる必要がある (`focus:` と `flow:` が名前で指す) 一方、題は
|
|
5426
|
+
* 重なってよい。 同じ題の箱を並べる図と、題を持たない箱は、名前と切り離さないと書けない。
|
|
5427
|
+
*/
|
|
5428
|
+
function 箱の題(a: DslActor): string {
|
|
5429
|
+
return a.title ?? a.name;
|
|
5430
|
+
}
|
|
5431
|
+
|
|
5021
5432
|
/** 放射では描けない欄。 書かれていたら伝える */
|
|
5022
5433
|
type 放射で描けない欄 =
|
|
5023
5434
|
| "kind"
|
|
@@ -5041,7 +5452,17 @@ type 放射で描けない欄 =
|
|
|
5041
5452
|
| "scaleKeys"
|
|
5042
5453
|
| "posRel"
|
|
5043
5454
|
| "nodes"
|
|
5044
|
-
| "layoutPos"
|
|
5455
|
+
| "layoutPos"
|
|
5456
|
+
// 箱の中に描く図形 (#1374)。 放射の枝は箱の中に図形を持たない
|
|
5457
|
+
| "shape"
|
|
5458
|
+
// 出す条件 (#1381)。 放射の枝は個別に出し分けられない
|
|
5459
|
+
| "visibleIf"
|
|
5460
|
+
// 値に追随する 5 欄 (#1392)。 放射の枝は大きさも位置も中心からの配置で決まる
|
|
5461
|
+
| "wBind"
|
|
5462
|
+
| "hBind"
|
|
5463
|
+
| "opacity"
|
|
5464
|
+
| "renderOffsetX"
|
|
5465
|
+
| "renderOffsetY";
|
|
5045
5466
|
|
|
5046
5467
|
/** 引数が `never` でなければ型検査が落ちる */
|
|
5047
5468
|
type 空であること<T extends never> = T;
|
|
@@ -5096,6 +5517,13 @@ const 放射で描けない欄の名前: Record<放射で描けない欄, string
|
|
|
5096
5517
|
posRel: "位置 (相対)",
|
|
5097
5518
|
nodes: "中の箱ごとの指定",
|
|
5098
5519
|
layoutPos: "配置のずらし",
|
|
5520
|
+
shape: "箱の中の図形",
|
|
5521
|
+
visibleIf: "出す条件",
|
|
5522
|
+
wBind: "値に追随する大きさ",
|
|
5523
|
+
hBind: "値に追随する大きさ",
|
|
5524
|
+
opacity: "濃さ",
|
|
5525
|
+
renderOffsetX: "描く時のずらし",
|
|
5526
|
+
renderOffsetY: "描く時のずらし",
|
|
5099
5527
|
};
|
|
5100
5528
|
|
|
5101
5529
|
/**
|
|
@@ -5126,7 +5554,7 @@ function 放射で描けない欄を書いたか(a: DslActor, 欄: 放射で描
|
|
|
5126
5554
|
*/
|
|
5127
5555
|
function 放射に出す文字(a: DslActor): { title: string; subtitle?: string } {
|
|
5128
5556
|
const 続き = [a.subtitle, a.value].map((x) => x?.trim()).filter((x): x is string => !!x);
|
|
5129
|
-
return { title: a
|
|
5557
|
+
return { title: 箱の題(a), ...(続き.length > 0 ? { subtitle: 続き.join(" ") } : {}) };
|
|
5130
5558
|
}
|
|
5131
5559
|
|
|
5132
5560
|
function compileMind(doc: DslDocument, onNotice?: (n: CompileNotice) => void): CdlDiagram {
|
|
@@ -5226,7 +5654,8 @@ function compileMind(doc: DslDocument, onNotice?: (n: CompileNotice) => void): C
|
|
|
5226
5654
|
const 使った = new Set<string>([rootId]);
|
|
5227
5655
|
記録する(root);
|
|
5228
5656
|
// 中心は色の欄を持たない (`MindBranchPayload` に `tone` が無い)
|
|
5229
|
-
if (root.tone)
|
|
5657
|
+
if (root.tone)
|
|
5658
|
+
消えた欄.set(root.name, [...(消えた欄.get(root.name) ?? []), "色 (中心は持てない)"]);
|
|
5230
5659
|
|
|
5231
5660
|
見本でない.slice(1).forEach((a, i) => {
|
|
5232
5661
|
const id = slugify(a.name) || `leaf-${i}`;
|
|
@@ -5304,10 +5733,7 @@ function compileMind(doc: DslDocument, onNotice?: (n: CompileNotice) => void): C
|
|
|
5304
5733
|
* 後から来た見本の縦列に書いた値を移し、先に作った空の方を外す。 書いた人から見れば
|
|
5305
5734
|
* 「id を書けば効く」 が成り立つ。
|
|
5306
5735
|
*/
|
|
5307
|
-
function mergeDuplicateDeclaredLanes(
|
|
5308
|
-
diagram: CdlDiagram,
|
|
5309
|
-
追加した縦列: readonly DslLane[],
|
|
5310
|
-
): void {
|
|
5736
|
+
function mergeDuplicateDeclaredLanes(diagram: CdlDiagram, 追加した縦列: readonly DslLane[]): void {
|
|
5311
5737
|
for (const 宣言 of 追加した縦列) {
|
|
5312
5738
|
const { id } = 宣言;
|
|
5313
5739
|
const 同一idの縦列 = diagram.lanes.filter((l) => l.id === id);
|
|
@@ -5399,14 +5825,31 @@ function applyV05Extensions(
|
|
|
5399
5825
|
// `s{idx}-{laneId}` が actor 名末尾 "Header" (slug `...-header`) で誤マッチし、 option が invisible
|
|
5400
5826
|
// な step anchor にも copy される (cc-codex #883 MAJOR)。 lane id との構造 exact 一致で header だけを
|
|
5401
5827
|
// 引くことで step box / spacer / footer を排除する。
|
|
5402
|
-
primaryNodes =
|
|
5403
|
-
|
|
5404
|
-
|
|
5828
|
+
primaryNodes =
|
|
5829
|
+
ownedLaneIds.size > 0
|
|
5830
|
+
? diagram.nodes.filter((n) => ownedLaneIds.has(n.lane) && n.id === `${n.lane}-header`)
|
|
5831
|
+
: diagram.nodes.filter((n) => n.id === `${dragonSlug}-header`);
|
|
5405
5832
|
} else {
|
|
5406
5833
|
// 非 seq preset は 1 actor = 1 node (id = dragon slug) で node id と dragon slug が一致する。
|
|
5407
5834
|
primaryNodes = diagram.nodes.filter((n) => n.id === dragonSlug);
|
|
5408
5835
|
}
|
|
5409
5836
|
for (const node of primaryNodes) {
|
|
5837
|
+
// 識別に使う名前と、箱に出す題を分ける (#1381)。 preset が actor 名で
|
|
5838
|
+
// node を作る経路 (sequence / solidity / swimlane / c4) もここで書き換える。
|
|
5839
|
+
if (a.title !== undefined) {
|
|
5840
|
+
node.title = a.title;
|
|
5841
|
+
if (isSeqLike) {
|
|
5842
|
+
const footer = diagram.nodes.find((n) => n.id === `${node.lane}-footer`);
|
|
5843
|
+
if (footer) footer.title = a.title;
|
|
5844
|
+
|
|
5845
|
+
// 名前で決めた preset の幅を題に合わせる。 明示の大きさは後段が優先する。
|
|
5846
|
+
if (a.posW === undefined) {
|
|
5847
|
+
const titleW = Math.max(140, a.title.length * 22 + 52);
|
|
5848
|
+
node.w = titleW;
|
|
5849
|
+
if (footer) footer.w = titleW;
|
|
5850
|
+
}
|
|
5851
|
+
}
|
|
5852
|
+
}
|
|
5410
5853
|
// `type: c4` では説明の先頭に段の目印 (`L1` / `L2` / `L3`) を書く。 目印は組み立てに
|
|
5411
5854
|
// 段を伝えるためのもので読む人に意味を持たず、 段の名前は枠のラベルが既に出している。
|
|
5412
5855
|
// ここで落とさないと、 組み立てが読み取った目印がそのまま箱の説明として出る (#1098)
|
|
@@ -5482,6 +5925,42 @@ function applyV05Extensions(
|
|
|
5482
5925
|
const footer = diagram.nodes.find((n) => n.id === `${node.lane}-footer`);
|
|
5483
5926
|
if (footer && a.posW !== undefined) footer.w = a.posW;
|
|
5484
5927
|
}
|
|
5928
|
+
// 箱の中に描く図形 (#1374)。 renderer が shape を描くのは dyn-* kind だけなので、
|
|
5929
|
+
// shape 自身を SSOT にして対応する kind へ揃える。 card 等のまま shape だけ渡すと、指定を
|
|
5930
|
+
// 保持しているのに画面には何も出ない。 paint 検査が入力を mutate しないよう object も写す。
|
|
5931
|
+
if (a.shape !== undefined) {
|
|
5932
|
+
const dynamicKind = `dyn-${a.shape.kind}` as typeof node.kind;
|
|
5933
|
+
node.kind = dynamicKind;
|
|
5934
|
+
node.shape = { ...a.shape };
|
|
5935
|
+
if (isSeqLike) {
|
|
5936
|
+
const footer = diagram.nodes.find((n) => n.id === `${node.lane}-footer`);
|
|
5937
|
+
if (footer) {
|
|
5938
|
+
footer.kind = dynamicKind;
|
|
5939
|
+
footer.shape = { ...a.shape };
|
|
5940
|
+
}
|
|
5941
|
+
}
|
|
5942
|
+
}
|
|
5943
|
+
// その箱を出すかどうかの条件 (#1381)。 名札と足にも同じ条件を渡す = 片方だけ隠すと
|
|
5944
|
+
// 順序図で縦線の頭と足が食い違う
|
|
5945
|
+
if (a.visibleIf !== undefined) {
|
|
5946
|
+
node.visibleIf = a.visibleIf;
|
|
5947
|
+
if (isSeqLike) {
|
|
5948
|
+
const footer = diagram.nodes.find((n) => n.id === `${node.lane}-footer`);
|
|
5949
|
+
if (footer) footer.visibleIf = a.visibleIf;
|
|
5950
|
+
}
|
|
5951
|
+
}
|
|
5952
|
+
/*
|
|
5953
|
+
* 値に追随する 5 欄 (#1392)。
|
|
5954
|
+
*
|
|
5955
|
+
* **名札と足へは渡さない**。 `visibleIf` は片方だけ隠すと順序図の縦線の頭と足が
|
|
5956
|
+
* 食い違うため揃えるが、こちらは見た目の大きさ / 濃さ / ずらしで、名札まで同じだけ
|
|
5957
|
+
* 動かすと縦線の頭が本体から離れる。 書いた箱にだけ効かせる。
|
|
5958
|
+
*/
|
|
5959
|
+
if (a.wBind !== undefined) node.wBind = a.wBind;
|
|
5960
|
+
if (a.hBind !== undefined) node.hBind = a.hBind;
|
|
5961
|
+
if (a.opacity !== undefined) node.opacity = a.opacity;
|
|
5962
|
+
if (a.renderOffsetX !== undefined) node.renderOffsetX = a.renderOffsetX;
|
|
5963
|
+
if (a.renderOffsetY !== undefined) node.renderOffsetY = a.renderOffsetY;
|
|
5485
5964
|
}
|
|
5486
5965
|
}
|
|
5487
5966
|
// 名札の高さを揃える。 kind ごとに高さが変わると縦線の始まる位置がばらけ、 順序図の
|
|
@@ -5599,7 +6078,12 @@ function injectPhasesFallback(diagram: CdlDiagram, doc: DslDocument): void {
|
|
|
5599
6078
|
}
|
|
5600
6079
|
// 線を持たない種類 (帯の依存等) では矢印が edge にならない。 両端が実在するなら
|
|
5601
6080
|
// その箱を光らせる = 矢印を指した段で何も光らないより意図に近い (#1077)
|
|
5602
|
-
if (
|
|
6081
|
+
if (
|
|
6082
|
+
!見つかった &&
|
|
6083
|
+
singleBoxNode !== undefined &&
|
|
6084
|
+
knownNames.has(entry.from) &&
|
|
6085
|
+
knownNames.has(entry.to)
|
|
6086
|
+
) {
|
|
5603
6087
|
out.push(singleBoxNode.id);
|
|
5604
6088
|
}
|
|
5605
6089
|
continue;
|
|
@@ -5629,9 +6113,7 @@ function injectPhasesFallback(diagram: CdlDiagram, doc: DslDocument): void {
|
|
|
5629
6113
|
// **`activate` と兼ねない**。 描画側は焦点と別集合で持つ (`cdl#512`) = 焦点が当たり
|
|
5630
6114
|
// 続ける図で毎段引き直しになるため。 書いた段だけが欄を持つ
|
|
5631
6115
|
const drawIds =
|
|
5632
|
-
p.draw !== undefined &&
|
|
5633
|
-
DRAW_TARGETS.get(p.draw) === doc.type &&
|
|
5634
|
-
singleBoxNode !== undefined
|
|
6116
|
+
p.draw !== undefined && DRAW_TARGETS.get(p.draw) === doc.type && singleBoxNode !== undefined
|
|
5635
6117
|
? [singleBoxNode.id]
|
|
5636
6118
|
: [];
|
|
5637
6119
|
diagram.phases.push({
|
|
@@ -5666,6 +6148,7 @@ function compileSequence(doc: DslDocument): CdlDiagram {
|
|
|
5666
6148
|
to: s.to,
|
|
5667
6149
|
label: s.label,
|
|
5668
6150
|
...(s.sub ? { sub: s.sub } : {}),
|
|
6151
|
+
...(s.side ? { side: s.side } : {}),
|
|
5669
6152
|
...(s.tone ? { tone: s.tone } : {}),
|
|
5670
6153
|
...(s.style ? { style: s.style } : {}),
|
|
5671
6154
|
});
|
|
@@ -5705,8 +6188,8 @@ function compileSequenceWithAnimate(doc: DslDocument): CdlDiagram {
|
|
|
5705
6188
|
const headerId = `${id}-header`;
|
|
5706
6189
|
// header/footer 幅を title 長に応じて auto-size (text-readability warning 解消)。
|
|
5707
6190
|
// formula = 22px/char + 52px padding (visualValidate text-readability と完全一致)、 min 140 で従来 sample 互換維持。
|
|
5708
|
-
const actorW = Math.max(140, a.
|
|
5709
|
-
b.node(headerId, { lane: id, stack: 0, kind: "card", title: a
|
|
6191
|
+
const actorW = Math.max(140, 箱の題(a).length * 22 + 52);
|
|
6192
|
+
b.node(headerId, { lane: id, stack: 0, kind: "card", title: 箱の題(a), w: actorW, h: 72 });
|
|
5710
6193
|
headerNodeIds.push(headerId);
|
|
5711
6194
|
const spacerId = `${id}-spacer`;
|
|
5712
6195
|
b.node(spacerId, { lane: id, stack: 1, kind: "card", title: "", w: 2, h: 40 });
|
|
@@ -5733,6 +6216,7 @@ function compileSequenceWithAnimate(doc: DslDocument): CdlDiagram {
|
|
|
5733
6216
|
id: edgeId,
|
|
5734
6217
|
label: s.label,
|
|
5735
6218
|
...(s.sub ? { sub: s.sub } : {}),
|
|
6219
|
+
...(s.side ? { side: s.side } : {}),
|
|
5736
6220
|
...(s.tone ? { tone: s.tone } : {}),
|
|
5737
6221
|
...(s.style ? { style: s.style } : {}),
|
|
5738
6222
|
});
|
|
@@ -5744,7 +6228,7 @@ function compileSequenceWithAnimate(doc: DslDocument): CdlDiagram {
|
|
|
5744
6228
|
doc.actors.forEach((a) => {
|
|
5745
6229
|
const laneId = actorIds.get(a.name) ?? slugify(a.name);
|
|
5746
6230
|
const footerId = `${laneId}-footer`;
|
|
5747
|
-
const actorW = Math.max(140, a.
|
|
6231
|
+
const actorW = Math.max(140, 箱の題(a).length * 22 + 52);
|
|
5748
6232
|
// `role` を付ける (#1273)。 付けないと生命線の終わりが footer より 100 下まで伸びる
|
|
5749
6233
|
// (実測 = 組立て API は `y2=848`、記法は `y2=948`)。 枠の大きさは同じなので
|
|
5750
6234
|
// 描いた図の大きさの比較では捕まらない
|
|
@@ -5752,7 +6236,7 @@ function compileSequenceWithAnimate(doc: DslDocument): CdlDiagram {
|
|
|
5752
6236
|
lane: laneId,
|
|
5753
6237
|
stack: footerStack,
|
|
5754
6238
|
kind: "card",
|
|
5755
|
-
title: a
|
|
6239
|
+
title: 箱の題(a),
|
|
5756
6240
|
w: actorW,
|
|
5757
6241
|
h: 72,
|
|
5758
6242
|
role: "lifeline-footer",
|
|
@@ -5901,7 +6385,7 @@ function compileFlow(doc: DslDocument): CdlDiagram {
|
|
|
5901
6385
|
{
|
|
5902
6386
|
id: slugify(a.name) || `n${i}`,
|
|
5903
6387
|
kind: a.kind,
|
|
5904
|
-
title: a
|
|
6388
|
+
title: 箱の題(a),
|
|
5905
6389
|
},
|
|
5906
6390
|
edgeLabel,
|
|
5907
6391
|
);
|
|
@@ -5951,6 +6435,7 @@ function compileSwimlane(doc: DslDocument): CdlDiagram {
|
|
|
5951
6435
|
id: `e${edgeIdx++}-${fromId}-${toId}`,
|
|
5952
6436
|
label: s.label,
|
|
5953
6437
|
...(s.sub ? { sub: s.sub } : {}),
|
|
6438
|
+
...(s.side ? { side: s.side } : {}),
|
|
5954
6439
|
...(s.tone ? { tone: s.tone } : {}),
|
|
5955
6440
|
...(s.style ? { style: s.style } : {}),
|
|
5956
6441
|
});
|
|
@@ -5970,7 +6455,7 @@ function compileSwimlane(doc: DslDocument): CdlDiagram {
|
|
|
5970
6455
|
lane: swim.laneId(a.name),
|
|
5971
6456
|
stack: 0,
|
|
5972
6457
|
kind: a.kind ?? "actor",
|
|
5973
|
-
title: a
|
|
6458
|
+
title: 箱の題(a),
|
|
5974
6459
|
});
|
|
5975
6460
|
});
|
|
5976
6461
|
}
|
|
@@ -5995,7 +6480,7 @@ function compileEr(doc: DslDocument): CdlDiagram {
|
|
|
5995
6480
|
// v0.3 で「列定義」 ブロックを追加検討
|
|
5996
6481
|
erBuilder.entity({
|
|
5997
6482
|
id: slugify(a.name) || a.name,
|
|
5998
|
-
title: a
|
|
6483
|
+
title: 箱の題(a),
|
|
5999
6484
|
rows: [], // v0.2 では rows なし
|
|
6000
6485
|
});
|
|
6001
6486
|
}
|
|
@@ -6052,7 +6537,7 @@ function compileState(doc: DslDocument): CdlDiagram {
|
|
|
6052
6537
|
const final = 決め方.終わり(a, i);
|
|
6053
6538
|
fsm.state({
|
|
6054
6539
|
id: slugify(a.name) || `s${i}`,
|
|
6055
|
-
title: a
|
|
6540
|
+
title: 箱の題(a),
|
|
6056
6541
|
...(initial ? { initial: true } : {}),
|
|
6057
6542
|
...(final ? { final: true } : {}),
|
|
6058
6543
|
});
|
|
@@ -6093,13 +6578,14 @@ function compileTopology(doc: DslDocument): CdlDiagram {
|
|
|
6093
6578
|
groupBuilder.add({
|
|
6094
6579
|
id: slugify(a.name) || a.name,
|
|
6095
6580
|
kind: a.kind,
|
|
6096
|
-
title: a
|
|
6581
|
+
title: 箱の題(a),
|
|
6097
6582
|
});
|
|
6098
6583
|
}
|
|
6099
6584
|
for (const s of doc.flow) {
|
|
6100
6585
|
topo.connect(slugify(s.from), slugify(s.to), {
|
|
6101
6586
|
label: s.label,
|
|
6102
6587
|
...(s.sub ? { sub: s.sub } : {}),
|
|
6588
|
+
...(s.side ? { side: s.side } : {}),
|
|
6103
6589
|
...(s.tone ? { tone: s.tone } : {}),
|
|
6104
6590
|
...(s.style ? { style: s.style } : {}),
|
|
6105
6591
|
});
|
|
@@ -6179,33 +6665,72 @@ function compileGenericWithAnimate(doc: DslDocument, opts: GenericOpts): CdlDiag
|
|
|
6179
6665
|
// lane / node 配置 ... preset kind に応じて切替
|
|
6180
6666
|
const actorToNodeId = new Map<string, string>();
|
|
6181
6667
|
if (書いた縦列に置く(kind, doc)) {
|
|
6182
|
-
|
|
6183
|
-
|
|
6184
|
-
|
|
6668
|
+
/*
|
|
6669
|
+
* **書いた縦列に置く** (#1263)。 縦列を並べるための入れ物として使う図種でだけ効く。
|
|
6670
|
+
*
|
|
6671
|
+
* 縦列の並びは **`lanes:` に書いた順** を優先する (#1394)。 箱が最初に使った順で
|
|
6672
|
+
* 並べていた間、`lanes:` で左から順に宣言しても箱の書き順で入れ替わっていた
|
|
6673
|
+
* (実測 = 中心を先に書いた放射の図で、左端の縦列が中心の右へ回った)。
|
|
6674
|
+
*
|
|
6675
|
+
* `lanes:` に無い縦列は、これまでどおり箱が使った順で後ろに続ける。
|
|
6676
|
+
*/
|
|
6677
|
+
const 使った: string[] = [];
|
|
6185
6678
|
for (const a of doc.actors) {
|
|
6186
6679
|
const lid = a.lane;
|
|
6187
|
-
if (lid !== undefined &&
|
|
6680
|
+
if (lid !== undefined && !使った.includes(lid)) 使った.push(lid);
|
|
6188
6681
|
}
|
|
6682
|
+
const 書いた順 = doc.lanes ? Object.keys(doc.lanes) : [];
|
|
6683
|
+
const 並び = [
|
|
6684
|
+
...書いた順.filter((lid) => 使った.includes(lid)),
|
|
6685
|
+
...使った.filter((lid) => !書いた順.includes(lid)),
|
|
6686
|
+
];
|
|
6189
6687
|
for (const lid of 並び) {
|
|
6190
6688
|
b.lane(lid, { width: laneWidth, ...(kind === "topology" ? { contain: true } : {}) });
|
|
6191
6689
|
}
|
|
6192
|
-
|
|
6690
|
+
/*
|
|
6691
|
+
* 段は **書いた番号をそのまま持つ** (#1394)。
|
|
6692
|
+
*
|
|
6693
|
+
* 書き順で 0 から詰め直していた間、`stack: 1` と書いた箱が 0 へ落ちていた。
|
|
6694
|
+
* 決定木のように「同じ高さに並ばない」 ことが図の意味そのものになる形では、
|
|
6695
|
+
* 詰めた瞬間に別の図になる (実測 = 3 段の木の根が 1 段目へ上がった)。
|
|
6696
|
+
*
|
|
6697
|
+
* 書かなかった箱は、その縦列で **空いている一番小さい段** に置く。 単に数え上げると
|
|
6698
|
+
* 書いた番号と重なり、2 つの箱が同じ段に載る。
|
|
6699
|
+
*/
|
|
6700
|
+
const 埋まった段 = new Map<string, Set<number>>();
|
|
6701
|
+
const 埋める = (lid: string, stack: number): void => {
|
|
6702
|
+
const 集合 = 埋まった段.get(lid) ?? new Set<number>();
|
|
6703
|
+
集合.add(stack);
|
|
6704
|
+
埋まった段.set(lid, 集合);
|
|
6705
|
+
};
|
|
6706
|
+
for (const a of doc.actors) {
|
|
6707
|
+
if (a.lane !== undefined && a.stack !== undefined) 埋める(a.lane, a.stack);
|
|
6708
|
+
}
|
|
6193
6709
|
doc.actors.forEach((a, idx) => {
|
|
6194
6710
|
const id = slugify(a.name) || `n${idx}`;
|
|
6195
6711
|
actorToNodeId.set(a.name, id);
|
|
6196
6712
|
const lid = a.lane!;
|
|
6197
|
-
|
|
6198
|
-
|
|
6199
|
-
|
|
6713
|
+
let stack = a.stack;
|
|
6714
|
+
if (stack === undefined) {
|
|
6715
|
+
stack = 0;
|
|
6716
|
+
const 集合 = 埋まった段.get(lid);
|
|
6717
|
+
while (集合?.has(stack)) stack += 1;
|
|
6718
|
+
埋める(lid, stack);
|
|
6719
|
+
}
|
|
6720
|
+
b.node(id, { lane: lid, stack, kind: a.kind, title: 箱の題(a) });
|
|
6200
6721
|
});
|
|
6201
6722
|
} else if (kind === "flow" || kind === "topology") {
|
|
6202
6723
|
// 1 lane に全 actor を縦 stack
|
|
6203
6724
|
const lid = opts.laneId ?? "main";
|
|
6204
|
-
b.lane(lid, {
|
|
6725
|
+
b.lane(lid, {
|
|
6726
|
+
width: laneWidth,
|
|
6727
|
+
label: doc.title,
|
|
6728
|
+
...(kind === "topology" ? { contain: true } : {}),
|
|
6729
|
+
});
|
|
6205
6730
|
doc.actors.forEach((a, idx) => {
|
|
6206
6731
|
const id = slugify(a.name) || `n${idx}`;
|
|
6207
6732
|
actorToNodeId.set(a.name, id);
|
|
6208
|
-
b.node(id, { lane: lid, stack: idx, kind: a.kind, title: a
|
|
6733
|
+
b.node(id, { lane: lid, stack: idx, kind: a.kind, title: 箱の題(a) });
|
|
6209
6734
|
});
|
|
6210
6735
|
} else {
|
|
6211
6736
|
// swimlane / er / state ... actor ごとに 1 lane (横並び)
|
|
@@ -6231,7 +6756,7 @@ function compileGenericWithAnimate(doc: DslDocument, opts: GenericOpts): CdlDiag
|
|
|
6231
6756
|
lane: lid,
|
|
6232
6757
|
stack: 0,
|
|
6233
6758
|
kind: a.kind,
|
|
6234
|
-
title: a
|
|
6759
|
+
title: 箱の題(a),
|
|
6235
6760
|
...(isInitial ? { eyebrow: "初期" } : {}),
|
|
6236
6761
|
...(isFinal ? { eyebrow: "最終" } : {}),
|
|
6237
6762
|
});
|
|
@@ -6264,8 +6789,11 @@ function compileGenericWithAnimate(doc: DslDocument, opts: GenericOpts): CdlDiag
|
|
|
6264
6789
|
b.edge(fromId, toId, {
|
|
6265
6790
|
id: edgeId,
|
|
6266
6791
|
label: labelWithCard,
|
|
6267
|
-
|
|
6792
|
+
...(後ろへ戻る矢印か(kind, fromId, toId, 箱の並び)
|
|
6793
|
+
? { routing: "back-detour" as const }
|
|
6794
|
+
: {}),
|
|
6268
6795
|
...(s.sub ? { sub: s.sub } : {}),
|
|
6796
|
+
...(s.side ? { side: s.side } : {}),
|
|
6269
6797
|
...(s.tone ? { tone: s.tone } : {}),
|
|
6270
6798
|
...(s.style ? { style: s.style } : {}),
|
|
6271
6799
|
});
|