@cardenelabs/dragon 0.30.0 → 0.32.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 +6966 -1438
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2150 -118
- package/dist/index.d.ts +2150 -118
- package/dist/index.js +6950 -1440
- 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 +1550 -370
- package/src/focus.ts +2 -2
- package/src/index.ts +34 -2
- package/src/json-parser.ts +1407 -48
- package/src/keywords.ts +251 -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
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import type { DslActor, DslDocument, DslStep } from "../types";
|
|
2
|
+
import { 縦列ごとの段を決める } from "./lanes";
|
|
3
|
+
import { slugify } from "./slug";
|
|
4
|
+
import { 分かれ道の札, 泳法図で印をどう描く } from "./swimlane-marks";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 段の箱 (`shape: stages`, #2797 / #2831) の配置規則。
|
|
8
|
+
*
|
|
9
|
+
* 段階ごとに同じ上端と高さの列を 1 本作り、列の見出しに「段階 N」と段階の名前を出す。
|
|
10
|
+
* 箱は列の中へ書いた順に積む札にし、担当 (`lane`) は札の右の小さな字として添える。
|
|
11
|
+
* 列の面・見出し・札の右の字は描く側 (`lane.stage` と `card` の `subtitle`) が描く。
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 段階の列 1 本の幅。描く側は列の面をこの幅から列の間の 48 を引いた幅で描く。
|
|
16
|
+
* 幅 332 の列の面に対して、札の左右へ内側の余白を 32 ずつ残す。
|
|
17
|
+
*/
|
|
18
|
+
export const STAGE_LANE_WIDTH = 380;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 札の幅。見本は列の面の内側を 24 ずつ空けるが、幅 284 では列の間 (48) に置く
|
|
22
|
+
* 「いいえ」の札と札の右端が近づき、描く側が衝突を避けて札を列の下端より下へ押し出す。
|
|
23
|
+
* 幅 270 でも押し出されるため 268 とし、列の間を描く側が広げずに済めば cardene777/cdl#1035 で 24 に寄せる。
|
|
24
|
+
*/
|
|
25
|
+
export const STAGE_CARD_WIDTH = STAGE_LANE_WIDTH - 112;
|
|
26
|
+
|
|
27
|
+
/** `stageLayout.columnWidths` を書いた列では、見本どおり列面の左右を 24 ずつ空ける。 */
|
|
28
|
+
export const STAGE_CARD_COLUMN_INSET = 48;
|
|
29
|
+
|
|
30
|
+
/** 札の高さ。名前 1 行と右の担当を同じ行に置く札で、見本の札と同じく 1 行分に収める。 */
|
|
31
|
+
export const STAGE_CARD_HEIGHT = 48;
|
|
32
|
+
|
|
33
|
+
/** cdl の列面の間 48 に足し、見本の列面の間 64 にする lane の間。 */
|
|
34
|
+
export const STAGE_LANE_GAP = 16;
|
|
35
|
+
|
|
36
|
+
/** 列の上端 130 から見本の 1 段目の中心 324 までを world 座標に直した値。 */
|
|
37
|
+
export const STAGE_FIRST_CARD_CENTER_Y = 222;
|
|
38
|
+
|
|
39
|
+
/** 描画側の自動配置と同じ、高さ 48 の札と次の札の間 100。 */
|
|
40
|
+
export const STAGE_CARD_PITCH = 148;
|
|
41
|
+
|
|
42
|
+
/** cdl の自動 lane と同じ上端。列を固定して先頭の余白を保つため使う。 */
|
|
43
|
+
export const STAGE_LANE_Y = 28;
|
|
44
|
+
|
|
45
|
+
export type StageColumn = {
|
|
46
|
+
id: string;
|
|
47
|
+
/** 見出しの番号。描く側が「段階 N」の形で出す */
|
|
48
|
+
number: string;
|
|
49
|
+
name: string;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export type StageCard = {
|
|
53
|
+
columnId: string;
|
|
54
|
+
/** 左から数えた列の位置 (0 始まり)。線を曲線にするか戻り線にするかの判定に使う */
|
|
55
|
+
column: number;
|
|
56
|
+
stack: number;
|
|
57
|
+
/** 札の右へ出す字。担当の名前か、分かれ道の札 */
|
|
58
|
+
subtitle?: string;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export type StagesPlacement = {
|
|
62
|
+
columns: StageColumn[];
|
|
63
|
+
/** 箱の名前ごとの札 */
|
|
64
|
+
cards: ReadonlyMap<string, StageCard>;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/** 段階を書かない箱は、担当、担当も無ければ箱の名前を段階に使う (#2797)。 */
|
|
68
|
+
export function 段階名(actor: Pick<DslActor, "stage" | "lane" | "name">): string {
|
|
69
|
+
return actor.stage ?? actor.lane ?? actor.name;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 札の右に出す字。分かれ道は「分かれ道」、それ以外は担当の名前を出す。
|
|
74
|
+
*
|
|
75
|
+
* 担当の名前が段階の名前と同じ時は出さない。段階を書かない箱は担当を段階に使うため、
|
|
76
|
+
* 出すと見出しと同じ字が札の右にもう 1 度並ぶ。
|
|
77
|
+
*/
|
|
78
|
+
function 札の右の字(
|
|
79
|
+
doc: Pick<DslDocument, "type" | "shape" | "lanes">,
|
|
80
|
+
actor: DslActor,
|
|
81
|
+
stage: string,
|
|
82
|
+
): string | undefined {
|
|
83
|
+
if (泳法図で印をどう描く(doc, actor) === "札") return 分かれ道の札;
|
|
84
|
+
if (actor.lane === undefined) return undefined;
|
|
85
|
+
const 担当 = doc.lanes?.[actor.lane]?.label ?? actor.lane;
|
|
86
|
+
return 担当 === stage ? undefined : 担当;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 段を書かない列で、列の札をどの段から積むかを決める。
|
|
91
|
+
*
|
|
92
|
+
* 左の列から入る線のうち、札と同じ段から出る線が最も多くなる位置を選ぶ。
|
|
93
|
+
* 同じ段から入る線は列をまたいでも真横に走るため、見本と同じく曲げずに読める。
|
|
94
|
+
* 揃う本数が同じなら、図の一番下の段を下げない位置、その中でも上の位置を選ぶ。
|
|
95
|
+
*/
|
|
96
|
+
function 積み始める段(
|
|
97
|
+
札たち: readonly DslActor[],
|
|
98
|
+
flow: DslDocument["flow"],
|
|
99
|
+
左の列の段: ReadonlyMap<string, number>,
|
|
100
|
+
これまでの最下段: number,
|
|
101
|
+
): number {
|
|
102
|
+
const 列の中の順 = new Map(札たち.map((actor, index) => [actor.name, index]));
|
|
103
|
+
const 揃う本数 = new Map<number, number>();
|
|
104
|
+
for (const step of flow) {
|
|
105
|
+
const index = 列の中の順.get(step.to);
|
|
106
|
+
const 出る段 = 左の列の段.get(step.from);
|
|
107
|
+
if (index === undefined || 出る段 === undefined || 出る段 < index) continue;
|
|
108
|
+
const 始め = 出る段 - index;
|
|
109
|
+
揃う本数.set(始め, (揃う本数.get(始め) ?? 0) + 1);
|
|
110
|
+
}
|
|
111
|
+
const 最下段 = (始め: number): number => Math.max(これまでの最下段, 始め + 札たち.length - 1);
|
|
112
|
+
let 選んだ = 0;
|
|
113
|
+
for (const [始め, 本数] of 揃う本数) {
|
|
114
|
+
const 今の本数 = 揃う本数.get(選んだ) ?? 0;
|
|
115
|
+
if (本数 < 今の本数) continue;
|
|
116
|
+
if (本数 === 今の本数) {
|
|
117
|
+
if (最下段(始め) > 最下段(選んだ)) continue;
|
|
118
|
+
if (最下段(始め) === 最下段(選んだ) && 始め >= 選んだ) continue;
|
|
119
|
+
}
|
|
120
|
+
選んだ = 始め;
|
|
121
|
+
}
|
|
122
|
+
return 選んだ;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* 段の箱の列と札を決める。
|
|
127
|
+
*
|
|
128
|
+
* 列は段階が箱に最初に現れた順に左から並べる。札は列の中で書いた順に下へ積み、
|
|
129
|
+
* 段を書いた札がある列だけは書いた段をそのまま使う (空いた段へ書かない札を置く)。
|
|
130
|
+
* 段を書かない列は、左の列から真横に入れる段から積む (`積み始める段`)。
|
|
131
|
+
*/
|
|
132
|
+
export function placeStages(
|
|
133
|
+
doc: Pick<DslDocument, "type" | "shape" | "actors" | "flow" | "lanes">,
|
|
134
|
+
): StagesPlacement {
|
|
135
|
+
const 段階たち: string[] = [];
|
|
136
|
+
for (const actor of doc.actors) {
|
|
137
|
+
const stage = 段階名(actor);
|
|
138
|
+
if (!段階たち.includes(stage)) 段階たち.push(stage);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const 使ったid = new Set<string>();
|
|
142
|
+
const columns = 段階たち.map((stage, index): StageColumn => {
|
|
143
|
+
const base = `stage-${slugify(stage) || index}`;
|
|
144
|
+
let id = base;
|
|
145
|
+
let suffix = 2;
|
|
146
|
+
while (使ったid.has(id)) id = `${base}-${suffix++}`;
|
|
147
|
+
使ったid.add(id);
|
|
148
|
+
return { id, number: String(index + 1), name: stage };
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const cards = new Map<string, StageCard>();
|
|
152
|
+
const 置いた段 = new Map<string, number>();
|
|
153
|
+
let これまでの最下段 = -1;
|
|
154
|
+
for (const [column, stage] of 段階たち.entries()) {
|
|
155
|
+
const 札たち = doc.actors.filter((actor) => 段階名(actor) === stage);
|
|
156
|
+
const 書いた段 = 札たち.some((actor) => actor.stack !== undefined)
|
|
157
|
+
? 縦列ごとの段を決める(札たち, () => stage)
|
|
158
|
+
: undefined;
|
|
159
|
+
const 始め = 書いた段 === undefined
|
|
160
|
+
? 積み始める段(札たち, doc.flow, 置いた段, これまでの最下段)
|
|
161
|
+
: 0;
|
|
162
|
+
for (const [index, actor] of 札たち.entries()) {
|
|
163
|
+
const stack = 書いた段?.get(actor) ?? 始め + index;
|
|
164
|
+
const subtitle = 札の右の字(doc, actor, stage);
|
|
165
|
+
cards.set(actor.name, {
|
|
166
|
+
columnId: columns[column]!.id,
|
|
167
|
+
column,
|
|
168
|
+
stack,
|
|
169
|
+
...(subtitle === undefined ? {} : { subtitle }),
|
|
170
|
+
});
|
|
171
|
+
これまでの最下段 = Math.max(これまでの最下段, stack);
|
|
172
|
+
}
|
|
173
|
+
for (const actor of 札たち) 置いた段.set(actor.name, cards.get(actor.name)!.stack);
|
|
174
|
+
}
|
|
175
|
+
return { columns, cards };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* 段の箱の線の通し方。
|
|
180
|
+
*
|
|
181
|
+
* 右の列へ進む線は曲線 (`curve`) で列の間を渡す。左の列や同じ列の上の札へ戻る線は
|
|
182
|
+
* 列の見出しより上を回る戻り線 (`back-detour`) にする。同じ列で下の札へ進む線は
|
|
183
|
+
* 描く側の既定 (真下へ下ろす直線) のまま返さない。
|
|
184
|
+
*/
|
|
185
|
+
export function 段の箱の線の通し方(
|
|
186
|
+
placement: StagesPlacement,
|
|
187
|
+
from: string,
|
|
188
|
+
to: string,
|
|
189
|
+
): "curve" | "back-detour" | undefined {
|
|
190
|
+
const 出る札 = placement.cards.get(from);
|
|
191
|
+
const 入る札 = placement.cards.get(to);
|
|
192
|
+
if (出る札 === undefined || 入る札 === undefined) return undefined;
|
|
193
|
+
if (出る札.column < 入る札.column) return "curve";
|
|
194
|
+
if (出る札.column > 入る札.column) return "back-detour";
|
|
195
|
+
return 入る札.stack < 出る札.stack ? "back-detour" : undefined;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* 描く側が線の札を描く高さ。字 1 行の札は 36、補足 (`sub`) を持つ札は 68 で描く
|
|
200
|
+
* (`@cardenelabs/cdl` の `LABEL_PILL_H_MAIN_ONLY` / `LABEL_PILL_H_WITH_SUB`、公開されていない)。
|
|
201
|
+
* 描く側が値を変えたら、ここも合わせる。
|
|
202
|
+
*/
|
|
203
|
+
const 線の札の高さ = (sub: string | undefined): number => ((sub ?? "") === "" ? 40 : 68);
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* 真横に渡る線と札の上端の間。見本の「いいえ」は線のすぐ下に札を置く。
|
|
207
|
+
*
|
|
208
|
+
* 描く側が線と札の間に求める最小の間 (`LABEL_TO_PATH_CLEARANCE` = 16) と同じにする。
|
|
209
|
+
* これより詰めると、描く側が札を線から押し出して列の下端より下へ逃がす。
|
|
210
|
+
*/
|
|
211
|
+
const 札を線の下に置く間 = 16;
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* 段の箱の線に渡す指定。通し方 (`段の箱の線の通し方`) と、札の置き場所を返す。
|
|
215
|
+
*
|
|
216
|
+
* 列をまたぐ線と戻り線の札は線の上に重ねる (`overlay`)。見本は「はい」を曲線の上に、
|
|
217
|
+
* 「翌日もう一度」を戻り線の上に置く。
|
|
218
|
+
*
|
|
219
|
+
* 同じ段の札へ真横に渡る線だけは、札を線のすぐ下へずらす (見本の「いいえ」)。描く側は
|
|
220
|
+
* 札を線から離して置き、同じ分かれ道から出る曲線の札と重ならない側へ逃がすため、
|
|
221
|
+
* 何もしないと札が列の下端より下へ出る。書いた `overlay` / `labelOffsetY` は後段で上書きされる。
|
|
222
|
+
*/
|
|
223
|
+
export function 段の箱の線の指定(
|
|
224
|
+
placement: StagesPlacement,
|
|
225
|
+
step: Pick<DslStep, "from" | "to" | "label" | "sub">,
|
|
226
|
+
): {
|
|
227
|
+
routing?: "curve" | "back-detour";
|
|
228
|
+
overlay?: true;
|
|
229
|
+
labelOffsetY?: number;
|
|
230
|
+
fromSide?: "right";
|
|
231
|
+
toSide?: "top";
|
|
232
|
+
} {
|
|
233
|
+
const routing = 段の箱の線の通し方(placement, step.from, step.to);
|
|
234
|
+
const 札がある = step.label !== "" || (step.sub ?? "") !== "";
|
|
235
|
+
if (routing === undefined) return {};
|
|
236
|
+
const 戻る辺 = routing === "back-detour" ? { fromSide: "right" as const, toSide: "top" as const } : {};
|
|
237
|
+
if (!札がある) return { routing, ...戻る辺 };
|
|
238
|
+
const 真横 =
|
|
239
|
+
routing === "curve" &&
|
|
240
|
+
placement.cards.get(step.from)?.stack === placement.cards.get(step.to)?.stack;
|
|
241
|
+
return {
|
|
242
|
+
routing,
|
|
243
|
+
...戻る辺,
|
|
244
|
+
overlay: true,
|
|
245
|
+
...(真横 ? { labelOffsetY: 線の札の高さ(step.sub) / 2 + 札を線の下に置く間 } : {}),
|
|
246
|
+
};
|
|
247
|
+
}
|
package/src/compile/subtitle.ts
CHANGED
|
@@ -11,8 +11,21 @@ import type { DslDocument } from "../types";
|
|
|
11
11
|
* 書かなければ何も渡さない = 従来どおり小見出しは付かない。 `undefined` を明示して渡すと、
|
|
12
12
|
* 組立て側が「空の小見出しを書いた」 と区別できなくなるので、 項目ごと落とす。
|
|
13
13
|
*/
|
|
14
|
-
export function 図の小見出し(
|
|
15
|
-
|
|
14
|
+
export function 図の小見出し(
|
|
15
|
+
doc: DslDocument,
|
|
16
|
+
): {
|
|
17
|
+
eyebrow?: string;
|
|
18
|
+
figureCard?: NonNullable<DslDocument["figureCard"]>;
|
|
19
|
+
cornerForm?: NonNullable<DslDocument["cornerForm"]>;
|
|
20
|
+
figureInset?: NonNullable<DslDocument["figureInset"]>;
|
|
21
|
+
} {
|
|
22
|
+
const figureInset = doc.figureInset === undefined ? undefined : { ...doc.figureInset };
|
|
23
|
+
return {
|
|
24
|
+
...(doc.eyebrow === undefined ? {} : { eyebrow: doc.eyebrow }),
|
|
25
|
+
...(doc.figureCard === undefined ? {} : { figureCard: doc.figureCard }),
|
|
26
|
+
...(doc.cornerForm === undefined ? {} : { cornerForm: doc.cornerForm }),
|
|
27
|
+
...(figureInset === undefined ? {} : { figureInset }),
|
|
28
|
+
};
|
|
16
29
|
}
|
|
17
30
|
|
|
18
31
|
/** 知らせに載せる値を短く切る。 長い URL をそのまま出すと画面の帯が読めなくなる */
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { SWIMLANE_SHAPES, type DslSwimlaneShape } from "../keywords";
|
|
2
|
+
import type { DslActor, DslDocument, DslStep } from "../types";
|
|
3
|
+
|
|
4
|
+
const 泳法図の印 = ["decision", "mark-start", "mark-end"] as const;
|
|
5
|
+
type 泳法図の印 = (typeof 泳法図の印)[number];
|
|
6
|
+
|
|
7
|
+
export type 泳法図の印の描き方 = "描く" | "札" | "省く";
|
|
8
|
+
|
|
9
|
+
/** 段の箱で、分かれ道を普通の箱に添えて見分ける字。 */
|
|
10
|
+
export const 分かれ道の札 = "分かれ道";
|
|
11
|
+
|
|
12
|
+
/** 泳法図の形と印ごとに、描画側へ渡す形を決める表。 */
|
|
13
|
+
export const 泳法図の印の描き方: Record<
|
|
14
|
+
DslSwimlaneShape,
|
|
15
|
+
Record<泳法図の印, 泳法図の印の描き方>
|
|
16
|
+
> = {
|
|
17
|
+
stages: {
|
|
18
|
+
decision: "札",
|
|
19
|
+
"mark-start": "省く",
|
|
20
|
+
"mark-end": "省く",
|
|
21
|
+
},
|
|
22
|
+
metro: {
|
|
23
|
+
decision: "描く",
|
|
24
|
+
"mark-start": "描く",
|
|
25
|
+
"mark-end": "描く",
|
|
26
|
+
},
|
|
27
|
+
timeline: {
|
|
28
|
+
decision: "描く",
|
|
29
|
+
"mark-start": "省く",
|
|
30
|
+
"mark-end": "描く",
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** 泳法図の3形で印をどう描くかを返し、それ以外の図と種類では返さない。 */
|
|
35
|
+
export function 泳法図で印をどう描く(
|
|
36
|
+
doc: Pick<DslDocument, "type" | "shape">,
|
|
37
|
+
actor: Pick<DslActor, "kind">,
|
|
38
|
+
): 泳法図の印の描き方 | undefined {
|
|
39
|
+
if (doc.type !== "swimlane") return undefined;
|
|
40
|
+
if (!(SWIMLANE_SHAPES as readonly string[]).includes(doc.shape ?? "")) return undefined;
|
|
41
|
+
if (!(泳法図の印 as readonly string[]).includes(actor.kind)) return undefined;
|
|
42
|
+
return 泳法図の印の描き方[doc.shape as DslSwimlaneShape][actor.kind as 泳法図の印];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 省く印を本文から外し、その印へ入る線と出る線を直接結ぶ。
|
|
47
|
+
*
|
|
48
|
+
* 繋いだ線は入る線の行と指定を保つ。説明だけは入る線が空なら出る線から受け継ぎ、
|
|
49
|
+
* 後段が元の行から色や線種を写せるようにする。
|
|
50
|
+
*/
|
|
51
|
+
export function 省く泳法図の印を外す(doc: DslDocument): DslDocument {
|
|
52
|
+
const 省く印 = doc.actors.filter((actor) => 泳法図で印をどう描く(doc, actor) === "省く");
|
|
53
|
+
if (省く印.length === 0) return doc;
|
|
54
|
+
|
|
55
|
+
let actors = doc.actors;
|
|
56
|
+
let flow = doc.flow;
|
|
57
|
+
for (const mark of 省く印) {
|
|
58
|
+
const 出る線 = flow.filter((step) => step.from === mark.name && step.to !== mark.name);
|
|
59
|
+
const 既に在る組 = new Set(
|
|
60
|
+
flow
|
|
61
|
+
.filter((step) => step.from !== mark.name && step.to !== mark.name)
|
|
62
|
+
.map((step) => `${step.from}\u0000${step.to}`),
|
|
63
|
+
);
|
|
64
|
+
const 繋ぎ直した: DslStep[] = [];
|
|
65
|
+
for (const step of flow) {
|
|
66
|
+
if (step.from !== mark.name && step.to !== mark.name) {
|
|
67
|
+
繋ぎ直した.push(step);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (step.to !== mark.name || step.from === mark.name) continue;
|
|
71
|
+
for (const next of 出る線) {
|
|
72
|
+
if (step.from === next.to) continue;
|
|
73
|
+
const 組 = `${step.from}\u0000${next.to}`;
|
|
74
|
+
if (既に在る組.has(組)) continue;
|
|
75
|
+
既に在る組.add(組);
|
|
76
|
+
繋ぎ直した.push({
|
|
77
|
+
...step,
|
|
78
|
+
to: next.to,
|
|
79
|
+
label: step.label === "" ? next.label : step.label,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
actors = actors.filter((actor) => actor !== mark);
|
|
84
|
+
flow = 繋ぎ直した;
|
|
85
|
+
}
|
|
86
|
+
return { ...doc, actors, flow };
|
|
87
|
+
}
|
package/src/compile/swimlane.ts
CHANGED
|
@@ -1,22 +1,24 @@
|
|
|
1
1
|
import { swimlane } from "@cardenelabs/cdl";
|
|
2
2
|
import type { CdlDiagram } from "@cardenelabs/cdl";
|
|
3
3
|
import type { DslDocument } from "../types";
|
|
4
|
-
import { compileGenericWithAnimate } from "./generic";
|
|
4
|
+
import { compileGenericWithAnimate, 共通の組み立てへ回す } from "./generic";
|
|
5
5
|
import { 描ける種別 } from "./kinds";
|
|
6
|
-
import { 書いた縦列に置く } from "./lanes";
|
|
7
6
|
import { 箱の題 } from "./node-title";
|
|
8
7
|
import { slugify } from "./slug";
|
|
8
|
+
import { STAGE_LANE_WIDTH } from "./stages";
|
|
9
9
|
export function compileSwimlane(doc: DslDocument): CdlDiagram {
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
// 形を指定した泳法図は動き・縦列・向きの有無で経路を分けない (#2797 / #2799)。
|
|
11
|
+
// 段の箱の列と札の決め方は `compile/stages.ts` が持つ (#2831)。
|
|
12
|
+
if (doc.shape === "stages" || doc.shape === "metro" || doc.shape === "timeline") {
|
|
13
|
+
return compileGenericWithAnimate(doc, {
|
|
14
|
+
kind: "swimlane",
|
|
15
|
+
laneWidth: doc.shape === "stages" ? STAGE_LANE_WIDTH : 400,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
// 動きを書いた形と縦列を書いた形は共通の組み立てへ (#1263 / #2348 で 1 箇所にまとめた)。
|
|
19
|
+
// **向きを書いた形も** (#1494)。 静止図の経路は並びを固定で持つので、
|
|
14
20
|
// ここを通さないと書いた向きが黙って消える (縦列を書いた形と同じ理由)
|
|
15
|
-
if (
|
|
16
|
-
(doc.animate && doc.animate.phases.length > 0) ||
|
|
17
|
-
書いた縦列に置く("swimlane", doc) ||
|
|
18
|
-
doc.direction !== undefined
|
|
19
|
-
) {
|
|
21
|
+
if (共通の組み立てへ回す("swimlane", doc, doc.direction !== undefined)) {
|
|
20
22
|
return compileGenericWithAnimate(doc, { kind: "swimlane", laneWidth: 400 });
|
|
21
23
|
}
|
|
22
24
|
// swimlane preset は lane 配置 + 自由 node/edge。
|
|
@@ -55,9 +57,6 @@ export function compileSwimlane(doc: DslDocument): CdlDiagram {
|
|
|
55
57
|
label: s.label,
|
|
56
58
|
...(s.sub ? { sub: s.sub } : {}),
|
|
57
59
|
...(s.side ? { side: s.side } : {}),
|
|
58
|
-
|
|
59
|
-
...(s.tone ? { tone: s.tone } : {}),
|
|
60
|
-
...(s.style ? { style: s.style } : {}),
|
|
61
60
|
});
|
|
62
61
|
}
|
|
63
62
|
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import type { DslDocument } from "../types";
|
|
2
|
+
import { slugify } from "./slug";
|
|
3
|
+
|
|
4
|
+
/** 見本の札と同じ横長の比率にする。 */
|
|
5
|
+
export const TIMELINE_CARD_WIDTH = 420;
|
|
6
|
+
export const TIMELINE_CARD_HEIGHT = 56;
|
|
7
|
+
|
|
8
|
+
/** 分かれ道の脇に置く札は、見本どおり通常の札より少し短くする。 */
|
|
9
|
+
export const TIMELINE_SIDE_CARD_WIDTH = 380;
|
|
10
|
+
|
|
11
|
+
/** 描く側の `timeline-number` が半径 24・縁 4 の丸を描く大きさ。 */
|
|
12
|
+
export const TIMELINE_NUMBER_SIZE = 52;
|
|
13
|
+
|
|
14
|
+
/** 描く側が四辺を 8 内側へ縮めるため、実描画を見本の 124 x 92 にする大きさ。 */
|
|
15
|
+
export const TIMELINE_DECISION_WIDTH = 140;
|
|
16
|
+
export const TIMELINE_DECISION_HEIGHT = 108;
|
|
17
|
+
|
|
18
|
+
/** 軸の lane は、軸上で最も広い菱形をはみ出させない幅にする。 */
|
|
19
|
+
export const TIMELINE_AXIS_WIDTH = TIMELINE_DECISION_WIDTH;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 描く側は終わりの印を短辺いっぱいに描き、輪の太さの半分だけ半径を縮める。
|
|
23
|
+
* `80 x 42` なら node-visibility の下限 80 x 40 を保ち、外輪の半径が 19 になる。
|
|
24
|
+
*/
|
|
25
|
+
export const TIMELINE_END_WIDTH = 80;
|
|
26
|
+
export const TIMELINE_END_HEIGHT = 42;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 軸から通常の札の内側までの距離。
|
|
30
|
+
* 見本と 7 意匠の実測に合わせ、札の内側を軸の中心から 70 離す。
|
|
31
|
+
*/
|
|
32
|
+
export const TIMELINE_AXIS_TO_CARD = 70;
|
|
33
|
+
|
|
34
|
+
/** 正解頁の軸 x=800 と「持ち戻る」の左辺 x=1020 から測った距離。 */
|
|
35
|
+
export const TIMELINE_AXIS_TO_SIDE_CARD = 220;
|
|
36
|
+
|
|
37
|
+
/** 見本どおり、番号の段を 125 ずつ進める。 */
|
|
38
|
+
export const TIMELINE_STEP_GAP = 125;
|
|
39
|
+
|
|
40
|
+
/** 7 意匠とも前後の和が 285 なので、CSS で意匠を替える見本帳へ届く一つの値を使う。 */
|
|
41
|
+
export const TIMELINE_DECISION_GAP = 142.5;
|
|
42
|
+
|
|
43
|
+
/** 見本どおり、最後の番号から終わりの印まで 105 空ける。 */
|
|
44
|
+
export const TIMELINE_END_GAP = 105;
|
|
45
|
+
|
|
46
|
+
/** 軸の左右の札と、戻る線の折れ曲がりを図の中へ収める余白。 */
|
|
47
|
+
const TIMELINE_SIDE_MARGIN = 160;
|
|
48
|
+
|
|
49
|
+
const AXIS_X = TIMELINE_SIDE_MARGIN + TIMELINE_CARD_WIDTH + TIMELINE_AXIS_TO_CARD;
|
|
50
|
+
|
|
51
|
+
/** 最初の札と番号の上側を図の中へ収める中心 y。 */
|
|
52
|
+
const FIRST_STEP_Y = 60;
|
|
53
|
+
|
|
54
|
+
const TIMELINE_WIDTH = AXIS_X * 2;
|
|
55
|
+
|
|
56
|
+
export type TimelineEdgeKind = "advance" | "back" | "branch";
|
|
57
|
+
|
|
58
|
+
export type TimelineStep = {
|
|
59
|
+
cardId: string;
|
|
60
|
+
numberId?: string;
|
|
61
|
+
cardX: number;
|
|
62
|
+
cardWidth: number;
|
|
63
|
+
stage: number;
|
|
64
|
+
y: number;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export type TimelinePlacement = {
|
|
68
|
+
axisX: number;
|
|
69
|
+
axisHeight: number;
|
|
70
|
+
width: number;
|
|
71
|
+
stepsHeight: number;
|
|
72
|
+
steps: TimelineStep[];
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/** 見た目の段の差で、進行線・戻り線・分かれ道を分ける。 */
|
|
76
|
+
export function classifyTimelineEdge(fromStage: number, toStage: number): TimelineEdgeKind {
|
|
77
|
+
if (toStage === fromStage + 1) return "advance";
|
|
78
|
+
if (toStage < fromStage) return "back";
|
|
79
|
+
return "branch";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 分かれ道だけから入る普通の段のうち、分かれ道の直後に書かれていない段を脇の札にする。
|
|
84
|
+
* 省く印は呼び出し前に除かれるため、配列の直前が「書いた順で次」の判定になる。
|
|
85
|
+
*/
|
|
86
|
+
function timelineSideCards(doc: Pick<DslDocument, "actors" | "flow">): Map<string, string> {
|
|
87
|
+
const actorsByName = new Map(doc.actors.map((actor) => [actor.name, actor]));
|
|
88
|
+
const sideCards = new Map<string, string>();
|
|
89
|
+
doc.actors.forEach((actor, index) => {
|
|
90
|
+
if (actor.kind === "decision" || actor.kind === "mark-end") return;
|
|
91
|
+
const incoming = doc.flow.filter((edge) => edge.to === actor.name);
|
|
92
|
+
if (incoming.length === 0) return;
|
|
93
|
+
if (!incoming.every((edge) => actorsByName.get(edge.from)?.kind === "decision")) return;
|
|
94
|
+
if (doc.actors[index - 1]?.kind === "decision") return;
|
|
95
|
+
const firstIncoming = incoming[0];
|
|
96
|
+
if (firstIncoming === undefined) return;
|
|
97
|
+
sideCards.set(actor.name, firstIncoming.from);
|
|
98
|
+
});
|
|
99
|
+
return sideCards;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function gapBefore(kind: string | undefined): number {
|
|
103
|
+
if (kind === "decision") return TIMELINE_DECISION_GAP;
|
|
104
|
+
if (kind === "mark-end") return TIMELINE_END_GAP;
|
|
105
|
+
return TIMELINE_STEP_GAP;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** 時間軸の絶対座標と、札に重ならない番号 id を返す。 */
|
|
109
|
+
export function placeTimeline(
|
|
110
|
+
doc: Pick<DslDocument, "actors" | "flow">,
|
|
111
|
+
軸に置く段: ReadonlySet<string> = new Set(),
|
|
112
|
+
): TimelinePlacement {
|
|
113
|
+
const sideCardSource = timelineSideCards(doc);
|
|
114
|
+
const cardIds = doc.actors.map((actor, index) => slugify(actor.name) || `n${index}`);
|
|
115
|
+
const usedIds = new Set(cardIds);
|
|
116
|
+
const stageByName = new Map<string, number>();
|
|
117
|
+
const yByName = new Map<string, number>();
|
|
118
|
+
let stage = -1;
|
|
119
|
+
let y = FIRST_STEP_Y;
|
|
120
|
+
let previousKind: string | undefined;
|
|
121
|
+
|
|
122
|
+
for (const actor of doc.actors) {
|
|
123
|
+
const source = sideCardSource.get(actor.name);
|
|
124
|
+
if (source !== undefined) {
|
|
125
|
+
const sourceStage = stageByName.get(source);
|
|
126
|
+
const sourceY = yByName.get(source);
|
|
127
|
+
if (sourceStage === undefined || sourceY === undefined) {
|
|
128
|
+
throw new Error(`時間軸の脇の札 "${actor.name}" の分かれ道 "${source}" がありません`);
|
|
129
|
+
}
|
|
130
|
+
stageByName.set(actor.name, sourceStage);
|
|
131
|
+
yByName.set(actor.name, sourceY);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
stage += 1;
|
|
135
|
+
if (stage > 0) {
|
|
136
|
+
y += previousKind === "decision" ? TIMELINE_DECISION_GAP : gapBefore(actor.kind);
|
|
137
|
+
}
|
|
138
|
+
stageByName.set(actor.name, stage);
|
|
139
|
+
yByName.set(actor.name, y);
|
|
140
|
+
previousKind = actor.kind;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const cardSideByName = new Map<string, "left" | "right">();
|
|
144
|
+
doc.actors
|
|
145
|
+
.map((actor, index) => ({ actor, index, y: yByName.get(actor.name) ?? FIRST_STEP_Y }))
|
|
146
|
+
.filter(({ actor }) => !軸に置く段.has(actor.name))
|
|
147
|
+
.sort((a, b) => a.y - b.y || a.index - b.index)
|
|
148
|
+
.forEach(({ actor }, index) => cardSideByName.set(actor.name, index % 2 === 0 ? "left" : "right"));
|
|
149
|
+
|
|
150
|
+
const cardLayoutByName = new Map<string, { centerX: number; width: number }>();
|
|
151
|
+
for (const actor of doc.actors) {
|
|
152
|
+
const sideCard = sideCardSource.has(actor.name);
|
|
153
|
+
const width = sideCard ? TIMELINE_SIDE_CARD_WIDTH : TIMELINE_CARD_WIDTH;
|
|
154
|
+
const axisToCard = sideCard ? TIMELINE_AXIS_TO_SIDE_CARD : TIMELINE_AXIS_TO_CARD;
|
|
155
|
+
const side = cardSideByName.get(actor.name) ?? "left";
|
|
156
|
+
cardLayoutByName.set(actor.name, {
|
|
157
|
+
centerX: side === "left"
|
|
158
|
+
? AXIS_X - axisToCard - width / 2
|
|
159
|
+
: AXIS_X + axisToCard + width / 2,
|
|
160
|
+
width,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
for (const actor of doc.actors) {
|
|
165
|
+
if (!sideCardSource.has(actor.name)) continue;
|
|
166
|
+
const source = cardLayoutByName.get(actor.name);
|
|
167
|
+
const sourceStage = stageByName.get(actor.name);
|
|
168
|
+
if (source === undefined || sourceStage === undefined) continue;
|
|
169
|
+
for (const edge of doc.flow.filter((item) => item.from === actor.name)) {
|
|
170
|
+
const target = cardLayoutByName.get(edge.to);
|
|
171
|
+
const targetStage = stageByName.get(edge.to);
|
|
172
|
+
if (target === undefined || targetStage === undefined || targetStage >= sourceStage) continue;
|
|
173
|
+
const targetLeft = target.centerX - target.width / 2;
|
|
174
|
+
const targetRight = target.centerX + target.width / 2;
|
|
175
|
+
if (source.centerX < targetLeft || source.centerX > targetRight) continue;
|
|
176
|
+
/*
|
|
177
|
+
* cardene777/cdl#1031: `back-detour` は札の上辺の中点どうしを結び、
|
|
178
|
+
* 両端の札を障害物に数えない。右側の脇札も線札の clearance を保つ位置へ置く。
|
|
179
|
+
*/
|
|
180
|
+
source.centerX = AXIS_X + TIMELINE_AXIS_TO_SIDE_CARD + source.width / 2;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
let 次の番号 = 1;
|
|
185
|
+
const steps = doc.actors.map((actor, index): TimelineStep => {
|
|
186
|
+
const sideCard = sideCardSource.has(actor.name);
|
|
187
|
+
const 番号 = 軸に置く段.has(actor.name) || sideCard ? undefined : 次の番号++;
|
|
188
|
+
const base = 番号 === undefined ? undefined : `timeline-number-${番号}`;
|
|
189
|
+
let numberId = base;
|
|
190
|
+
let suffix = 2;
|
|
191
|
+
while (numberId !== undefined && usedIds.has(numberId)) numberId = `${base}-${suffix++}`;
|
|
192
|
+
if (numberId !== undefined) usedIds.add(numberId);
|
|
193
|
+
const cardLayout = cardLayoutByName.get(actor.name);
|
|
194
|
+
if (cardLayout === undefined) throw new Error(`時間軸の札 "${actor.name}" の配置がありません`);
|
|
195
|
+
return {
|
|
196
|
+
cardId: cardIds[index]!,
|
|
197
|
+
numberId,
|
|
198
|
+
cardX: cardLayout.centerX,
|
|
199
|
+
cardWidth: cardLayout.width,
|
|
200
|
+
stage: stageByName.get(actor.name) ?? index,
|
|
201
|
+
y: yByName.get(actor.name) ?? FIRST_STEP_Y,
|
|
202
|
+
};
|
|
203
|
+
});
|
|
204
|
+
const lastY = Math.max(FIRST_STEP_Y, ...steps.map((step) => step.y));
|
|
205
|
+
const rightEdge = Math.max(...steps.map((step) => step.cardX + step.cardWidth / 2));
|
|
206
|
+
return {
|
|
207
|
+
axisX: AXIS_X,
|
|
208
|
+
axisHeight: lastY + TIMELINE_END_HEIGHT / 2,
|
|
209
|
+
width: Math.max(TIMELINE_WIDTH, rightEdge + TIMELINE_SIDE_MARGIN),
|
|
210
|
+
stepsHeight: lastY + TIMELINE_CARD_HEIGHT / 2 + TIMELINE_SIDE_MARGIN,
|
|
211
|
+
steps,
|
|
212
|
+
};
|
|
213
|
+
}
|
package/src/compile/topology.ts
CHANGED
|
@@ -3,17 +3,14 @@ import { 箱の題 } from "./node-title";
|
|
|
3
3
|
import type { CdlDiagram } from "@cardenelabs/cdl";
|
|
4
4
|
import type { DslDocument } from "../types";
|
|
5
5
|
|
|
6
|
-
import { compileGenericWithAnimate } from "./generic";
|
|
6
|
+
import { compileGenericWithAnimate, 共通の組み立てへ回す } from "./generic";
|
|
7
7
|
import { 描ける種別 } from "./kinds";
|
|
8
|
-
import { 書いた縦列に置く } from "./lanes";
|
|
9
8
|
|
|
10
9
|
import { slugify } from "./slug";
|
|
11
10
|
|
|
12
11
|
export function compileTopology(doc: DslDocument): CdlDiagram {
|
|
13
|
-
//
|
|
14
|
-
|
|
15
|
-
// 同じ記法でも静止図では指定が黙って消える (実測 = 縦列 3 本のはずが 1 本になり知らせも出ない)
|
|
16
|
-
if ((doc.animate && doc.animate.phases.length > 0) || 書いた縦列に置く("topology", doc)) {
|
|
12
|
+
// 動きを書いた形と縦列を書いた形は共通の組み立てへ (#1263 / #2348 で 1 箇所にまとめた)
|
|
13
|
+
if (共通の組み立てへ回す("topology", doc)) {
|
|
17
14
|
return compileGenericWithAnimate(doc, { kind: "topology", laneWidth: 460 });
|
|
18
15
|
}
|
|
19
16
|
// 登場人物が 0 人なら枠も作らない。 描画側の `topology()` は枠を必ず 1 つ作るため、 そのまま
|
|
@@ -41,9 +38,6 @@ export function compileTopology(doc: DslDocument): CdlDiagram {
|
|
|
41
38
|
label: s.label,
|
|
42
39
|
...(s.sub ? { sub: s.sub } : {}),
|
|
43
40
|
...(s.side ? { side: s.side } : {}),
|
|
44
|
-
|
|
45
|
-
...(s.tone ? { tone: s.tone } : {}),
|
|
46
|
-
...(s.style ? { style: s.style } : {}),
|
|
47
41
|
});
|
|
48
42
|
}
|
|
49
43
|
return topo.build();
|