@cardenelabs/dragon 0.28.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.
Files changed (66) hide show
  1. package/README.md +391 -16
  2. package/dist/index.cjs +13479 -7403
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +2360 -208
  5. package/dist/index.d.ts +2360 -208
  6. package/dist/index.js +13462 -7405
  7. package/dist/index.js.map +1 -1
  8. package/package.json +12 -13
  9. package/src/compile/actor-ids.ts +297 -0
  10. package/src/compile/actor-option-notice.ts +379 -0
  11. package/src/compile/actors.ts +103 -0
  12. package/src/compile/c4.ts +74 -0
  13. package/src/compile/chain.ts +93 -0
  14. package/src/compile/chart-fields.ts +193 -0
  15. package/src/compile/deep-rewrite.ts +30 -0
  16. package/src/compile/derived-source.ts +18 -0
  17. package/src/compile/direction.ts +102 -0
  18. package/src/compile/edge-option-notice.ts +112 -0
  19. package/src/compile/er-relation.ts +200 -0
  20. package/src/compile/flow.ts +54 -0
  21. package/src/compile/flowchart.ts +344 -0
  22. package/src/compile/funnel.ts +87 -0
  23. package/src/compile/gantt.ts +337 -0
  24. package/src/compile/generic.ts +696 -0
  25. package/src/compile/hierarchy.ts +92 -0
  26. package/src/compile/journey.ts +93 -0
  27. package/src/compile/kinds.ts +148 -0
  28. package/src/compile/lanes.ts +138 -0
  29. package/src/compile/metro.ts +271 -0
  30. package/src/compile/mind.ts +393 -0
  31. package/src/compile/node-title.ts +24 -0
  32. package/src/compile/notice.ts +141 -0
  33. package/src/compile/order.ts +62 -0
  34. package/src/compile/parts-lookup.ts +17 -0
  35. package/src/compile/parts.ts +2709 -0
  36. package/src/compile/placement-apply.ts +689 -0
  37. package/src/compile/placement.ts +15 -0
  38. package/src/compile/quadrant.ts +265 -0
  39. package/src/compile/record.ts +75 -0
  40. package/src/compile/row-marks.ts +128 -0
  41. package/src/compile/rows.ts +40 -0
  42. package/src/compile/sequence.ts +305 -0
  43. package/src/compile/slug.ts +37 -0
  44. package/src/compile/stages.ts +247 -0
  45. package/src/compile/subtitle.ts +35 -0
  46. package/src/compile/swimlane-marks.ts +87 -0
  47. package/src/compile/swimlane.ts +82 -0
  48. package/src/compile/timeline.ts +213 -0
  49. package/src/compile/topology.ts +44 -0
  50. package/src/compile/tree.ts +69 -0
  51. package/src/compile/value-chart.ts +337 -0
  52. package/src/compile/values.ts +432 -0
  53. package/src/compile/word-state.ts +82 -0
  54. package/src/compile.ts +1806 -6971
  55. package/src/focus.ts +2 -2
  56. package/src/index.ts +42 -9
  57. package/src/json-line-map.ts +30 -0
  58. package/src/json-parser.ts +1760 -168
  59. package/src/keywords.ts +251 -17
  60. package/src/near-name.ts +42 -0
  61. package/src/notation-lint.ts +41 -64
  62. package/src/relative-pos.ts +71 -2
  63. package/src/schema.ts +1 -1
  64. package/src/schemas/diagram.json +680 -23
  65. package/src/types.ts +337 -36
  66. package/src/v05/parser.ts +2553 -354
