@cardenelabs/dragon 0.30.0 → 0.33.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 +314 -12
- package/dist/index.cjs +6960 -1434
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2151 -115
- package/dist/index.d.ts +2151 -115
- package/dist/index.js +6944 -1436
- package/dist/index.js.map +1 -1
- package/package.json +12 -13
- package/src/compile/actor-ids.ts +6 -15
- package/src/compile/actor-option-notice.ts +379 -0
- package/src/compile/actors.ts +32 -3
- package/src/compile/c4.ts +0 -3
- package/src/compile/chain.ts +43 -4
- package/src/compile/chart-fields.ts +16 -1
- package/src/compile/direction.ts +50 -11
- package/src/compile/edge-option-notice.ts +112 -0
- package/src/compile/flowchart.ts +344 -0
- package/src/compile/funnel.ts +14 -2
- package/src/compile/gantt.ts +141 -36
- package/src/compile/generic.ts +473 -61
- package/src/compile/journey.ts +14 -5
- package/src/compile/kinds.ts +5 -13
- package/src/compile/lanes.ts +95 -13
- package/src/compile/metro.ts +271 -0
- package/src/compile/mind.ts +55 -5
- package/src/compile/notice.ts +51 -4
- package/src/compile/order.ts +62 -0
- package/src/compile/parts.ts +107 -33
- package/src/compile/placement-apply.ts +27 -11
- package/src/compile/quadrant.ts +171 -16
- package/src/compile/record.ts +75 -0
- package/src/compile/row-marks.ts +128 -0
- package/src/compile/sequence.ts +105 -26
- package/src/compile/slug.ts +9 -1
- package/src/compile/stages.ts +247 -0
- package/src/compile/subtitle.ts +15 -2
- package/src/compile/swimlane-marks.ts +87 -0
- package/src/compile/swimlane.ts +13 -14
- package/src/compile/timeline.ts +213 -0
- package/src/compile/topology.ts +3 -9
- package/src/compile/tree.ts +13 -4
- package/src/compile/value-chart.ts +179 -25
- package/src/compile.ts +1547 -370
- package/src/focus.ts +2 -2
- package/src/index.ts +34 -2
- package/src/json-parser.ts +1407 -48
- package/src/keywords.ts +255 -17
- package/src/notation-lint.ts +41 -64
- package/src/schemas/diagram.json +643 -20
- package/src/types.ts +336 -35
- package/src/v05/parser.ts +2451 -271
- package/src/compile/class.ts +0 -100
- package/src/compile/er.ts +0 -44
- package/src/compile/solidity.ts +0 -39
- package/src/compile/state.ts +0 -71
package/src/compile/generic.ts
CHANGED
|
@@ -1,27 +1,156 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
CLEARANCE_NODE_LABEL,
|
|
3
|
+
diagram,
|
|
4
|
+
LABEL_INIT_CLEARANCE,
|
|
5
|
+
LABEL_TO_PATH_CLEARANCE,
|
|
6
|
+
} from "@cardenelabs/cdl";
|
|
7
|
+
import type { CdlDiagram, CdlEdge, NodeKind, Tone } from "@cardenelabs/cdl";
|
|
3
8
|
import { parseFocusEntry } from "../focus";
|
|
4
|
-
import type { DslDocument, DslPhase } from "../types";
|
|
9
|
+
import type { DslDocument, DslPhase, DslStep } from "../types";
|
|
5
10
|
import { 始まりと終わりの決め方 } from "./actors";
|
|
6
11
|
import { 並べる向き, 後ろへ戻る矢印か, type GenericKind } from "./direction";
|
|
7
|
-
import { ERの関係の指定を作る, ERの関係の矢印 } from "./er-relation";
|
|
8
12
|
import { 描ける種別 } from "./kinds";
|
|
9
|
-
import { 書いた縦列に置く } from "./lanes";
|
|
13
|
+
import { 縦列ごとの段を決める, 書いた縦列に置く } from "./lanes";
|
|
14
|
+
import { metroMarkFrameSize, placeMetro } from "./metro";
|
|
10
15
|
import { 箱の題 } from "./node-title";
|
|
11
16
|
import { slugify } from "./slug";
|
|
17
|
+
import {
|
|
18
|
+
placeStages,
|
|
19
|
+
STAGE_CARD_COLUMN_INSET,
|
|
20
|
+
STAGE_CARD_HEIGHT,
|
|
21
|
+
STAGE_CARD_PITCH,
|
|
22
|
+
STAGE_CARD_WIDTH,
|
|
23
|
+
STAGE_FIRST_CARD_CENTER_Y,
|
|
24
|
+
STAGE_LANE_GAP,
|
|
25
|
+
STAGE_LANE_Y,
|
|
26
|
+
段の箱の線の指定,
|
|
27
|
+
type StagesPlacement,
|
|
28
|
+
} from "./stages";
|
|
29
|
+
import { 泳法図で印をどう描く } from "./swimlane-marks";
|
|
30
|
+
import {
|
|
31
|
+
classifyTimelineEdge,
|
|
32
|
+
placeTimeline,
|
|
33
|
+
TIMELINE_AXIS_WIDTH,
|
|
34
|
+
TIMELINE_CARD_HEIGHT,
|
|
35
|
+
TIMELINE_DECISION_HEIGHT,
|
|
36
|
+
TIMELINE_DECISION_WIDTH,
|
|
37
|
+
TIMELINE_END_HEIGHT,
|
|
38
|
+
TIMELINE_END_WIDTH,
|
|
39
|
+
TIMELINE_NUMBER_SIZE,
|
|
40
|
+
} from "./timeline";
|
|
41
|
+
|
|
42
|
+
/** 見本帳と共通の、時間軸の横分岐に置く札の寸法。 */
|
|
43
|
+
const 時間軸の横分岐の札の大きさ = { width: 96, height: 40, fontSize: 21 } as const;
|
|
44
|
+
|
|
45
|
+
/** 時間軸の横分岐の札の下端と線の間。 */
|
|
46
|
+
const 時間軸の横分岐の札と線の間 = 20;
|
|
47
|
+
|
|
48
|
+
/*
|
|
49
|
+
* 短い横線では札の横幅が両端の節の安全域に重なるため、cdl の routePath は札の中心を
|
|
50
|
+
* `線 - 節の半高 - 節との余白 - 標準札の半高` に置く。そこから見本の位置へ戻す量を渡す。
|
|
51
|
+
*/
|
|
52
|
+
const 時間軸の横分岐の札の下げ幅 =
|
|
53
|
+
TIMELINE_DECISION_HEIGHT / 2 +
|
|
54
|
+
CLEARANCE_NODE_LABEL +
|
|
55
|
+
(LABEL_INIT_CLEARANCE - LABEL_TO_PATH_CLEARANCE) -
|
|
56
|
+
(時間軸の横分岐の札の大きさ.height / 2 + 時間軸の横分岐の札と線の間);
|
|
57
|
+
|
|
12
58
|
/**
|
|
13
59
|
* 段を持つ図種の共通の組み立て (#2030 で `compile.ts` から移した)。
|
|
14
60
|
*
|
|
15
|
-
* `flow` / `swimlane` / `
|
|
61
|
+
* `flow` / `swimlane` / `record` / `topology` の 4 図種が通る。 `record` は段を書かない図も
|
|
62
|
+
* ここを通る (経路が 1 本なので、#2782)。
|
|
16
63
|
*/
|
|
17
64
|
|
|
18
65
|
export type GenericOpts = {
|
|
19
66
|
kind: GenericKind;
|
|
20
|
-
/** flow / topology は 1 lane に全 actor、 swimlane /
|
|
67
|
+
/** flow / topology は 1 lane に全 actor、 swimlane / record は actor ごと lane */
|
|
21
68
|
laneId?: string;
|
|
22
69
|
laneWidth: number;
|
|
70
|
+
/** 図種固有の関係を、共通の edge に渡す既定値へ直す。本文に明記した値は後段で勝つ。 */
|
|
71
|
+
edgeDefaults?: (step: DslStep) => Partial<CdlEdge>;
|
|
23
72
|
};
|
|
24
73
|
|
|
74
|
+
/**
|
|
75
|
+
* 分かれ道から続く線と駅の色を返す。
|
|
76
|
+
*
|
|
77
|
+
* 枝の意味を表す明示の欄は無いため、読み手が追う flow の記述順を使い、1 本目を「はい」、
|
|
78
|
+
* 2 本目以降を「いいえ」とする。枝の先も同じ結果の道筋なので色を引き継ぐ。
|
|
79
|
+
* 分かれ道より手前へ戻る線にも色は付けるが、そこで止める = 戻った本線まで枝色にしない。
|
|
80
|
+
*/
|
|
81
|
+
function metroBranchTones(doc: DslDocument): {
|
|
82
|
+
edgeToneByIndex: Map<number, Tone>;
|
|
83
|
+
nodeToneByName: Map<string, Tone>;
|
|
84
|
+
} {
|
|
85
|
+
const edgeToneByIndex = new Map<number, Tone>();
|
|
86
|
+
const nodeToneByName = new Map<string, Tone>();
|
|
87
|
+
const actorByName = new Map(doc.actors.map((actor) => [actor.name, actor]));
|
|
88
|
+
const actorIndex = new Map(doc.actors.map((actor, index) => [actor.name, index]));
|
|
89
|
+
const outgoing = new Map<string, number[]>();
|
|
90
|
+
doc.flow.forEach((step, index) => {
|
|
91
|
+
const indexes = outgoing.get(step.from) ?? [];
|
|
92
|
+
indexes.push(index);
|
|
93
|
+
outgoing.set(step.from, indexes);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
doc.actors.forEach((decision, decisionIndex) => {
|
|
97
|
+
if (decision.kind !== "decision") return;
|
|
98
|
+
const branches = outgoing.get(decision.name) ?? [];
|
|
99
|
+
branches.forEach((branchEdgeIndex, branchIndex) => {
|
|
100
|
+
const tone: Tone = branchIndex === 0 ? "success" : "error";
|
|
101
|
+
const follow = (edgeIndex: number): void => {
|
|
102
|
+
// 合流した道筋は、flow に先に書いた枝の色を保つ。
|
|
103
|
+
if (edgeToneByIndex.has(edgeIndex)) return;
|
|
104
|
+
edgeToneByIndex.set(edgeIndex, tone);
|
|
105
|
+
const targetName = doc.flow[edgeIndex]?.to;
|
|
106
|
+
if (targetName === undefined) return;
|
|
107
|
+
const target = actorByName.get(targetName);
|
|
108
|
+
const targetIndex = actorIndex.get(targetName);
|
|
109
|
+
if (target === undefined || targetIndex === undefined) return;
|
|
110
|
+
if (
|
|
111
|
+
targetIndex > decisionIndex &&
|
|
112
|
+
target.kind !== "decision" &&
|
|
113
|
+
target.kind !== "mark-start" &&
|
|
114
|
+
target.kind !== "mark-end" &&
|
|
115
|
+
!nodeToneByName.has(targetName)
|
|
116
|
+
) {
|
|
117
|
+
nodeToneByName.set(targetName, tone);
|
|
118
|
+
}
|
|
119
|
+
if (targetIndex <= decisionIndex || target.kind === "decision") return;
|
|
120
|
+
for (const nextEdgeIndex of outgoing.get(targetName) ?? []) follow(nextEdgeIndex);
|
|
121
|
+
};
|
|
122
|
+
follow(branchEdgeIndex);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
return { edgeToneByIndex, nodeToneByName };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* この共通の組み立てへ回すか (#2348)。
|
|
131
|
+
*
|
|
132
|
+
* **条件を 1 箇所に集める**。 図種ごとの入口が同じ条件を手で並べていた間、
|
|
133
|
+
* 「縦列を書いた形」 が 2 図種 (状態の図 / 表の図) で抜け落ちていた =
|
|
134
|
+
* 動きを書かない図では、書いた縦列に箱が入らず、代わりに箱ごとの縦列が作られ、
|
|
135
|
+
* 「どの箱も入らない縦列です」 という **事実と逆の知らせ** だけが出ていた。
|
|
136
|
+
* その 2 図種は #2782 で `record` に畳み、経路を 1 本にしたのでここを通らない。
|
|
137
|
+
*
|
|
138
|
+
* 2 つの条件はどちらも「静止図の経路が持っていない並べ方を書いた」 ことを意味する。
|
|
139
|
+
* 静止図の経路は並びを固定で持つため、通すと書いた指定が黙って消える (#1263)。
|
|
140
|
+
*
|
|
141
|
+
* `図種ごとの理由` はその図種にしか無い条件 (泳法図の向き) を渡す。
|
|
142
|
+
* フローは `鎖でつなぐ形か` が同じ判定を内側に持つため、ここは通らない。
|
|
143
|
+
*/
|
|
144
|
+
export function 共通の組み立てへ回す(
|
|
145
|
+
kind: GenericKind,
|
|
146
|
+
doc: DslDocument,
|
|
147
|
+
図種ごとの理由 = false,
|
|
148
|
+
): boolean {
|
|
149
|
+
if (doc.animate !== undefined && doc.animate.phases.length > 0) return true;
|
|
150
|
+
if (書いた縦列に置く(kind, doc)) return true;
|
|
151
|
+
return 図種ごとの理由;
|
|
152
|
+
}
|
|
153
|
+
|
|
25
154
|
/**
|
|
26
155
|
* v0.4 ... 5 preset (flow / swimlane / er / state / topology) 共通 animation compile。
|
|
27
156
|
* sequence preset と異なり header / footer / step box 構造はない、 シンプルな lane + node + edge 構造。
|
|
@@ -31,9 +160,259 @@ export function compileGenericWithAnimate(doc: DslDocument, opts: GenericOpts):
|
|
|
31
160
|
const { kind, laneWidth } = opts;
|
|
32
161
|
const b = diagram(slugify(doc.title), { topic: doc.title, type: kind });
|
|
33
162
|
|
|
163
|
+
/*
|
|
164
|
+
* 行を持つ図で、書かなかった時の既定 (#2352 / #2782)。
|
|
165
|
+
*
|
|
166
|
+
* **行を書いた箱は表の箱 (`storage`)、書かない箱は札 (`card`)**。 畳む前は図種で
|
|
167
|
+
* 決めており (移り変わりの図は札、表の図は書かないと人の形)、同じ本文でも動きの段を
|
|
168
|
+
* 1 つ足すだけで箱が札の形から人の形に変わっていた。
|
|
169
|
+
*
|
|
170
|
+
* 書いた種類はそのまま通す。 **書いたかどうかは `kindWritten` で見る** = 書かなかった箱の
|
|
171
|
+
* `kind` には既定の `actor` が入るので、値だけでは「`actor` と書いた」 と「書かなかった」 を
|
|
172
|
+
* 分けられない。
|
|
173
|
+
*/
|
|
174
|
+
const 箱の種類 = (a: (typeof doc.actors)[number]): NodeKind => {
|
|
175
|
+
// 段の箱は札で固定するのでここを通らない。路線図と時間軸の印は書いた種類で描く
|
|
176
|
+
if (泳法図で印をどう描く(doc, a) === "描く") return 描ける種別(a.kind);
|
|
177
|
+
if (kind !== "record" || a.kindWritten === true) return 描ける種別(a.kind);
|
|
178
|
+
return (a.rows?.length ?? 0) > 0 ? "storage" : "card";
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
/*
|
|
182
|
+
* 始まりと終わりと途中の札 (#2349 / #2352)。
|
|
183
|
+
*
|
|
184
|
+
* **並べ方の分岐ごとに書かない**。 3 つある並べ方のうち 1 つにしか札の処理が無く、
|
|
185
|
+
* 縦列を書いた状態の図では `initial: true` を書いても札が 1 つも出なかった
|
|
186
|
+
* (知らせも無い = 縦列を足しただけで別の意味の図になっていた)。
|
|
187
|
+
*
|
|
188
|
+
* 札を出すのは状態の図だけ。 どの箱が始まり / 終わりかは `始まりと終わりの決め方` が
|
|
189
|
+
* 1 箇所で決める。 **書いた値はどの並べ方でも効き**、1 つも書いていない時の既定
|
|
190
|
+
* (並びの最初と最後) は、並びが意味を持つ並べ方でだけ効く。
|
|
191
|
+
*
|
|
192
|
+
* **札を出すのは既定の形の箱だけ** (#2352)。 印の箱 (`mark-start` / `mark-end`) は
|
|
193
|
+
* 形そのものが始点と終点を表すので、字を重ねると同じことを 2 度言う
|
|
194
|
+
* (実測 = #2349 で既定を無条件に配った時、印の箱に「初期」 の字が出て見本が壊れた)。
|
|
195
|
+
*/
|
|
196
|
+
/*
|
|
197
|
+
* **並びで決める既定を使わない** (#2782)。 書いた値だけを見る。
|
|
198
|
+
*
|
|
199
|
+
* 並びから推し量ると、1 つだけ書いた図でもう片方が勝手に決まる
|
|
200
|
+
* (実測 = 真ん中の箱に始まりを書いた図で、最後の箱に「最終」 が出た)。
|
|
201
|
+
*/
|
|
202
|
+
const 決め方 = 始まりと終わりの決め方(doc, { 並びで決める: false });
|
|
203
|
+
/*
|
|
204
|
+
* 始まりと終わりの札 (#2349 / #2352 / #2782)。
|
|
205
|
+
*
|
|
206
|
+
* **書いた箱にだけ出す**。 畳む前は移り変わりの図だけが札を出し、1 つも書かない図では
|
|
207
|
+
* 並びの最初と最後を始まり / 終わりとみなし、残りの箱に「状態」 の字を配っていた。
|
|
208
|
+
* 畳んだ後も同じ形にすると、行を並べた表の箱に「状態」 の字が出る = 書いた人が
|
|
209
|
+
* 言っていないことを図が名乗る。
|
|
210
|
+
*
|
|
211
|
+
* **札を出すのは札の形の箱だけ**。 印の箱 (`mark-start` / `mark-end`) は形そのものが
|
|
212
|
+
* 始点と終点を表すので、字を重ねると同じことを 2 度言う。
|
|
213
|
+
*/
|
|
214
|
+
const 札 = (a: (typeof doc.actors)[number], idx: number): { eyebrow?: string } => {
|
|
215
|
+
if (kind !== "record") return {};
|
|
216
|
+
if (箱の種類(a) !== "card") return {};
|
|
217
|
+
if (決め方.始まり(a, idx)) return { eyebrow: "初期" };
|
|
218
|
+
if (決め方.終わり(a, idx)) return { eyebrow: "最終" };
|
|
219
|
+
return {};
|
|
220
|
+
};
|
|
221
|
+
|
|
34
222
|
// lane / node 配置 ... preset kind に応じて切替
|
|
35
223
|
const actorToNodeId = new Map<string, string>();
|
|
36
|
-
|
|
224
|
+
const 段階ごとの箱か = kind === "swimlane" && doc.shape === "stages";
|
|
225
|
+
const 路線図か = kind === "swimlane" && doc.shape === "metro";
|
|
226
|
+
const 時間軸か = kind === "swimlane" && doc.shape === "timeline";
|
|
227
|
+
const 路線図の色 = 路線図か ? metroBranchTones(doc) : undefined;
|
|
228
|
+
let timelineActorIndex: Map<string, number> | undefined;
|
|
229
|
+
let timelineNumberIdByCardId: Map<string, string> | undefined;
|
|
230
|
+
let stagesPlacement: StagesPlacement | undefined;
|
|
231
|
+
if (時間軸か) {
|
|
232
|
+
const numberIdByCardId = new Map<string, string>();
|
|
233
|
+
timelineNumberIdByCardId = numberIdByCardId;
|
|
234
|
+
const actorsByName = new Map(doc.actors.map((actor) => [actor.name, actor]));
|
|
235
|
+
const numberToneByActorName = new Map(
|
|
236
|
+
doc.flow.flatMap((edge) =>
|
|
237
|
+
actorsByName.get(edge.from)?.kind === "decision" && edge.tone !== undefined
|
|
238
|
+
? [[edge.to, edge.tone] as const]
|
|
239
|
+
: []),
|
|
240
|
+
);
|
|
241
|
+
const 軸に置く段 = new Set(
|
|
242
|
+
doc.actors
|
|
243
|
+
.filter((actor) => 泳法図で印をどう描く(doc, actor) === "描く")
|
|
244
|
+
.map((actor) => actor.name),
|
|
245
|
+
);
|
|
246
|
+
const placement = placeTimeline(doc, 軸に置く段);
|
|
247
|
+
const timelineStageByName = new Map<string, number>();
|
|
248
|
+
timelineActorIndex = timelineStageByName;
|
|
249
|
+
doc.actors.forEach((actor, index) => {
|
|
250
|
+
const step = placement.steps[index];
|
|
251
|
+
if (step === undefined) throw new Error(`時間軸の段 "${actor.name}" の配置がありません`);
|
|
252
|
+
timelineStageByName.set(actor.name, step.stage);
|
|
253
|
+
});
|
|
254
|
+
b.lane("timeline-axis", {
|
|
255
|
+
width: TIMELINE_AXIS_WIDTH,
|
|
256
|
+
role: "overlay",
|
|
257
|
+
timelineAxis: true,
|
|
258
|
+
posX: placement.axisX - TIMELINE_AXIS_WIDTH / 2,
|
|
259
|
+
posY: 0,
|
|
260
|
+
posW: TIMELINE_AXIS_WIDTH,
|
|
261
|
+
posH: placement.axisHeight,
|
|
262
|
+
});
|
|
263
|
+
b.lane("timeline-steps", {
|
|
264
|
+
width: placement.width,
|
|
265
|
+
role: "overlay",
|
|
266
|
+
posX: 0,
|
|
267
|
+
posY: 0,
|
|
268
|
+
posW: placement.width,
|
|
269
|
+
posH: placement.stepsHeight,
|
|
270
|
+
});
|
|
271
|
+
doc.actors.forEach((actor, index) => {
|
|
272
|
+
const step = placement.steps[index];
|
|
273
|
+
if (step === undefined) throw new Error(`時間軸の段 "${actor.name}" の配置がありません`);
|
|
274
|
+
actorToNodeId.set(actor.name, step.cardId);
|
|
275
|
+
if (泳法図で印をどう描く(doc, actor) === "描く") {
|
|
276
|
+
const markSize = actor.kind === "decision"
|
|
277
|
+
? { w: TIMELINE_DECISION_WIDTH, h: TIMELINE_DECISION_HEIGHT }
|
|
278
|
+
: actor.kind === "mark-end"
|
|
279
|
+
? { w: TIMELINE_END_WIDTH, h: TIMELINE_END_HEIGHT }
|
|
280
|
+
: {};
|
|
281
|
+
b.node(step.cardId, {
|
|
282
|
+
lane: "timeline-axis",
|
|
283
|
+
stack: index,
|
|
284
|
+
kind: 箱の種類(actor),
|
|
285
|
+
title: 箱の題(actor),
|
|
286
|
+
...(actor.subtitlePlacement !== undefined
|
|
287
|
+
? { subtitlePlacement: actor.subtitlePlacement }
|
|
288
|
+
: {}),
|
|
289
|
+
...markSize,
|
|
290
|
+
posX: placement.axisX,
|
|
291
|
+
posY: step.y,
|
|
292
|
+
});
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
const numberId = step.numberId;
|
|
296
|
+
if (numberId !== undefined) {
|
|
297
|
+
numberIdByCardId.set(step.cardId, numberId);
|
|
298
|
+
const numberTone = numberToneByActorName.get(actor.name);
|
|
299
|
+
b.node(numberId, {
|
|
300
|
+
lane: "timeline-axis",
|
|
301
|
+
stack: index,
|
|
302
|
+
kind: "timeline-number",
|
|
303
|
+
title: String(numberIdByCardId.size),
|
|
304
|
+
w: TIMELINE_NUMBER_SIZE,
|
|
305
|
+
h: TIMELINE_NUMBER_SIZE,
|
|
306
|
+
...(numberTone !== undefined ? { tone: numberTone } : {}),
|
|
307
|
+
posX: placement.axisX,
|
|
308
|
+
posY: step.y,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
b.node(step.cardId, {
|
|
312
|
+
lane: "timeline-steps",
|
|
313
|
+
stack: index,
|
|
314
|
+
kind: "card",
|
|
315
|
+
title: 箱の題(actor),
|
|
316
|
+
...(actor.lane !== undefined
|
|
317
|
+
? { subtitle: doc.lanes?.[actor.lane]?.label ?? actor.lane }
|
|
318
|
+
: {}),
|
|
319
|
+
...(actor.subtitlePlacement !== undefined
|
|
320
|
+
? { subtitlePlacement: actor.subtitlePlacement }
|
|
321
|
+
: {}),
|
|
322
|
+
...(numberId !== undefined ? { leaderTo: numberId } : {}),
|
|
323
|
+
w: step.cardWidth,
|
|
324
|
+
h: TIMELINE_CARD_HEIGHT,
|
|
325
|
+
posX: step.cardX,
|
|
326
|
+
posY: step.y,
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
} else if (路線図か) {
|
|
330
|
+
const placement = placeMetro(doc);
|
|
331
|
+
for (const track of placement.tracks) {
|
|
332
|
+
b.lane(track.id, {
|
|
333
|
+
width: placement.width,
|
|
334
|
+
label: track.label,
|
|
335
|
+
metro: track.subtitle === undefined ? {} : { subtitle: track.subtitle },
|
|
336
|
+
posX: track.posX,
|
|
337
|
+
posY: track.posY,
|
|
338
|
+
posW: track.posW,
|
|
339
|
+
posH: track.posH,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
doc.actors.forEach((actor, index) => {
|
|
343
|
+
const station = placement.stations[index]!;
|
|
344
|
+
const id = slugify(actor.name) || `n${index}`;
|
|
345
|
+
const markFrameSize = metroMarkFrameSize(actor.kind);
|
|
346
|
+
actorToNodeId.set(actor.name, id);
|
|
347
|
+
b.node(id, {
|
|
348
|
+
lane: station.trackId,
|
|
349
|
+
stack: index,
|
|
350
|
+
kind: 泳法図で印をどう描く(doc, actor) === "描く" ? 箱の種類(actor) : "station",
|
|
351
|
+
title: 箱の題(actor),
|
|
352
|
+
...(actor.stationNamePosition !== undefined
|
|
353
|
+
? { stationNamePosition: actor.stationNamePosition }
|
|
354
|
+
: {}),
|
|
355
|
+
...(路線図の色?.nodeToneByName.get(actor.name) !== undefined
|
|
356
|
+
? { tone: 路線図の色.nodeToneByName.get(actor.name) }
|
|
357
|
+
: {}),
|
|
358
|
+
...(markFrameSize === undefined ? {} : { w: markFrameSize, h: markFrameSize }),
|
|
359
|
+
posX: station.posX,
|
|
360
|
+
posY: station.posY,
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
} else if (段階ごとの箱か) {
|
|
364
|
+
/*
|
|
365
|
+
* 段階ごとに同じ高さの列を左から並べ、その中へ札を積む (#2797 / #2831)。
|
|
366
|
+
*
|
|
367
|
+
* 列の見出し (`段階 N` と段階の名前) と、札の右の担当は描く側が描く。
|
|
368
|
+
* 列と札の決め方は `placeStages` が持ち、静止図と動く図を同じ配置に揃える。
|
|
369
|
+
* 種類は札 (`card`) で固定する = 札の右の字は描く側が `card` にだけ描く。
|
|
370
|
+
*/
|
|
371
|
+
const placement = placeStages(doc);
|
|
372
|
+
stagesPlacement = placement;
|
|
373
|
+
const lastStack = Math.max(0, ...[...placement.cards.values()].map((card) => card.stack));
|
|
374
|
+
const stageLaneHeight =
|
|
375
|
+
STAGE_FIRST_CARD_CENTER_Y + lastStack * STAGE_CARD_PITCH + STAGE_CARD_HEIGHT / 2;
|
|
376
|
+
for (const [columnIndex, column] of placement.columns.entries()) {
|
|
377
|
+
b.lane(column.id, {
|
|
378
|
+
width: laneWidth,
|
|
379
|
+
// cdl は段階列の面の間を 48 取る。見本の 64 にするため、
|
|
380
|
+
// lane 自体の間に残りの 16 を書き、描画側の列幅は変えない。
|
|
381
|
+
posX: columnIndex * (laneWidth + STAGE_LANE_GAP),
|
|
382
|
+
posY: STAGE_LANE_Y,
|
|
383
|
+
posW: laneWidth,
|
|
384
|
+
posH: stageLaneHeight,
|
|
385
|
+
stage: {
|
|
386
|
+
number: column.number,
|
|
387
|
+
name: column.name,
|
|
388
|
+
...(doc.stageHeader === undefined ? {} : { header: doc.stageHeader }),
|
|
389
|
+
},
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
doc.actors.forEach((a, idx) => {
|
|
393
|
+
const id = slugify(a.name) || `n${idx}`;
|
|
394
|
+
actorToNodeId.set(a.name, id);
|
|
395
|
+
const card = placement.cards.get(a.name);
|
|
396
|
+
if (card === undefined) throw new Error(`段の箱の "${a.name}" を置く列がありません`);
|
|
397
|
+
const columnName = placement.columns[card.column]?.name;
|
|
398
|
+
const writtenColumnWidth = columnName === undefined
|
|
399
|
+
? undefined
|
|
400
|
+
: doc.stageLayout?.columnWidths?.[columnName];
|
|
401
|
+
b.node(id, {
|
|
402
|
+
lane: card.columnId,
|
|
403
|
+
stack: card.stack,
|
|
404
|
+
w: writtenColumnWidth === undefined
|
|
405
|
+
? STAGE_CARD_WIDTH
|
|
406
|
+
: Math.max(1, writtenColumnWidth - STAGE_CARD_COLUMN_INSET),
|
|
407
|
+
h: STAGE_CARD_HEIGHT,
|
|
408
|
+
posX: card.column * (laneWidth + STAGE_LANE_GAP) + laneWidth / 2,
|
|
409
|
+
posY: STAGE_FIRST_CARD_CENTER_Y + card.stack * STAGE_CARD_PITCH,
|
|
410
|
+
kind: "card",
|
|
411
|
+
title: 箱の題(a),
|
|
412
|
+
...(card.subtitle === undefined ? {} : { subtitle: card.subtitle }),
|
|
413
|
+
});
|
|
414
|
+
});
|
|
415
|
+
} else if (書いた縦列に置く(kind, doc)) {
|
|
37
416
|
/*
|
|
38
417
|
* **書いた縦列に置く** (#1263)。 縦列を並べるための入れ物として使う図種でだけ効く。
|
|
39
418
|
*
|
|
@@ -56,41 +435,19 @@ export function compileGenericWithAnimate(doc: DslDocument, opts: GenericOpts):
|
|
|
56
435
|
for (const lid of 並び) {
|
|
57
436
|
b.lane(lid, { width: laneWidth, ...(kind === "topology" ? { contain: true } : {}) });
|
|
58
437
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
* 書き順で 0 から詰め直していた間、`stack: 1` と書いた箱が 0 へ落ちていた。
|
|
63
|
-
* 決定木のように「同じ高さに並ばない」 ことが図の意味そのものになる形では、
|
|
64
|
-
* 詰めた瞬間に別の図になる (実測 = 3 段の木の根が 1 段目へ上がった)。
|
|
65
|
-
*
|
|
66
|
-
* 書かなかった箱は、その縦列で **空いている一番小さい段** に置く。 単に数え上げると
|
|
67
|
-
* 書いた番号と重なり、2 つの箱が同じ段に載る。
|
|
68
|
-
*/
|
|
69
|
-
const 埋まった段 = new Map<string, Set<number>>();
|
|
70
|
-
const 埋める = (lid: string, stack: number): void => {
|
|
71
|
-
const 集合 = 埋まった段.get(lid) ?? new Set<number>();
|
|
72
|
-
集合.add(stack);
|
|
73
|
-
埋まった段.set(lid, 集合);
|
|
74
|
-
};
|
|
75
|
-
for (const a of doc.actors) {
|
|
76
|
-
if (a.lane !== undefined && a.stack !== undefined) 埋める(a.lane, a.stack);
|
|
77
|
-
}
|
|
438
|
+
// 段の決め方は `縦列ごとの段を決める` が持つ (#1394 / #2396)。
|
|
439
|
+
// 箱を並べる図種すべてが同じ決め方を使う = 2 か所に置くと片方だけ直した日に食い違う
|
|
440
|
+
const 段 = 縦列ごとの段を決める(doc.actors);
|
|
78
441
|
doc.actors.forEach((a, idx) => {
|
|
79
442
|
const id = slugify(a.name) || `n${idx}`;
|
|
80
443
|
actorToNodeId.set(a.name, id);
|
|
81
444
|
const lid = a.lane!;
|
|
82
|
-
let stack = a.stack;
|
|
83
|
-
if (stack === undefined) {
|
|
84
|
-
stack = 0;
|
|
85
|
-
const 集合 = 埋まった段.get(lid);
|
|
86
|
-
while (集合?.has(stack)) stack += 1;
|
|
87
|
-
埋める(lid, stack);
|
|
88
|
-
}
|
|
89
445
|
b.node(id, {
|
|
90
446
|
lane: lid,
|
|
91
|
-
stack,
|
|
92
|
-
kind:
|
|
447
|
+
stack: 段.get(a) ?? 0,
|
|
448
|
+
kind: 箱の種類(a),
|
|
93
449
|
title: 箱の題(a),
|
|
450
|
+
...札(a, idx),
|
|
94
451
|
});
|
|
95
452
|
});
|
|
96
453
|
} else if (並べる向き(kind, doc) === "縦") {
|
|
@@ -113,38 +470,34 @@ export function compileGenericWithAnimate(doc: DslDocument, opts: GenericOpts):
|
|
|
113
470
|
b.node(id, {
|
|
114
471
|
lane: lid,
|
|
115
472
|
stack: idx,
|
|
116
|
-
kind:
|
|
473
|
+
kind: 箱の種類(a),
|
|
117
474
|
title: 箱の題(a),
|
|
475
|
+
...札(a, idx),
|
|
118
476
|
});
|
|
119
477
|
});
|
|
120
478
|
} else {
|
|
121
|
-
// actor ごとに 1 lane (横並び)。 `swimlane` / `
|
|
122
|
-
// `向き: 横`
|
|
479
|
+
// actor ごとに 1 lane (横並び)。 `swimlane` / `record` の既定と、
|
|
480
|
+
// `向き: 横` を書いたフローがここに来る (#1494)
|
|
123
481
|
//
|
|
124
|
-
// **見出しを付けるのは `swimlane` だけ** (#1241)。
|
|
482
|
+
// **見出しを付けるのは `swimlane` だけ** (#1241)。 どちらも箱を 1 つずつ持ち、
|
|
125
483
|
// その箱が既に名前を描く。 縦列にも同じ名前を渡すと **同じ字が縦に 2 つ並ぶ**
|
|
126
484
|
// (実測 = 描いた絵に `Alpha` `Beta` が 2 度出る)。
|
|
127
485
|
//
|
|
128
486
|
// `swimlane` は縦列そのものが「誰の担当か」 を読ませる図なので見出しが要る。
|
|
129
|
-
// `
|
|
130
|
-
//
|
|
487
|
+
// `record` の縦列は箱を並べるための入れ物で、読む人に見せる意味を持たない
|
|
488
|
+
// (組立て API 側も見出しを空のまま置く)。
|
|
131
489
|
const 見出しを付ける = kind === "swimlane";
|
|
132
|
-
const 決め方2 = 始まりと終わりの決め方(doc);
|
|
133
490
|
doc.actors.forEach((a, idx) => {
|
|
134
491
|
const lid = `lane-${slugify(a.name) || idx}`;
|
|
135
492
|
b.lane(lid, { width: laneWidth, ...(見出しを付ける ? { label: a.name } : {}) });
|
|
136
493
|
const id = slugify(a.name) || `n${idx}`;
|
|
137
494
|
actorToNodeId.set(a.name, id);
|
|
138
|
-
// er は entity、 state は initial/final marker、 swimlane はそのまま actor
|
|
139
|
-
const isInitial = kind === "state" && 決め方2.始まり(a, idx);
|
|
140
|
-
const isFinal = kind === "state" && 決め方2.終わり(a, idx);
|
|
141
495
|
b.node(id, {
|
|
142
496
|
lane: lid,
|
|
143
497
|
stack: 0,
|
|
144
|
-
kind:
|
|
498
|
+
kind: 箱の種類(a),
|
|
145
499
|
title: 箱の題(a),
|
|
146
|
-
|
|
147
|
-
...(isFinal ? { eyebrow: "最終" } : {}),
|
|
500
|
+
...札(a, idx),
|
|
148
501
|
});
|
|
149
502
|
});
|
|
150
503
|
}
|
|
@@ -165,23 +518,62 @@ export function compileGenericWithAnimate(doc: DslDocument, opts: GenericOpts):
|
|
|
165
518
|
const toId = actorToNodeId.get(s.to);
|
|
166
519
|
if (fromId === undefined || toId === undefined) return;
|
|
167
520
|
const edgeId = `e${idx}-${fromId}-${toId}`;
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
|
|
171
|
-
|
|
521
|
+
// 多重度と名前と両端は 1 か所で作る (#2105)。 札に `(1:N)` と添えるだけだった間、
|
|
522
|
+
// 段を持つ図には端の形が 1 つも付かなかった。
|
|
523
|
+
//
|
|
524
|
+
// **端の既定は持たない** (#2591 / #2595 / #2782)。 畳む前は移り変わりの図にだけ
|
|
525
|
+
// 「実線に開いた矢」 を無条件で渡していたが、畳んだ後も渡すと多重度から導いた端を
|
|
526
|
+
// 上から潰す = 箱どうしの個数が消える。 書いた端だけを渡す形に倒す
|
|
527
|
+
const 関係 = opts.edgeDefaults?.(s);
|
|
528
|
+
const timelineEdgeKind = timelineActorIndex !== undefined
|
|
529
|
+
? classifyTimelineEdge(timelineActorIndex.get(s.from)!, timelineActorIndex.get(s.to)!)
|
|
530
|
+
: undefined;
|
|
531
|
+
const timelineHorizontalBranch = timelineEdgeKind === "branch"
|
|
532
|
+
&& timelineActorIndex?.get(s.from) === timelineActorIndex?.get(s.to);
|
|
533
|
+
const edgeFromId =
|
|
534
|
+
timelineEdgeKind === "advance" ? (timelineNumberIdByCardId!.get(fromId) ?? fromId) : fromId;
|
|
535
|
+
const edgeToId =
|
|
536
|
+
timelineEdgeKind === "advance" ? (timelineNumberIdByCardId!.get(toId) ?? toId) : toId;
|
|
537
|
+
const timelineEnd = 時間軸か && doc.actors.find((actor) => actor.name === s.to)?.kind === "mark-end";
|
|
538
|
+
b.edge(edgeFromId, edgeToId, {
|
|
172
539
|
id: edgeId,
|
|
173
540
|
label: 関係?.label ?? s.label,
|
|
174
|
-
...(
|
|
175
|
-
|
|
541
|
+
...(関係?.tone !== undefined ? { tone: 関係.tone } : {}),
|
|
542
|
+
...(関係?.style !== undefined ? { style: 関係.style } : {}),
|
|
543
|
+
...(路線図の色?.edgeToneByIndex.get(idx) !== undefined
|
|
544
|
+
? { tone: 路線図の色.edgeToneByIndex.get(idx) }
|
|
176
545
|
: {}),
|
|
546
|
+
...(timelineEdgeKind === "back"
|
|
547
|
+
? { routing: "back-detour" as const, fromSide: "right" as const, toSide: "right" as const }
|
|
548
|
+
: 路線図か
|
|
549
|
+
? { routing: "metro" as const }
|
|
550
|
+
: stagesPlacement !== undefined
|
|
551
|
+
? 段の箱の線の指定(stagesPlacement, s)
|
|
552
|
+
: 後ろへ戻る矢印か(kind, fromId, toId, 箱の並び, 書いた縦列に置く(kind, doc))
|
|
553
|
+
? { routing: "back-detour" as const }
|
|
554
|
+
: {}),
|
|
177
555
|
...(関係?.sub ? { sub: 関係.sub } : {}),
|
|
178
556
|
...(関係?.head ? { head: 関係.head } : {}),
|
|
557
|
+
...(関係?.headFill ? { headFill: 関係.headFill } : {}),
|
|
558
|
+
...(路線図か && s.style !== "dashed" ? { head: "none" as const } : {}),
|
|
179
559
|
...(関係?.tailHead ? { tailHead: 関係.tailHead } : {}),
|
|
180
|
-
...(
|
|
560
|
+
...(関係?.tailHeadFill ? { tailHeadFill: 関係.tailHeadFill } : {}),
|
|
561
|
+
...(関係?.headLabel ? { headLabel: 関係.headLabel } : {}),
|
|
562
|
+
...(関係?.tailLabel ? { tailLabel: 関係.tailLabel } : {}),
|
|
181
563
|
...(s.side ? { side: s.side } : {}),
|
|
182
|
-
|
|
183
|
-
...(s.
|
|
184
|
-
...(
|
|
564
|
+
...(s.fromSide !== undefined ? { fromSide: s.fromSide } : {}),
|
|
565
|
+
...(s.toSide !== undefined ? { toSide: s.toSide } : {}),
|
|
566
|
+
...(timelineHorizontalBranch && s.label !== ""
|
|
567
|
+
? {
|
|
568
|
+
overlay: true,
|
|
569
|
+
labelCollision: "author" as const,
|
|
570
|
+
labelSize: 時間軸の横分岐の札の大きさ,
|
|
571
|
+
labelOffsetY: 時間軸の横分岐の札の下げ幅,
|
|
572
|
+
}
|
|
573
|
+
: timelineEdgeKind !== undefined && s.label !== ""
|
|
574
|
+
? { overlay: true }
|
|
575
|
+
: {}),
|
|
576
|
+
...(timelineEdgeKind === "advance" && !timelineEnd ? { head: "none" as const } : {}),
|
|
185
577
|
});
|
|
186
578
|
edgeIds.push(edgeId);
|
|
187
579
|
});
|
|
@@ -202,6 +594,16 @@ export function compileGenericWithAnimate(doc: DslDocument, opts: GenericOpts):
|
|
|
202
594
|
},
|
|
203
595
|
(pb) => {
|
|
204
596
|
const activateIds = resolveHighlightGeneric(p, doc, actorToNodeId, edgeIds);
|
|
597
|
+
if (timelineNumberIdByCardId !== undefined) {
|
|
598
|
+
const activeNumberIds = new Set<string>();
|
|
599
|
+
for (const id of activateIds) {
|
|
600
|
+
const numberId = timelineNumberIdByCardId.get(id);
|
|
601
|
+
if (numberId !== undefined) activeNumberIds.add(numberId);
|
|
602
|
+
}
|
|
603
|
+
for (const numberId of timelineNumberIdByCardId.values()) {
|
|
604
|
+
if (activeNumberIds.has(numberId)) activateIds.push(numberId);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
205
607
|
if (activateIds.length > 0) {
|
|
206
608
|
pb.activate(...activateIds);
|
|
207
609
|
}
|
|
@@ -219,7 +621,17 @@ export function compileGenericWithAnimate(doc: DslDocument, opts: GenericOpts):
|
|
|
219
621
|
);
|
|
220
622
|
}
|
|
221
623
|
|
|
222
|
-
|
|
624
|
+
const built = b.build();
|
|
625
|
+
if (時間軸か) {
|
|
626
|
+
const endIds = new Set(
|
|
627
|
+
doc.actors.filter((actor) => actor.kind === "mark-end").map((actor) => actorToNodeId.get(actor.name)),
|
|
628
|
+
);
|
|
629
|
+
for (const edge of built.edges) {
|
|
630
|
+
// `head` を書かないことが既定の三角を表すため、終わりへ入る線には欄自体を残さない。
|
|
631
|
+
if (endIds.has(edge.to) && edge.head === undefined) delete edge.head;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
return built;
|
|
223
635
|
}
|
|
224
636
|
|
|
225
637
|
/**
|
|
@@ -252,7 +664,7 @@ export function resolveHighlightGeneric(
|
|
|
252
664
|
}
|
|
253
665
|
// actor 名 → node id。 見つからなければ slug の形でも探す。
|
|
254
666
|
// 順序図だけが slug を受理する状態にすると、 同じ記述が図種で別の意味になる
|
|
255
|
-
// (実測 = `api-gateway` が順序図では光り、
|
|
667
|
+
// (実測 = `api-gateway` が順序図では光り、 フローでは何も光らなかった)
|
|
256
668
|
const nodeId = actorToNodeId.get(entry.name) ?? slugLookup(actorToNodeId, entry.name);
|
|
257
669
|
if (nodeId) {
|
|
258
670
|
out.push(nodeId);
|