@@ -0,0 +1,305 @@
1
+ import { sequence, sequenceStepId } from "@cardenelabs/cdl";
2
+ import type { CdlDiagram } from "@cardenelabs/cdl";
3
+ import type { DslActor, DslDocument } from "../types";
4
+
5
+ import { parseFocusEntry } from "../focus";
6
+ import { 並べ替えた文書 } from "./order";
7
+ import { 箱の題 } from "./node-title";
8
+
9
+ import { slugify } from "./slug";
10
+
11
+ export function compileSequence(doc: DslDocument): CdlDiagram {
12
+ // **書いた並び順を先に当てる** (#2655)。 以降は並べ替え済の箱で組む = 板の見出しも
13
+ // 端の番号も同じ並びから決まる。 書かなかった図は同じ文書がそのまま返る
14
+ doc = 並べ替えた文書(doc);
15
+ // v0.3 ... アニメーション 有無で経路を分岐。
16
+ // 有り = builder 直接経路で state / 複数 phase を注入。
17
+ /*
18
+ * **段があっても組み立て器へ渡す** (#1466)。
19
+ *
20
+ * 順序図は 1 つの箱が図を丸ごと描く形になり、言づては箱の中の行になった。 段ごとに
21
+ * 別経路で箱と縦線を組む形 (`compileSequenceWithAnimate`) では、その骨格が出ない。
22
+ */
23
+ /*
24
+ * **見出しと端の番号を別の組み立てで決める** (#2408)。
25
+ *
26
+ * 見出しに出すのは書いた題 (#1466)、 矢印の端と帯は名前で指す (`箱の題` の doc comment)。
27
+ * 組み立て器は渡された面の名前 1 つから「見出し」 と「端を引く表」 の両方を作るため、
28
+ * 題を渡すと名前で指した端が 1 度も見つからず、 番号 0 (先頭の面) に倒れる
29
+ * (実測 = 全ての面に題を付けると、 言づてが全て先頭の面の自分宛てになった)。
30
+ *
31
+ * 板の幅と高さは見出しの字幅と言づての文字と件数から決まり、 端の番号には依らない
32
+ * (組み立て器の `sequenceBoardMetrics`)。 そこで題で組んだ板を土台にし、 端の番号と帯だけを
33
+ * 名前で組んだ板から写す。 名前は図の中で 1 つに決まるので、 題が重なる図でも番号は正しい。
34
+ *
35
+ * 題と名前がどの面でも同じなら 2 回目は組まない = 題を書かない図は組み上がりが変わらない。
36
+ */
37
+ const 見出しの板 = 板を組む(doc, 箱の題);
38
+ const built = doc.actors.some((a) => 箱の題(a) !== a.name)
39
+ ? 端を写す(見出しの板, 板を組む(doc, (a) => a.name))
40
+ : 見出しの板;
41
+ const 段 = doc.animate?.phases ?? [];
42
+ if (段.length === 0) return built;
43
+ /*
44
+ * 書いた段を「今どの言づてか」 に読み替える (#1466)。
45
+ *
46
+ * 記法は段ごとに光らせる矢印を並べる (`focus: [A -> B, ...]`) が、言づては矢印ではなく
47
+ * 箱の中の行になった = 光らせる先が無い。 代わりに **その段までに出た言づての番号** を
48
+ * 状態へ書き、箱がそこまでを描く。 番号の導き方は `段の番号` が持つ。
49
+ */
50
+ const 番号 = 段の番号(doc);
51
+ const 状態名 = sequenceStepId();
52
+ return {
53
+ ...built,
54
+ states: [...built.states, { id: 状態名, initial: "0" }],
55
+ phases: 段.map((p, i) => {
56
+ const 番 = 番号[i]!;
57
+ return {
58
+ id: `p${i}`,
59
+ duration: p.durationMs,
60
+ title: p.name,
61
+ body: p.body ?? "",
62
+ activate: [slugify(doc.title)],
63
+ ...(p.badge ? { badge: p.badge } : {}),
64
+ /*
65
+ * **書いた状態の動きも一緒に運ぶ** (#1466)。 板の段は「今どの言づてか」 を状態に
66
+ * 書くが、記法は同じ段に `遷移:` / `切替:` も書ける。 板の分だけを載せると、
67
+ * 書いた動きが黙って落ちる (実測で `tweens` が 0 件になっていた)。
68
+ */
69
+ sets: [
70
+ ...(p.sets ?? []).map((x) => ({ stateId: x.state, value: x.value })),
71
+ { stateId: 状態名, value: 番 },
72
+ ],
73
+ tweens: (p.tweens ?? []).map((t) => ({ stateId: t.state, from: t.from, to: t.to })),
74
+ };
75
+ }),
76
+ };
77
+ }
78
+
79
+ /**
80
+ * 順序図の板を 1 枚組む。 面の名前として組み立て器へ渡すものだけを `面の名` で差し替える。
81
+ *
82
+ * 組み立て器はこの名前から見出しと「端を引く表」 を作る。 言づての端と帯は書いた名前のまま渡す。
83
+ */
84
+ function 板を組む(doc: DslDocument, 面の名: (a: DslActor) => string): CdlDiagram {
85
+ const b = sequence({
86
+ id: slugify(doc.title),
87
+ topic: doc.title,
88
+ actors: doc.actors.map((a) =>
89
+ a.subtitle ? { name: 面の名(a), subtitle: a.subtitle } : 面の名(a),
90
+ ),
91
+ ...(doc.bands && doc.bands.length > 0 ? { bands: doc.bands } : {}),
92
+ ...(doc.sequenceActorForm !== undefined ? { sequenceActorForm: doc.sequenceActorForm } : {}),
93
+ ...(doc.sequenceLabelForm !== undefined ? { sequenceLabelForm: doc.sequenceLabelForm } : {}),
94
+ ...(doc.sequenceReturnHead !== undefined ? { sequenceReturnHead: doc.sequenceReturnHead } : {}),
95
+ ...(doc.sequenceKindLabels !== undefined ? { sequenceKindLabels: doc.sequenceKindLabels } : {}),
96
+ ...(doc.sequenceRowGap !== undefined ? { sequenceRowGap: doc.sequenceRowGap } : {}),
97
+ ...(doc.sequenceGutter !== undefined ? { sequenceGutter: doc.sequenceGutter } : {}),
98
+ ...(doc.sequenceBandWidth !== undefined ? { sequenceBandWidth: doc.sequenceBandWidth } : {}),
99
+ ...(doc.sequenceSelfLoop !== undefined ? { sequenceSelfLoop: doc.sequenceSelfLoop } : {}),
100
+ });
101
+ for (const s of doc.flow) {
102
+ b.step({
103
+ from: s.from,
104
+ to: s.to,
105
+ label: s.label,
106
+ ...(s.sub ? { sub: s.sub } : {}),
107
+ ...(s.side ? { side: s.side } : {}),
108
+ ...(s.tone ? { tone: s.tone } : {}),
109
+ ...(s.style ? { style: s.style } : {}),
110
+ ...(s.msgKind ? { kind: s.msgKind } : {}),
111
+ });
112
+ }
113
+ const built = b.build();
114
+ if (doc.sequenceGutter === undefined || doc.viewport?.width === undefined) return built;
115
+ const canvasWidth = doc.viewport.width;
116
+
117
+ /*
118
+ * `sequenceGutter` は見本の canvas 原点から最初の糸を決める。cdl の sequence preset は
119
+ * 板の左右へさらに lane padding を足すため、そのままでは gutter 610 の糸が 25 右へずれ、
120
+ * 1800 の canvas も 1948 まで広がる。幅を書いた図では lane の左右 25 ずつを除いた幅を
121
+ * 板へ渡し、layout 後の lane を canvas 幅に揃える。gutter + 列幅 / 2 は板の原点から測る。
122
+ * 幅を書かない従来図は cdl の自動幅を保つ。
123
+ */
124
+ return {
125
+ ...built,
126
+ lanes: built.lanes.map((lane) => lane.id === "seq" ? { ...lane, width: canvasWidth } : lane),
127
+ nodes: built.nodes.map((node) => node.kind === "sequence-board"
128
+ ? { ...node, w: Math.max(1, canvasWidth - 50) }
129
+ : node),
130
+ };
131
+ }
132
+
133
+ /**
134
+ * 見出しで組んだ板へ、 名前で組んだ板の「言づての端」 と「帯」 を写す (#2408)。
135
+ *
136
+ * 写すのはこの 2 つだけ。 板の大きさ・見出し・言づての文字は見出しで組んだ板のものを使う。
137
+ * 帯は書かない時も組み立て器が言づての端から導くため、 端と一緒に写さないと導いた帯がずれる。
138
+ */
139
+ function 端を写す(見出し: CdlDiagram, 名前: CdlDiagram): CdlDiagram {
140
+ const 名前の板 = 名前.nodes.find((n) => n.kind === "sequence-board")?.sequenceData;
141
+ if (!名前の板) return 見出し;
142
+ return {
143
+ ...見出し,
144
+ nodes: 見出し.nodes.map((n) => {
145
+ if (n.kind !== "sequence-board" || !n.sequenceData) return n;
146
+ return {
147
+ ...n,
148
+ sequenceData: {
149
+ ...n.sequenceData,
150
+ messages: n.sequenceData.messages.map((m, i) => {
151
+ const 正 = 名前の板.messages[i];
152
+ return 正 ? { ...m, from: 正.from, to: 正.to } : m;
153
+ }),
154
+ bands: 名前の板.bands,
155
+ },
156
+ };
157
+ }),
158
+ };
159
+ }
160
+
161
+ /** 名前が同じ箱を指すか。 書いた名前のほか、 id の形 (`api-gateway`) で書いた指定も受ける */
162
+ function 同じ箱(a: string, b: string): boolean {
163
+ return a === b || slugify(a) === slugify(b);
164
+ }
165
+
166
+ type 言づて = { from: string; to: string };
167
+
168
+ /**
169
+ * 段の `focus:` を、 言づてが合うかを返す関数にする。 何も書いていない段は `undefined`。
170
+ *
171
+ * | 書いたもの | 合う言づて |
172
+ * |---|---|
173
+ * | 矢印 `A -> B` | 元が A で先が B |
174
+ * | 箱の名前 2 つ以上 | 元と先の両方が書いた箱に含まれる |
175
+ * | 箱の名前 1 つ | 元か先がその箱 |
176
+ *
177
+ * 矢印は 1 本ずつ、 箱の名前はまとめて 1 件として扱い、 どれか 1 つに合えば合うとする。
178
+ *
179
+ * **矢印を 1 本でも書いた段は、 矢印だけで言づてを選ぶ**。 同じ段に並べた箱の名前は縦線を
180
+ * 光らせるための指定で、 言づてを選ぶには広すぎる。 `["利用者側 -> 処理側", 利用者側]` の
181
+ * 箱の名前まで数えると、 利用者側が関わる全ての言づてに合い、 矢印で指した言づてが決まらない
182
+ * (見本 `websocket` / `retryBackoff` で実測)。
183
+ */
184
+ function 合い方(
185
+ focus: readonly string[],
186
+ 箱の名前: ReadonlySet<string>,
187
+ ): ((f: 言づて) => boolean) | undefined {
188
+ const 合う: ((f: 言づて) => boolean)[] = [];
189
+ const 名前: string[] = [];
190
+ for (const raw of focus) {
191
+ const e = parseFocusEntry(raw, 箱の名前);
192
+ if (e.kind === "edge") 合う.push((f) => 同じ箱(f.from, e.from) && 同じ箱(f.to, e.to));
193
+ else 名前.push(e.name);
194
+ }
195
+ const 矢印がある = 合う.length > 0;
196
+ if (!矢印がある && 名前.length === 1) {
197
+ const n = 名前[0]!;
198
+ 合う.push((f) => 同じ箱(f.from, n) || 同じ箱(f.to, n));
199
+ } else if (!矢印がある && 名前.length >= 2) {
200
+ const 含む = (x: string) => 名前.some((n) => 同じ箱(x, n));
201
+ 合う.push((f) => 含む(f.from) && 含む(f.to));
202
+ }
203
+ return 合う.length === 0 ? undefined : (f) => 合う.some((c) => c(f));
204
+ }
205
+
206
+ /**
207
+ * 切り分けの良さ。 前から順に比べ、 大きい方を良いとする。
208
+ *
209
+ * [合う言づてを 1 通以上持つ段の数, 段に合う言づての数, 合った言づてを置いた段の広さの合計に負号を付けたもの]
210
+ */
211
+ type 得点 = readonly [number, number, number];
212
+
213
+ function 上回る(x: 得点, y: 得点): boolean {
214
+ for (let i = 0; i < x.length; i++) if (x[i] !== y[i]) return x[i]! > y[i]!;
215
+ return false;
216
+ }
217
+
218
+ /**
219
+ * 段ごとに、 板が今どの言づてを強調するかを導く (#2133)。
220
+ *
221
+ * 板は番号の言づてを強調し、 それより前を描き済みとして残し、 後ろを隠す。
222
+ *
223
+ * **言づてを先頭から段の数に切り分ける**。 `focus:` を書いた段ごとに続きの言づてを 1 区間ずつ
224
+ * 受け持たせ、 次の順で最も良い切り分けを選ぶ。
225
+ *
226
+ * 1. 合う言づてを 1 通以上持つ段が多い (どの段も板を進める)
227
+ * 2. 段に合う言づての数が多い (段が束ねる言づてを、 束ねた段に置く)
228
+ * 3. 両方の段に合う言づてを、 狭く指した段に置く。 段の広さは、 図の全ての言づてのうち
229
+ * その段に合う数
230
+ * 4. それでも並んだら、 前の段の区間を短くする
231
+ *
232
+ * 段の番号は、 その区間で `focus:` に合う最後の言づてにする。 箱の名前で書いた段は複数の
233
+ * 言づてを束ね、 見本の段の題は束の最後を指している (`[利用者, 認証窓口, DB]` の段の題は 2 通目の
234
+ * 「照合」)。
235
+ *
236
+ * 前の段から順に「合う最初の言づて」 を取る形では、 次の 3 つを同時に解けない。
237
+ *
238
+ * | 形 | 段の `focus:` | 起きること | 決める順 |
239
+ * |---|---|---|---|
240
+ * | 見本 `cacheReadThrough` (直す前) | `[利用者側, API, 一時置き場, DB]` → `[API, 一時置き場]` | 次の段が 2 通目 (探す) を先に取り、 前の段が 1 通目で止まる | 2 |
241
+ * | 見本 `rateLimit` | `[利用者側, 流量制限, API]` → `[利用者側, 流量制限]` | 5 通目は両方に合う。 前の段が取ると、 次の段の題「超過」 の前の要求が前の段で出る | 3 |
242
+ * | まとめの段 | `[C -> A]` → `[A -> B, B -> C, C -> A]` | 3 通目は両方に合う。 まとめの段が取ると `C -> A` の段が進まない | 3 |
243
+ *
244
+ * **合う言づてが無い段は前の段の番号を保つ**。 見出しだけの段や、 描いた矢印をまとめて並べる
245
+ * 段で板が 0 へ巻き戻らないようにする。
246
+ *
247
+ * 以前は「元 -> 先」 の組から番号を引く表だけを持っていた。 箱の名前は表に無いので全ての段が
248
+ * 0 になり (カタログの見本 30 件のうち 26 件)、 同じ組の言づてが複数あると表が最後の番号で
249
+ * 上書きされて、 最初の段で最後の言づてまで飛んでいた (3 件)。
250
+ */
251
+ export function 段の番号(doc: DslDocument): number[] {
252
+ const 段 = doc.animate?.phases ?? [];
253
+ const 箱の名前 = new Set(doc.actors.map((a) => a.name));
254
+ const 合う = 段.map((p) => 合い方(p.highlight ?? [], 箱の名前));
255
+ const 対象 = 合う.flatMap((c, k) => (c ? [k] : []));
256
+ const n = doc.flow.length;
257
+ const m = 対象.length;
258
+
259
+ // 累計[t][i] = 対象 t の段に、 先頭から i 通のうち何通が合うか。 累計[t][n] がその段の広さ
260
+ const 累計 = 対象.map((k) => {
261
+ const c = 合う[k]!;
262
+ const a = [0];
263
+ doc.flow.forEach((f, i) => a.push(a[i]! + (c(f) ? 1 : 0)));
264
+ return a;
265
+ });
266
+
267
+ // 最良[t][a] = 対象 t 以降の段で a 通目以降を切り分けた時の最良。 切れ目[t][a] = 対象 t の区間の終わり
268
+ const 最良: 得点[][] = Array.from({ length: m + 1 }, () => Array<得点>(n + 1).fill([0, 0, 0]));
269
+ const 切れ目: number[][] = Array.from({ length: m }, () => Array<number>(n + 1).fill(n));
270
+ for (let t = m - 1; t >= 0; t--) {
271
+ const 広さ = 累計[t]![n]!;
272
+ for (let a = 0; a <= n; a++) {
273
+ // 最後の段は残りを全て受け持つ。 短い区間から試し、 上回った時だけ置き換える (並んだら短い方)
274
+ for (let b = t === m - 1 ? n : a; b <= n; b++) {
275
+ const 数 = 累計[t]![b]! - 累計[t]![a]!;
276
+ const 次 = 最良[t + 1]![b]!;
277
+ const s: 得点 = [(数 > 0 ? 1 : 0) + 次[0], 数 + 次[1], -数 * 広さ + 次[2]];
278
+ if (b === (t === m - 1 ? n : a) || 上回る(s, 最良[t]![a]!)) {
279
+ 最良[t]![a] = s;
280
+ 切れ目[t]![a] = b;
281
+ }
282
+ }
283
+ }
284
+ }
285
+
286
+ const 番号 = new Map<number, number>();
287
+ let a = 0;
288
+ for (let t = 0; t < m; t++) {
289
+ const b = 切れ目[t]![a]!;
290
+ const c = 合う[対象[t]!]!;
291
+ for (let j = b - 1; j >= a; j--) {
292
+ if (c(doc.flow[j]!)) {
293
+ 番号.set(対象[t]!, j);
294
+ break;
295
+ }
296
+ }
297
+ a = b;
298
+ }
299
+
300
+ let 今 = 0;
301
+ return 段.map((_, k) => {
302
+ 今 = 番号.get(k) ?? 今;
303
+ return 今;
304
+ });
305
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * 名前から id を作る (#2030 で `compile.ts` から移した)。
3
+ *
4
+ * この repo で最も広く使われる小道具で、組み立ての全域から 37 箇所が呼ぶ。
5
+ *
6
+ * **葉に置く** = ここが他の file を 1 つも取り込まないことで、どこから呼んでも取り込みの輪が
7
+ * できない。 図種ごとの file と `compile.ts` の両方が呼ぶため、どちらかの側に置くと
8
+ * もう片方から逆向きの取り込みが生まれる。
9
+ */
10
+
11
+ /** id の長さの上限。 これを超える名前は切り詰める */
12
+ export const ID_MAX = 64;
13
+
14
+ /**
15
+ * 名前を id に直す。
16
+ *
17
+ * 英数字と日本語と `-` `_` だけを残し、それ以外は `-` にまとめる。 前後の `-` は落とす。
18
+ * 何も残らなければ `n` を返す = 空の id は図の参照先にできないため。
19
+ *
20
+ * **カタカナは伸ばす音 (`ー`) まで残す** (#2431)。 `ヶ` (U+30F6) で切ると `ー` (U+30FC) が
21
+ * 範囲の外へ落ち、`フロー` が `フロ-` になる。 読めない字が一覧に出るだけでなく、
22
+ * `ユーザー` と `ユーザ` が同じ `ユ-ザ` に潰れる (末尾の `-` も落ちるため完全に一致する)。
23
+ * 矢印は id で端を指すので、潰れると別の箱へ向かう。
24
+ *
25
+ * **区切りに使う記号は残さない**。 `・` (U+30FB) と `ヽヾ` (U+30FD-E) は範囲の外に置く =
26
+ * `東京・大阪` の `・` は名前の区切りで、字として残すと 1 つの名前に見える。
27
+ */
28
+ export function slugify(s: string): string {
29
+ return (
30
+ s
31
+ .toLowerCase()
32
+ .normalize("NFKC")
33
+ .replace(/[^a-z0-9ぁ-んァ-ヺー一-龯\-_]+/g, "-")
34
+ .replace(/^-+|-+$/g, "")
35
+ .slice(0, ID_MAX) || "n"
36
+ );
37
+ }
@@ -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
+ }
@@ -0,0 +1,35 @@
1
+ import type { DslDocument } from "../types";
2
+ /**
3
+ * 図の小見出しと、知らせに出す字の切り詰め (#2030 で `compile.ts` から移した)。
4
+ *
5
+ * 7 図種が小見出しを使う。 切り詰めは知らせを出す全域が使う。
6
+ */
7
+
8
+ /**
9
+ * 図全体を 1 箱にする図種で、 その箱の上に出す小見出しを渡す (#1247)。
10
+ *
11
+ * 書かなければ何も渡さない = 従来どおり小見出しは付かない。 `undefined` を明示して渡すと、
12
+ * 組立て側が「空の小見出しを書いた」 と区別できなくなるので、 項目ごと落とす。
13
+ */
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
+ };
29
+ }
30
+
31
+ /** 知らせに載せる値を短く切る。 長い URL をそのまま出すと画面の帯が読めなくなる */
32
+ export function truncateForMessage(v: string): string {
33
+ const s = v.trim();
34
+ return s.length <= 40 ? s : `${s.slice(0, 37)}...`;
35
+ }