@cardenelabs/dragon 0.33.0 → 0.35.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 (126) hide show
  1. package/README.md +39 -720
  2. package/dist/cli.cjs +4529 -0
  3. package/dist/cli.cjs.map +1 -0
  4. package/dist/cli.js +4523 -0
  5. package/dist/cli.js.map +1 -0
  6. package/dist/export.cjs +3912 -0
  7. package/dist/export.cjs.map +1 -0
  8. package/dist/export.d.cts +36 -0
  9. package/dist/export.d.ts +36 -0
  10. package/dist/export.js +3907 -0
  11. package/dist/export.js.map +1 -0
  12. package/dist/fonts.cjs +7 -0
  13. package/dist/fonts.cjs.map +1 -0
  14. package/dist/fonts.js +4 -0
  15. package/dist/fonts.js.map +1 -0
  16. package/dist/index.cjs +4105 -20299
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.d.cts +227 -5524
  19. package/dist/index.d.ts +227 -5524
  20. package/dist/index.js +4091 -20226
  21. package/dist/index.js.map +1 -1
  22. package/dist/player.global.js +2 -0
  23. package/dist/schema-BBWfniwH.d.cts +112 -0
  24. package/dist/schema-BBWfniwH.d.ts +112 -0
  25. package/package.json +39 -24
  26. package/src/cli.ts +237 -0
  27. package/src/export/capture.ts +277 -0
  28. package/src/export/fonts.ts +68 -0
  29. package/src/export/html.ts +32 -0
  30. package/src/export/index.ts +146 -0
  31. package/src/field-messages.ts +104 -0
  32. package/src/figure-text.ts +42 -0
  33. package/src/index.ts +30 -283
  34. package/src/layout/architecture.ts +381 -0
  35. package/src/layout/measure.ts +21 -0
  36. package/src/layout/scene.ts +353 -0
  37. package/src/layout/sequence.ts +189 -0
  38. package/src/load-fonts.ts +8 -0
  39. package/src/look/fonts.ts +33 -0
  40. package/src/look/palettes.json +209 -0
  41. package/src/look/values.ts +155 -0
  42. package/src/markdown.ts +35 -0
  43. package/src/messages.ts +122 -0
  44. package/src/parser.ts +342 -310
  45. package/src/player-global.ts +4 -0
  46. package/src/player.ts +271 -0
  47. package/src/printer.ts +76 -0
  48. package/src/render/icons.ts +41 -0
  49. package/src/render/motion.ts +106 -0
  50. package/src/render/parts.ts +279 -0
  51. package/src/render/svg.ts +341 -0
  52. package/src/schema.ts +161 -16
  53. package/src/schemas/diagram.schema.json +150 -0
  54. package/src/schemas/validate-diagram.d.ts +12 -0
  55. package/src/schemas/validate-diagram.js +8 -0
  56. package/src/timeline.ts +166 -0
  57. package/src/tokenize.ts +20 -281
  58. package/src/version.ts +55 -0
  59. package/src/view.ts +16 -0
  60. package/src/words.ts +79 -0
  61. package/examples/quick-start.md +0 -142
  62. package/src/canvas-bounds.ts +0 -52
  63. package/src/color.ts +0 -197
  64. package/src/compile/actor-ids.ts +0 -297
  65. package/src/compile/actor-option-notice.ts +0 -379
  66. package/src/compile/actors.ts +0 -103
  67. package/src/compile/c4.ts +0 -74
  68. package/src/compile/chain.ts +0 -93
  69. package/src/compile/chart-fields.ts +0 -193
  70. package/src/compile/deep-rewrite.ts +0 -30
  71. package/src/compile/derived-source.ts +0 -18
  72. package/src/compile/direction.ts +0 -102
  73. package/src/compile/edge-option-notice.ts +0 -112
  74. package/src/compile/er-relation.ts +0 -200
  75. package/src/compile/flow.ts +0 -54
  76. package/src/compile/flowchart.ts +0 -344
  77. package/src/compile/funnel.ts +0 -87
  78. package/src/compile/gantt.ts +0 -337
  79. package/src/compile/generic.ts +0 -696
  80. package/src/compile/hierarchy.ts +0 -92
  81. package/src/compile/journey.ts +0 -93
  82. package/src/compile/kinds.ts +0 -148
  83. package/src/compile/lanes.ts +0 -138
  84. package/src/compile/metro.ts +0 -271
  85. package/src/compile/mind.ts +0 -393
  86. package/src/compile/node-title.ts +0 -24
  87. package/src/compile/notice.ts +0 -141
  88. package/src/compile/order.ts +0 -62
  89. package/src/compile/parts-lookup.ts +0 -17
  90. package/src/compile/parts.ts +0 -2709
  91. package/src/compile/placement-apply.ts +0 -689
  92. package/src/compile/placement.ts +0 -15
  93. package/src/compile/quadrant.ts +0 -265
  94. package/src/compile/record.ts +0 -75
  95. package/src/compile/row-marks.ts +0 -128
  96. package/src/compile/rows.ts +0 -40
  97. package/src/compile/sequence.ts +0 -305
  98. package/src/compile/slug.ts +0 -37
  99. package/src/compile/stages.ts +0 -247
  100. package/src/compile/subtitle.ts +0 -35
  101. package/src/compile/swimlane-marks.ts +0 -87
  102. package/src/compile/swimlane.ts +0 -82
  103. package/src/compile/timeline.ts +0 -213
  104. package/src/compile/topology.ts +0 -44
  105. package/src/compile/tree.ts +0 -69
  106. package/src/compile/value-chart.ts +0 -337
  107. package/src/compile/values.ts +0 -432
  108. package/src/compile/word-state.ts +0 -82
  109. package/src/compile.ts +0 -3169
  110. package/src/focus.ts +0 -46
  111. package/src/input-size.ts +0 -181
  112. package/src/json-line-map.ts +0 -30
  113. package/src/json-parser.ts +0 -4289
  114. package/src/keywords.ts +0 -418
  115. package/src/near-name.ts +0 -42
  116. package/src/notation-lint.ts +0 -342
  117. package/src/relative-pos.ts +0 -251
  118. package/src/schemas/diagram.json +0 -3089
  119. package/src/types.ts +0 -1009
  120. package/src/v05/index.ts +0 -9
  121. package/src/v05/input-table.generated.ts +0 -138
  122. package/src/v05/parser-types.ts +0 -32
  123. package/src/v05/parser.ts +0 -6664
  124. package/src/v05/readout-table.generated.ts +0 -1088
  125. package/src/value-syntax.ts +0 -313
  126. package/src/write-position.ts +0 -270
@@ -1,2709 +0,0 @@
1
- import { layout, parseFormula } from "@cardenelabs/cdl";
2
- import type { CdlDiagram, CdlEdge, FormulaAst, LaidDiagram } from "@cardenelabs/cdl";
3
- import { isColorValue } from "../color";
4
- import { parseFocusEntry } from "../focus";
5
- import { MAX_INPUT_ELEMENTS, countDiagramElements } from "../input-size";
6
- import { type AnchorBox } from "../relative-pos";
7
- import type { DslActor, DslDocument, DslStep } from "../types";
8
- import { deepRewriteStrings } from "./deep-rewrite";
9
- import { recordDerivedSourceLine } from "./derived-source";
10
- import type { CompileNotice } from "./notice";
11
- import { lookupPartRaw } from "./parts-lookup";
12
- import { PLACEMENT_TOLERANCE } from "./placement";
13
- import { slugify } from "./slug";
14
- import { truncateForMessage } from "./subtitle";
15
- import { 近い名前 } from "../near-name";
16
- import { NODE_KIND_VALID } from "../v05/parser";
17
- /**
18
- * 見本 (parts) の取り込み (#2034 で `compile.ts` から移した)。
19
- *
20
- * 見本帳から引いた 1 つの図を、本文の図に重ねて 1 枚にする。 移した時点で
21
- * `compile.ts` の 38% を占めていた区画で、その中の宣言はここで閉じている。
22
- *
23
- * 外へ出す口は 4 つ。 入口 (`compileToCdl`) が呼ぶ `mergePartsFromActors` と
24
- * `reportPartNodeNotHonored`、相対の配置 (`resolveRelativeDoc`) が呼ぶ `partBoxes` と
25
- * `partSizes`。 残りはこの file の中だけで使う。
26
- *
27
- * ほかに `index.ts` と画面が読む宣言も持つ。 それらは `compile.ts` が再輸出するので、
28
- * 取り込む側の書き方は変わらない。
29
- */
30
-
31
- /**
32
- * catalog からパーツ 1 個の図を引く。
33
- *
34
- * `Object.hasOwn` で引く。 素の添字だと `__proto__` 等の既定の持ち物が引けてしまう
35
- * (catalog は呼出側が渡す untrusted な値)。
36
- *
37
- * **測れない図は「無い」 として扱う** (#1015)。 大きさを測れないまま取り込むと、既定の
38
- * 400x200 の枠を確保した場所に中身が全て展開される。 上限を置いた目的 (大きすぎる入力で
39
- * 止まらないようにする) も達成されない。
40
- */
41
- function lookupPart(
42
- partsCatalog: Record<string, CdlDiagram>,
43
- partId: string | undefined,
44
- ): CdlDiagram | undefined {
45
- const found = lookupPartRaw(partsCatalog, partId);
46
- if (found === undefined) return undefined;
47
- return partIsMeasurable(found) ? found : undefined;
48
- }
49
-
50
- /**
51
- * この図を取り込んでよいか (#1015)。
52
- *
53
- * 見るのは **要素数が上限 (`MAX_INPUT_ELEMENTS`) を超えていないこと** だけ。
54
- * 超えた図を取り込むと、既定の 400x200 の枠を確保した場所に中身が全て展開される。
55
- * 上限を置いた意図 (大きすぎる入力で止まらないようにする) も達成されない。
56
- *
57
- * **配置計算が通るかは見ない**。 取り込みは lane を張り替えるため、単体では配置計算が
58
- * 通らない図でも取り込みは成功する (実測 = 存在しない lane を指す箱を持つ見本が、
59
- * 取り込み後は正しい lane に載った)。 配置計算で弾くと、動いている本文が描けなくなる。
60
- */
61
- export function partIsMeasurable(part: CdlDiagram): boolean {
62
- return countDiagramElements(part) <= MAX_INPUT_ELEMENTS;
63
- }
64
-
65
- /**
66
- * 箱の大きさを書かなかった時に cdl が使う値。
67
- *
68
- * 幅は実測で 340 固定 (縦列の幅を変えても変わらない)。 高さは種類で変わるため、 よく使われる
69
- * 値を既定にする。 パーツの図が大きさを書いていれば、 こちらは使われない。
70
- */
71
- const CDL_DEFAULT_NODE_W = 340;
72
-
73
- const CDL_DEFAULT_NODE_H = 200;
74
-
75
- /** 有限で正の数だけを通す。 catalog は呼出側が渡す値なので、 異常値を計算に入れない。 */
76
- function positiveOr(value: unknown, fallback: number): number {
77
- return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback;
78
- }
79
-
80
- /**
81
- * 配列の最大値 / 最小値。 spread で展開しない (要素数が多い catalog で stack が溢れる)。
82
- *
83
- * 空の時だけ既定値を返す。 既定値を初期値にすると、 全要素が既定値より小さい (大きい) 時に
84
- * 存在しない値を範囲に含める (実測 = stack 5 だけのパーツで 0 を含め、 高さが 5 段分になった)。
85
- */
86
- function maxOf(values: readonly number[], fallback: number): number {
87
- if (values.length === 0) return fallback;
88
- let out = values[0]!;
89
- for (const v of values) if (v > out) out = v;
90
- return out;
91
- }
92
-
93
- function minOf(values: readonly number[], fallback: number): number {
94
- if (values.length === 0) return fallback;
95
- let out = values[0]!;
96
- for (const v of values) if (v < out) out = v;
97
- return out;
98
- }
99
-
100
- /**
101
- * 段 1 つぶんの送り幅。 2 つの用途で使う。
102
- *
103
- * - `大きさ:` の縦の基準 (`partScaleBase`)。 縦は段の送り幅の合計に対する倍率で掛かる
104
- * - 部品の頁を配置できない部品を、書いた値で置く時の段の間 (`書いた置き方`)
105
- *
106
- * 頁を配置できる部品の要素の縦位置は、頁で配置した位置から取る (#1992)。 この値で置くと、
107
- * 頁で段の間が 220 より広い部品 (実測 = `bind-grid-4` は段の中心の間 280) が置いた図で詰まる。
108
- */
109
- const PART_STACK_PITCH = 220;
110
-
111
- /**
112
- * 倍率の上限 (#1020)。
113
- *
114
- * 図枠は数百 world 単位なので、1000 倍で数十万になる。 これを超える倍率は画面上で意味を持たず、
115
- * 掛けた先が非有限になる危険だけが残る。
116
- */
117
- export const MAX_PART_SCALE = 1000;
118
-
119
- /**
120
- * 本文に書かれた倍率を、描ける値に直す (#1020 / #1026)。
121
- *
122
- * 記法は `倍率: -2` も `倍率: 0` も、桁が溢れて `Infinity` になる値も書ける。 置き場所と
123
- * 描画で別々に直すと、同じ見本が「置き場所は等倍・画面では消える」 状態になる (実測 =
124
- * `scale: 0` が等倍の場所を占めるのに画面には出なかった)。 読んだ時点で直す。
125
- *
126
- * **画面側と組み立て側の両方から呼ぶ**。 別々に持つと、同じ本文が経路で別の絵になる (#1026)。
127
- */
128
- export function normalizePartScale(value: number): number {
129
- if (!Number.isFinite(value) || value <= 0) return 1;
130
- return Math.min(value, MAX_PART_SCALE);
131
- }
132
-
133
- /**
134
- * `大きさ:` と `倍率:` を合成した最終の伸縮率 (#1026)。
135
- *
136
- * **上限は合成した後に 1 度だけ掛ける**。 率ごとに掛けると、`大きさ:` 由来 1000 倍と
137
- * `倍率: 2` で合わせて 2000 倍になり、1 度だけ掛ける経路 (1000 倍) と食い違う (実測)。
138
- *
139
- * 基準は `大きさ:` と同じ物差し (縦列の外接矩形と段の送り幅)。 図枠を基準にすると、
140
- * 図枠と外接矩形の差のぶんだけ余分に掛かる (実測 = 3 倍と書いて 4.0875 倍になった)。
141
- *
142
- * 画面側 (重ねて描く時の `transform`) と組み立て側 (取り込む時の伸縮) が同じ値を使う。
143
- */
144
- export function partScaleFactor(
145
- part: CdlDiagram,
146
- posW: number | undefined,
147
- posH: number | undefined,
148
- scale: number | undefined,
149
- ): { x: number; y: number } {
150
- const base = partScaleBase(part);
151
- const k = scale === undefined ? 1 : normalizePartScale(scale);
152
- const rx = posW !== undefined && posW > 0 ? posW / base.w : 1;
153
- const ry = posH !== undefined && posH > 0 ? posH / base.h : 1;
154
- return { x: normalizePartScale(rx * k), y: normalizePartScale(ry * k) };
155
- }
156
-
157
- /**
158
- * 見本 1 件の狙いの大きさ (#1026)。
159
- *
160
- * 合成した率を基準に掛けて返す。 取り込み側はこの値から自分で率を出し直すため、
161
- * ここで上限を掛けておかないと「見積りは上限どまり・実体は青天井」 になる (実測 =
162
- * 見積り 1000 倍に対して実体 10000 倍)。
163
- *
164
- * 何も書かれていない辺は「狙いなし」 のまま返す。 基準の値を入れると、取り込み側が
165
- * 自前で測る外接矩形との差だけ伸縮が掛かってしまう。
166
- */
167
- export function partTargetSize(
168
- part: CdlDiagram,
169
- posW: number | undefined,
170
- posH: number | undefined,
171
- scale: number | undefined,
172
- ): { w: number | undefined; h: number | undefined } {
173
- if (posW === undefined && posH === undefined && scale === undefined) {
174
- return { w: undefined, h: undefined };
175
- }
176
- const base = partScaleBase(part);
177
- const f = partScaleFactor(part, posW, posH, scale);
178
- return {
179
- w: posW === undefined && scale === undefined ? undefined : base.w * f.x,
180
- h: posH === undefined && scale === undefined ? undefined : base.h * f.y,
181
- };
182
- }
183
-
184
- /**
185
- * `大きさ:` と `倍率:` が掛かる時の基準の大きさ (#1026)。
186
- *
187
- * 横は部品の頁で配置した縦列の外接矩形 (`部品の置き方を読む`)、縦は段の送り幅の合計。
188
- * **図枠 (`partRenderSize`) ではない**。 図枠は余白を含むため、これを基準にすると書いた倍率より
189
- * 大きく掛かる。
190
- *
191
- * 横を頁の縦列にするのは、組み込みが要素を頁の縦列に沿って置くため (#1992)。 書いた縦列を
192
- * 基準のまま残すと、頁で縦列が広がる部品は `大きさ:` に書いた幅より広く描かれる。
193
- *
194
- * `partTargetScale` と `partTargetSize` が同じ物差しを使うことで、
195
- * `partTargetScale(part, base.w * k, base.h * k)` が丁度 `k` 倍を返す関係が保たれる。
196
- * 組み込み (`mergePartIntoDiagram`) も同じ基準で伸縮する。
197
- */
198
- function partScaleBase(part: CdlDiagram): { w: number; h: number } {
199
- const nodes = Array.isArray(part.nodes) ? part.nodes : [];
200
- const stacks = nodes.map((n) =>
201
- typeof n.stack === "number" && Number.isFinite(n.stack) ? n.stack : 0,
202
- );
203
- return {
204
- w: 部品の置き方を読む(part).幅,
205
- h: Math.max(1, (maxOf(stacks, 0) - minOf(stacks, 0) + 1) * PART_STACK_PITCH),
206
- };
207
- }
208
-
209
- /**
210
- * `大きさ:` を書いた時に、見本を何倍にするか (#1018)。
211
- *
212
- * 横は縦列の幅、縦は段の数から出す。 どちらも書かなければ 1 倍。
213
- *
214
- * **縦は「書いた高さにする」 ではなく「段の送り幅の合計に対する倍率」**。 `大きさ: 2000,300` を
215
- * 1 段の見本に書くと、横は 2000 になるが縦は 300 ではなく 409 になる (段の送り幅 220 に対して
216
- * 300 なので 1.36 倍、それが箱の高さ 300 に掛かる)。 意図した仕様かは怪しいが、既に本文が
217
- * この前提で書かれているため変えない。 画面側も同じ規則で拡大する。
218
- *
219
- * 組み立て側 (`partExtent`) と画面側 (playground) の両方から呼ぶ。 別々に持つと、`大きさ:` を
220
- * 書いた見本だけ経路で大きさが変わる。
221
- */
222
- export function partTargetScale(
223
- part: CdlDiagram,
224
- targetW?: number,
225
- targetH?: number,
226
- ): { x: number; y: number } {
227
- const none = { x: 1, y: 1 };
228
- if (!Array.isArray(part.lanes) || !Array.isArray(part.nodes)) return none;
229
- // 箱が 1 つも無い図でも縦列があれば取り込み側は伸縮する。 ここで 1 に倒すと、
230
- // 箱を持たない外部の見本だけ画面が等倍のまま残る
231
-
232
- // 倍率を書かない場合の合成率。 上限の掛け方を 1 箇所に閉じるため同じ関数を通す
233
- return partScaleFactor(part, targetW, targetH, undefined);
234
- }
235
-
236
- /**
237
- * パーツ 1 個が図の上で占める外接矩形。
238
- *
239
- * `w` / `h` は大きさ、 `dx` / `dy` は矩形の中心が「merge に渡す座標」 からどれだけずれるか。
240
- *
241
- * merge がパーツを置く時に基準にするのは段の中心で、 外接矩形の中心とは一致しない。 段ごとに
242
- * 箱の高さが違うと、 上下の伸び方が非対称になるため (実測 = 段 0 に高さ 50、 段 5 に高さ 200 の
243
- * パーツで中心が 37.5 下にずれる)。 ずれを返して呼ぶ側が引く。
244
- *
245
- * 箱ごとに位置と大きさを見る。 一番高い箱の高さと段の数から概算すると実際の矩形と合わない
246
- * (実測 = 段 5 だけのパーツで 200 空けたいところが 750、 段 0,5 で高さが違うと 275 になった)。
247
- *
248
- * 箱の位置は merge と同じ置き方 (`部品の置き方を読む`) と同じ式 (`部品の要素のずれ`) から出す。
249
- * ここで見たいのは「merge がどこに置くか」 なので、merge と別の規則を持たない (#1992)。
250
- */
251
- function partExtent(
252
- part: CdlDiagram,
253
- targetW?: number,
254
- targetH?: number,
255
- ): { w: number; h: number; dx: number; dy: number } {
256
- const fallback = { w: 400, h: 200, dx: 0, dy: 0 };
257
- if (!Array.isArray(part.lanes) || !Array.isArray(part.nodes)) return fallback;
258
- if (part.nodes.length === 0) return fallback;
259
-
260
- const 置き方 = 部品の置き方を読む(part);
261
- const { x: scaleX, y: scaleY } = partTargetScale(part, targetW, targetH);
262
-
263
- // 箱ごとに、 merge が置く位置 (基準からの相対) と大きさから上下左右の端を出す
264
- const tops: number[] = [];
265
- const bottoms: number[] = [];
266
- const lefts: number[] = [];
267
- const rights: number[] = [];
268
- part.nodes.forEach((n, i) => {
269
- const 頁 = 置き方.要素[i] ?? { x: 置き方.左端 + 置き方.幅 / 2, y: 0 };
270
- const { dx: cx, dy: cy } = 部品の要素のずれ(n, 頁, 置き方, scaleX, scaleY);
271
- const halfW = (positiveOr(n.w, CDL_DEFAULT_NODE_W) * scaleX) / 2;
272
- const halfH = (positiveOr(n.h, CDL_DEFAULT_NODE_H) * scaleY) / 2;
273
- lefts.push(cx - halfW);
274
- rights.push(cx + halfW);
275
- tops.push(cy - halfH);
276
- bottoms.push(cy + halfH);
277
- });
278
- const x0 = minOf(lefts, 0);
279
- const x1 = maxOf(rights, 400);
280
- const y0 = minOf(tops, 0);
281
- const y1 = maxOf(bottoms, 200);
282
-
283
- return {
284
- w: positiveOr(x1 - x0, 400),
285
- h: positiveOr(y1 - y0, 200),
286
- dx: Number.isFinite((x0 + x1) / 2) ? (x0 + x1) / 2 : 0,
287
- dy: Number.isFinite((y0 + y1) / 2) ? (y0 + y1) / 2 : 0,
288
- };
289
- }
290
-
291
- /**
292
- * パーツ 1 個が実際に描かれる大きさ (#937)。
293
- *
294
- * 図枠 (`viewBox`) を返す。 箱の外接矩形 (`partVisualSize`) ではない。 2 つは別物で、
295
- * 実測では図枠 525x520 に対し箱 380x400 と余白の分だけ違う。 SVG は図枠を基準に
296
- * `preserveAspectRatio` で収めるため、 箱の値を渡すと縮んで描いた大きさと食い違う
297
- * (実測 = achievement が箱の値で描くと約 275x275 になった)。
298
- *
299
- * 箱を持たないパーツ (実体が操作パネルの部品である 17 件) でも図枠は出る。 箱だけを見ると
300
- * 1x1 になり、 その値で描くと潰れる。
301
- *
302
- * ただし **図枠は場所を確保するだけで、図の中に何か描かれることは保証しない**。 上の 17 件は
303
- * 図の中に描く部品を持たず、重ねても図には出ない (`partDrawsInDiagram`、#1017)。
304
- *
305
- * 画面が描く大きさと、 組み立て側の格子が確保する場所の両方がこれを見る。 別々の物差しを
306
- * 持っていた頃は、 同じ本文でも通った経路でパーツの位置が変わっていた (#937)。
307
- *
308
- * 組み立てに失敗する図では、 既定の大きさに落とす (呼出側は catalog を渡すので通常起きない)。
309
- */
310
- export function partRenderSize(part: CdlDiagram): { w: number; h: number } {
311
- const g = partFrameGeometry(part);
312
- return { w: g.w, h: g.h };
313
- }
314
-
315
- /**
316
- * この見本が、図の中に描かれる部品を持っているか (#1017)。
317
- *
318
- * 見本の中には実体が **操作パネルの部品** (`readouts`) だけのものがある。 配置計算も描画も
319
- * `readouts` を図の中では扱わないため、図として重ねても何も出ない。 位置決めのための
320
- * 1x1 の箱が 1 つあるだけになる。
321
- *
322
- * catalog を測ると、この 2 群は `readouts` の有無で完全に分かれる。
323
- * `readouts` を持つ 17 件は箱と図枠の面積比が全件 0.00007 (箱は 1x1 で図枠は 120x120)、
324
- * 持たない群は最小でも 0.37662。 境目に入る件は無い
325
- * (#2681 で図枠が中身に寄るようになり、どちらの群も値が上がったが隔たりは残った)。
326
- *
327
- * 判定は面積の閾値ではなく **`readouts` を持ち、かつ箱が図枠に対して極小** の 2 条件で行う。
328
- * 閾値だけで見ると、小さい箱を意図して置いた見本を巻き込む。 `readouts` だけで見ると、
329
- * 箱も実体も両方持つ見本 (現状 0 件だが作れる) を誤って弾く。
330
- *
331
- * 測れない図では「持っている」 側に倒す。 弾く側に倒すと、測れないだけの見本が使えなくなる。
332
- */
333
- export function partDrawsInDiagram(part: CdlDiagram): boolean {
334
- const readouts = (part as { readouts?: unknown }).readouts;
335
- if (!Array.isArray(readouts) || readouts.length === 0) return true;
336
- const g = partFrameGeometry(part);
337
- if (!(g.w > 0) || !(g.h > 0)) return true;
338
- // 箱が図枠の 1% にも満たなければ、実体は図の外にある。
339
- //
340
- // **辺ごとに割ってから掛ける**。 面積を先に出すと桁の大きい図で溢れ、判定が反転する
341
- // (実測 = 箱 1.7e305 x 1e4 / 図枠 1.7e308 x 1e4 は比 0.001 で「描かない」 が正しいのに、
342
- // 面積を先に出すと Infinity / Infinity = NaN になって「描く」 に倒れた)。
343
- //
344
- // それでも出せない時は「持っている」 側に倒す。 弾く側に倒すと、
345
- // 測れないだけの見本が使えなくなる
346
- const ratio = (g.boxW / g.w) * (g.boxH / g.h);
347
- if (!Number.isFinite(ratio)) return true;
348
- return ratio >= 0.01;
349
- }
350
-
351
- /**
352
- * 図枠の中で、 箱の外接矩形がどこにどれだけの大きさで描かれるか (#1014)。
353
- *
354
- * `left` / `top` は図枠の左上からの余白、 `w` / `h` は箱の大きさ。 図枠は 1 対 1 で描かれるので、
355
- * 画面上の箱の位置は「図枠の左上 + `left`/`top`」 になる。
356
- *
357
- * 相対で書いた位置 (`位置: Web の右 200`) の間隔は、 見えている箱の縁から測る。 図枠の縁で
358
- * 測ると余白のぶんだけ広がる (実測 = 200 と書いて画面では 260 空いた)。 画面側が間隔を解く時に
359
- * 図枠ではなくこちらを使うことで、 組み立て側と同じ間隔になる。
360
- *
361
- * 測れない図では図枠と同じ大きさ・余白 0 を返す。 箱を持たない図でも同じで、 図枠がそのまま
362
- * 箱として扱われる。
363
- */
364
- export function partBoxInFrame(part: CdlDiagram): {
365
- w: number;
366
- h: number;
367
- left: number;
368
- top: number;
369
- } {
370
- const g = partFrameGeometry(part);
371
- return { w: g.boxW, h: g.boxH, left: g.left, top: g.top };
372
- }
373
-
374
- /**
375
- * 見本 1 個の図枠と、 その中の箱の外接矩形 (位置と大きさ)。
376
- *
377
- * 図枠の大きさ・余白・箱の大きさは同じ配置計算から出るので、 1 回で全部を取る。 別々に呼ぶと
378
- * 同じ図を何度も組み立てることになり、 パーツを 1 個置くたびに配置計算が 2 回走る。
379
- *
380
- * 結果は見本ごとに覚えておく。 catalog の見本は複数の別名から同じものを指すため、 覚えないと
381
- * 別名の数だけ組み立て直す (相対指定があると 1 個につき 4 回になる)。 覚えるのは大きさだけで、
382
- * 色などの見た目は含まないため、 呼出側が色を差し替えても古い値にはならない。
383
- *
384
- * 配置は `部品の頁を配置する` から受け取る。 置き方 (`部品の置き方を読む`) も同じ配置を読むので、
385
- * 部品を 1 つ置いても配置は 1 度で済む。
386
- *
387
- * 測れない図では既定の大きさと余白 0 に落とす。
388
- */
389
- type PartFrameGeometry = {
390
- w: number;
391
- h: number;
392
- left: number;
393
- top: number;
394
- boxW: number;
395
- boxH: number;
396
- };
397
-
398
- const PART_FRAME_CACHE = new WeakMap<CdlDiagram, PartFrameGeometry>();
399
-
400
- function partFrameGeometry(part: CdlDiagram): PartFrameGeometry {
401
- const cached = PART_FRAME_CACHE.get(part);
402
- if (cached) return cached;
403
- const fallback = { w: 400, h: 200, left: 0, top: 0, boxW: 400, boxH: 200 };
404
- let out = fallback;
405
- const own = 部品の頁を配置する(part);
406
- if (own !== undefined) {
407
- const vb = own.viewBox;
408
- const w = positiveOr(vb.w, 400);
409
- const h = positiveOr(vb.h, 200);
410
- if (own.nodes.length === 0) {
411
- // 箱を持たない図では図枠をそのまま箱として扱う。 相対指定の間隔は図枠の縁から測る
412
- out = { w, h, left: 0, top: 0, boxW: w, boxH: h };
413
- } else {
414
- let x0 = Infinity;
415
- let y0 = Infinity;
416
- let x1 = -Infinity;
417
- let y1 = -Infinity;
418
- for (const n of own.nodes) {
419
- x0 = Math.min(x0, n.cx - n.w / 2);
420
- x1 = Math.max(x1, n.cx + n.w / 2);
421
- y0 = Math.min(y0, n.cy - n.h / 2);
422
- y1 = Math.max(y1, n.cy + n.h / 2);
423
- }
424
- const left = x0 - vb.x;
425
- const top = y0 - vb.y;
426
- out = {
427
- w,
428
- h,
429
- left: Number.isFinite(left) ? left : 0,
430
- top: Number.isFinite(top) ? top : 0,
431
- boxW: positiveOr(x1 - x0, w),
432
- boxH: positiveOr(y1 - y0, h),
433
- };
434
- }
435
- }
436
- PART_FRAME_CACHE.set(part, out);
437
- return out;
438
- }
439
-
440
- /** 測るためだけの配置が受け取った知らせの数 (#2312)。 */
441
- let 測るために受け取った知らせ = 0;
442
-
443
- /**
444
- * **測るためだけに配置する** (#2312)。
445
- *
446
- * 組み立て処理は、縦列の位置や部品の図枠を知るために図を配置する。 その配置は読むだけで、
447
- * 画面にも書き出しにも出ない。 部品や位置はこの後に動かすので、測った時の並びは残らない。
448
- *
449
- * ところが記法の engine は配置のたびに重なりを `console.warn` に出す。 測るためだけの配置でも
450
- * 出るので、**描かれない図についての知らせ** が端末に流れていた。 カタログの群 14 file を
451
- * 読み込むだけで 481 行 (箱どうしの重なり 48 件は、出来上がった図では 1 件も重ならない)。
452
- *
453
- * engine の `layout()` は引数を 1 つしか取らず、知らせを止める口が無い。 呼ぶ側で受け取る。
454
- *
455
- * **黙って捨てない**。 受け取った数を覚えておき、`測るために受け取った知らせの数()` で読める。
456
- * 数えないと「知らせが無い」 と「この経路を通っていない」 が区別できず、読み込み時の知らせを
457
- * 0 行で固定する検査が空振りしても気付けない。
458
- *
459
- * 記法の engine 以外の知らせ (`[dragon]` で始まる組み立て側の知らせなど) はそのまま通す。
460
- * 止めるのは engine が配置のたびに出す分に限る。
461
- *
462
- * 出来上がった図についての知らせは止めない。 画面を描く側の配置は別の経路で、
463
- * そちらの知らせは `layout-warn-ledger` が宣言と突き合わせている (#2308)。
464
- *
465
- * **配置できない図の扱いは呼ぶ側のまま**。 例外はそのまま返す = ここで握ると、今まで
466
- * 例外を投げていた呼び出し (`partsBaseBottomWithoutParts`) が黙って `undefined` を返す形に
467
- * 変わる。 知らせを受け取ること以外は何も変えない。
468
- */
469
- export function 測るために配置する(d: CdlDiagram): LaidDiagram {
470
- const 元 =
471
- typeof console !== "undefined" && typeof console.warn === "function" ? console.warn : undefined;
472
- if (元) {
473
- console.warn = (...a: unknown[]) => {
474
- if (a.map(String).join(" ").includes("[cdl layout]")) {
475
- 測るために受け取った知らせ++;
476
- return;
477
- }
478
- 元(...(a as []));
479
- };
480
- }
481
- try {
482
- return layout(d);
483
- } finally {
484
- if (元) console.warn = 元;
485
- }
486
- }
487
-
488
- /**
489
- * 測るためだけの配置が受け取った知らせの数 (#2312)。
490
- *
491
- * 読み込み時の知らせを 0 行で固定する検査が、母数として読む。 0 件なら「知らせが無い」 では
492
- * なく「測るための配置を 1 度も通っていない」 = 検査が空振りしている。
493
- */
494
- export function 測るために受け取った知らせの数(): number {
495
- return 測るために受け取った知らせ;
496
- }
497
-
498
- /**
499
- * 部品の頁 (部品の図だけ) を配置した結果。 配置できない図では `undefined` (#1992)。
500
- *
501
- * 図枠 (`partFrameGeometry`) と置き方 (`部品の置き方を読む`) が同じ結果を読む。 見本ごとに
502
- * 覚えておくのは、catalog の見本が複数の別名から同じものを指し、画面を描くたびにも呼ばれるため。
503
- *
504
- * 大きすぎる図は配置する前に止める。 上限は組み立て側と同じ物差しを使う (#1005)。
505
- * catalog の見本は数十要素なので通常は掛からない。
506
- */
507
- const PART_PAGE_CACHE = new WeakMap<CdlDiagram, LaidDiagram | null>();
508
-
509
- function 部品の頁を配置する(part: CdlDiagram): LaidDiagram | undefined {
510
- const cached = PART_PAGE_CACHE.get(part);
511
- if (cached !== undefined) return cached ?? undefined;
512
- let out: LaidDiagram | null = null;
513
- if (countDiagramElements(part) <= MAX_INPUT_ELEMENTS) {
514
- try {
515
- out = 測るために配置する(part);
516
- } catch {
517
- out = null;
518
- }
519
- }
520
- PART_PAGE_CACHE.set(part, out);
521
- return out ?? undefined;
522
- }
523
-
524
- /**
525
- * 部品を図に置く時に、縦列と要素をどこに置くか (#1992)。
526
- *
527
- * 部品の頁で配置した縦列の位置と幅、要素の中心を使う。 組み込み (`mergePartIntoDiagram`)、
528
- * 組み込んだ大きさの見積り (`partExtent`)、`大きさ:` の横の基準 (`partScaleBase`) の 3 か所が
529
- * これを読む。
530
- *
531
- * 部品が書いた縦列の位置と幅は、頁で配置した後の値と違う。 描画側が隣と近づきすぎないよう
532
- * 縦列を広げ、段の間を箱の高さに合わせて取るため。 書いた値で置くと頁で空いた分が消え、
533
- * 置いた図だけが間隔の検査に掛かる (実測 = 80 種のうち 14 種。 `wifi-signal` は頁で要素の間 80、
534
- * 書いた値で置くと 40)。
535
- *
536
- * 頁を配置できない図 (描画側が止める図と、大きすぎて測らない図) は書いた値に落とす
537
- * (`書いた置き方`)。 頁の位置が数にならない図も同じ。 描画側は書いた値が数でない時に止めずに
538
- * 数でない座標を返す (実測 = 幅を書かない縦列は幅が `NaN`、段が数でない要素は縦が `-Infinity`)。
539
- *
540
- * 1 つの部品で 2 つを混ぜない。 数にならない縦列や要素が 1 つでもあれば全体を書いた値にする。
541
- * 混ぜると、要素ごとに別の物差しで置くことになる。
542
- */
543
- type 部品の置き方 = {
544
- /** 縦列ごとの左端と幅 */
545
- 縦列: ReadonlyMap<string, { x: number; w: number }>;
546
- /** 縦列の外接矩形の左端と幅。 横に写す時の中心と、`大きさ:` の横の基準に使う */
547
- 左端: number;
548
- 幅: number;
549
- /**
550
- * 要素ごとの中心 (`part.nodes` と同じ並び)。 横は縦列と同じ座標、縦は要素全体の縦の中心からの差。
551
- * どちらも伸縮を掛ける前の値
552
- */
553
- 要素: readonly { x: number; y: number }[];
554
- };
555
-
556
- const 部品の置き方の控え = new WeakMap<CdlDiagram, 部品の置き方>();
557
-
558
- function 部品の置き方を読む(part: CdlDiagram): 部品の置き方 {
559
- const 控え = 部品の置き方の控え.get(part);
560
- if (控え) return 控え;
561
- const 置き方 = 頁の置き方(part) ?? 書いた置き方(part);
562
- 部品の置き方の控え.set(part, 置き方);
563
- return 置き方;
564
- }
565
-
566
- /**
567
- * 縦列の外接矩形。 幅が正でない時 (縦列が 1 本も無い図) は 400 に落とす。
568
- * 1 未満の正の幅はそのまま使う。 1 に切り上げると、その分だけ `大きさ:` の倍率が小さくなる
569
- */
570
- function 縦列の外接(縦列: ReadonlyMap<string, { x: number; w: number }>): {
571
- 左端: number;
572
- 幅: number;
573
- } {
574
- const 値 = [...縦列.values()];
575
- const 左端 = minOf(
576
- 値.map((g) => g.x),
577
- 0,
578
- );
579
- const 右端 = maxOf(
580
- 値.map((g) => g.x + g.w),
581
- 400,
582
- );
583
- return { 左端, 幅: positiveOr(右端 - 左端, 400) };
584
- }
585
-
586
- /**
587
- * 部品の頁で配置した縦列と要素の中心。 頁を配置できない時と、頁の位置が数にならない時は
588
- * `undefined` を返し、呼出側が書いた値に落とす。
589
- *
590
- * 縦の中心は、頁に並んだ要素全体の上下の中心 (一番上と一番下の要素の中心の中間)。 書いた値の
591
- * 経路が段の番号の中間を中心にするのと同じ取り方で、部品の中心を置く位置に合わせる。
592
- *
593
- * 部品の要素が頁に見つからない場合も書いた値に落とす。 描画側は部品の要素を全て配置して返す
594
- * ため、この場合に届く入力は作れない (実測 = カタログ 80 種で見つからない要素は 0)。 残すのは、
595
- * 見つからない要素だけを別の物差しで置かないため。
596
- */
597
- function 頁の置き方(part: CdlDiagram): 部品の置き方 | undefined {
598
- if (!Array.isArray(part.lanes) || !Array.isArray(part.nodes)) return undefined;
599
- const laid = 部品の頁を配置する(part);
600
- if (laid === undefined) return undefined;
601
- const 縦列 = new Map<string, { x: number; w: number }>();
602
- for (const l of laid.lanes) {
603
- if (!Number.isFinite(l.x) || !Number.isFinite(l.width) || !(l.width > 0)) return undefined;
604
- 縦列.set(l.id, { x: l.x, w: l.width });
605
- }
606
- const 頁の箱 = new Map(laid.nodes.map((n) => [n.id, n]));
607
- const 箱たち: { cx: number; cy: number }[] = [];
608
- for (const n of part.nodes) {
609
- const 箱 = 頁の箱.get(n.id);
610
- if (箱 === undefined || !Number.isFinite(箱.cx) || !Number.isFinite(箱.cy)) return undefined;
611
- 箱たち.push(箱);
612
- }
613
- const 縦たち = 箱たち.map((b) => b.cy);
614
- const 縦の中心 = (minOf(縦たち, 0) + maxOf(縦たち, 0)) / 2;
615
- return {
616
- 縦列,
617
- ...縦列の外接(縦列),
618
- 要素: 箱たち.map((b) => ({ x: b.cx, y: b.cy - 縦の中心 })),
619
- };
620
- }
621
-
622
- /**
623
- * 部品が書いた値で置く時の縦列と要素の中心。 頁を配置できない部品だけが使う。
624
- *
625
- * 横は書いた縦列の中心、縦は段の番号に段の送り幅 (`PART_STACK_PITCH`) を掛けた位置。
626
- * 縦列の位置と幅は先に正す。 catalog は呼出側が渡す値で、生値のまま外接矩形を出すと
627
- * 拡大の基準が崩れて箱が桁違いに大きくなる (実測 = 指定間隔 200 が -31800 になった)。
628
- * 数でない段は 0 として扱う (#1018)。
629
- */
630
- function 書いた置き方(part: CdlDiagram): 部品の置き方 {
631
- const lanes = Array.isArray(part.lanes) ? part.lanes : [];
632
- const nodes = Array.isArray(part.nodes) ? part.nodes : [];
633
- const 縦列 = new Map<string, { x: number; w: number }>();
634
- for (const l of lanes) {
635
- 縦列.set(l.id, {
636
- x: typeof l.x === "number" && Number.isFinite(l.x) ? l.x : 0,
637
- w: positiveOr(l.width, 400),
638
- });
639
- }
640
- const stacks = nodes.map((n) =>
641
- typeof n.stack === "number" && Number.isFinite(n.stack) ? n.stack : 0,
642
- );
643
- const 中央の段 = (minOf(stacks, 0) + maxOf(stacks, 0)) / 2;
644
- return {
645
- 縦列,
646
- ...縦列の外接(縦列),
647
- 要素: nodes.map((n, i) => {
648
- const l = 縦列.get(n.lane) ?? { x: 0, w: 320 };
649
- return { x: l.x + l.w / 2, y: ((stacks[i] ?? 0) - 中央の段) * PART_STACK_PITCH };
650
- }),
651
- };
652
- }
653
-
654
- /**
655
- * 部品の要素 1 つの中心が、置く位置 (merge に渡す座標) からどれだけずれるか (#1992)。
656
- *
657
- * 組み込み (`mergePartIntoDiagram`) と見積り (`partExtent`) が同じ式を通る。 別々に書くと、
658
- * 格子が確保した場所と実際に置いた場所がずれる。
659
- *
660
- * 要素が自分で位置を書いていれば、その値を使う。 横は縦列と同じ写し方 (縦列の外接矩形の中心を
661
- * 置く位置に合わせて伸縮する) で写し、縦は置く位置に足すだけで伸縮を掛けない。
662
- */
663
- function 部品の要素のずれ(
664
- node: { posX?: number; posY?: number },
665
- 頁: { x: number; y: number },
666
- 置き方: 部品の置き方,
667
- scaleX: number,
668
- scaleY: number,
669
- ): { dx: number; dy: number } {
670
- return {
671
- dx: ((node.posX ?? 頁.x) - (置き方.左端 + 置き方.幅 / 2)) * scaleX,
672
- dy: node.posY ?? 頁.y * scaleY,
673
- };
674
- }
675
-
676
- /**
677
- * パーツ 1 個が図の上で確保する図枠 (merge に渡す座標での表し方)。
678
- *
679
- * `w` / `h` は図枠の大きさ、 `dx` / `dy` は図枠の中心が「merge に渡す座標」 からどれだけ
680
- * ずれるか。 画面側は図枠をそのまま置くので、 格子が図枠で場所を決めれば 2 経路が揃う。
681
- *
682
- * **合わせるのは箱の中心ではなく左上**。 段を 2 つ以上持つパーツは、 取り込んだ後に本体の
683
- * 送り幅で並び直すため箱の高さが単体の時と変わる (実測 = 単体 300 が取り込むと 320)。
684
- * 中心で合わせると、 高さの差の半分だけ上端がずれて段内の揃いが崩れる (実測で 12.5)。
685
- * 左上で合わせれば、 高さが変わっても上端は動かない。
686
- *
687
- * 図枠にも箱にも `大きさ:` の伸縮を掛ける。 画面側も同じ率で伸縮するので、掛けないと
688
- * 確保する場所だけが元の大きさのまま残る (実測 = 240 ずれた、#1018)。
689
- *
690
- * 確保するのは図枠と箱の両方を含む矩形。 縦横で率が違うと箱が図枠からはみ出すことがあり、
691
- * 図枠だけを確保すると隣に重なる (実測 = `大きさ: 2000,300` の箱が x=60..2060 に伸び、
692
- * 隣が 725 から始まって 1335 重なった)。 `大きさ:` を書かなければ図枠が箱を包むので、
693
- * 和は図枠と一致して 2 経路の一致は保たれる。
694
- */
695
- function partFrameExtent(
696
- part: CdlDiagram,
697
- targetW?: number,
698
- targetH?: number,
699
- ): { w: number; h: number; dx: number; dy: number } {
700
- const box = partExtent(part, targetW, targetH);
701
- const geom = partFrameGeometry(part);
702
- // 図枠にも `大きさ:` の伸縮を掛ける。 掛けないと箱だけが伸びて、確保する場所が足りなくなる
703
- // (実測 = `大きさ: 2000,300` で組み立て側の箱が 60..2060、画面側が 300..2300 と 240 ずれた、#1018)
704
- const t = partTargetScale(part, targetW, targetH);
705
- const frame = {
706
- w: geom.w * t.x,
707
- h: geom.h * t.y,
708
- left: geom.left * t.x,
709
- top: geom.top * t.y,
710
- };
711
- // merge に渡す座標を原点にした時の、 図枠の中心
712
- const frameDx = box.dx + frame.w / 2 - frame.left - box.w / 2;
713
- const frameDy = box.dy + frame.h / 2 - frame.top - box.h / 2;
714
- const x0 = Math.min(frameDx - frame.w / 2, box.dx - box.w / 2);
715
- const x1 = Math.max(frameDx + frame.w / 2, box.dx + box.w / 2);
716
- const y0 = Math.min(frameDy - frame.h / 2, box.dy - box.h / 2);
717
- const y1 = Math.max(frameDy + frame.h / 2, box.dy + box.h / 2);
718
- return {
719
- w: positiveOr(x1 - x0, frame.w),
720
- h: positiveOr(y1 - y0, frame.h),
721
- dx: Number.isFinite((x0 + x1) / 2) ? (x0 + x1) / 2 : frameDx,
722
- dy: Number.isFinite((y0 + y1) / 2) ? (y0 + y1) / 2 : frameDy,
723
- };
724
- }
725
-
726
- /**
727
- * パーツ 1 個の箱の外接矩形。
728
- *
729
- * 図枠 (`partRenderSize`) とは別で、 余白を含まない。 相対指定を解く時の「縁からの距離」 に使う。
730
- */
731
- export function partVisualSize(
732
- part: CdlDiagram,
733
- targetW?: number,
734
- targetH?: number,
735
- ): { w: number; h: number } {
736
- const e = partExtent(part, targetW, targetH);
737
- return { w: e.w, h: e.h };
738
- }
739
-
740
- /** 格子に並べる時の 1 行あたりの個数と隙間。 */
741
- const PARTS_PER_ROW = 3;
742
-
743
- const PARTS_GAP = 120;
744
-
745
- /**
746
- * 組み立てた図の下端 (#2002)。 箱が 1 つも無ければ `undefined`。
747
- *
748
- * **段数で概算しない**。 かつては箱の数から `箱 1 つ = 1 段 (280)` で見積もっていたが、
749
- * `flow` の箱は実際には高さ 68 で 168 おきに並ぶため 1 段あたり 112 ずつ余分に見積もる。
750
- * 誤差は箱の数に比例して積み上がり、実測で箱 2 つの図に 496、箱 4 つの図に 720 の空きが
751
- * 入っていた (部品どうしの間は 120)。
752
- *
753
- * **箱の下端ではなく図枠の下端を返す**。 格子は図枠どうしを `PARTS_GAP` 空けて並べるので、
754
- * 既存の図との間も図枠で測らないと、部品どうしの間と既存の図との間が揃わない
755
- * (箱で測ると 180、図枠で測ると 240 で、後者が部品どうしの段の間と一致する)。
756
- *
757
- * **箱が無いことを 0 に潰さない**。 0 を下端として使うと、部品だけを並べた図で部品が
758
- * 図の上端へ貼り付く。 呼出側が「箱が無い」 と「下端が 0」 を分けられるように `undefined` を返す。
759
- */
760
- export function partsBaseBottom(laid: LaidDiagram): number | undefined {
761
- if (laid.nodes.length === 0) return undefined;
762
- return laid.viewBox.y + laid.viewBox.h;
763
- }
764
-
765
- /**
766
- * 位置を書かなかったパーツを格子に並べた時の、 矩形の中心。
767
- *
768
- * 組み立て側 (`mergePartsFromActors`) と画面側 (playground の overlay) の両方から呼ぶ。
769
- * 別々に計算すると、 同じ本文でも経路によってパーツの位置が変わる。
770
- *
771
- * 列の送り幅は並べる全パーツの最大幅で揃える。 個々の幅で送ると、 幅の違うパーツが混ざった時に
772
- * 隣と重なる (実測 = 400 の次に 200 を置くと 280 重なった)。 段の高さも段内の最大高で揃える。
773
- * 縦は自分の高さの半分だけ段の上端から下げて、 段内で上端を揃える。
774
- *
775
- * @param baseBottom 既存の図の下端 (`partsBaseBottom`)。 その下に間を空けて並べ始める。
776
- * 箱が 1 つも無い図では `undefined` を渡す
777
- */
778
- export function partsGridCenters(
779
- baseBottom: number | undefined,
780
- items: ReadonlyArray<{ id: string; w: number; h: number }>,
781
- ): Map<string, { cx: number; cy: number }> {
782
- const out = new Map<string, { cx: number; cy: number }>();
783
- if (items.length === 0) return out;
784
- // 公開している関数なので、 呼出側が渡す値を入口で閉じる。 数でない下端や桁溢れを
785
- // そのまま計算に入れると、 描けない座標を返すことになる。
786
- //
787
- // 負の下端は捨てない = 図は原点より上にも置ける。 0 で下げ止めると、上にある図の下に
788
- // 無駄な空きが入る
789
- const top =
790
- baseBottom === undefined || !Number.isFinite(baseBottom)
791
- ? PARTS_GAP * 2
792
- : baseBottom + PARTS_GAP;
793
- // 同じ名前が 2 度来たら先の方だけを見る。 後の分を残すと、 どちらを指したか決められない
794
- // まま列の送り幅にも影響する
795
- const seen = new Set<string>();
796
- const unique = items.filter((i) => {
797
- if (seen.has(i.id)) return false;
798
- seen.add(i.id);
799
- return true;
800
- });
801
- const cellW = maxOf(
802
- unique.map((i) => positiveOr(i.w, 400)),
803
- 400,
804
- );
805
- const rowTops: number[] = [];
806
- {
807
- let y = top;
808
- for (let i = 0; i < unique.length; i += PARTS_PER_ROW) {
809
- rowTops.push(y);
810
- const rowH = maxOf(
811
- unique.slice(i, i + PARTS_PER_ROW).map((x) => positiveOr(x.h, 200)),
812
- 200,
813
- );
814
- y += rowH + PARTS_GAP;
815
- }
816
- }
817
- unique.forEach((item, i) => {
818
- const col = i % PARTS_PER_ROW;
819
- const row = Math.floor(i / PARTS_PER_ROW);
820
- const cx = col * (cellW + PARTS_GAP) + cellW / 2;
821
- const cy = (rowTops[row] ?? top) + positiveOr(item.h, 200) / 2;
822
- // 桁溢れした座標は描けない。 返さずに落として、 呼出側が自動配置に倒せるようにする
823
- if (!Number.isFinite(cx) || !Number.isFinite(cy)) return;
824
- out.set(item.id, { cx, cy });
825
- });
826
- return out;
827
- }
828
-
829
- /**
830
- * 取り込んでよい見本の名前 (#1015)。
831
- *
832
- * 1 件ずつが上限以下でも、同じ見本を別名で何度も参照すれば合計は上限を超える
833
- * (実測 = 1,001 要素の見本を 3 名で参照して最終図が 3,005 要素になった)。
834
- * 本体の分を引いた残りを予算とし、本文に書かれた順に配る。
835
- *
836
- * 順に配るのは、どれを落とすかを決める規則が要るため。 先に書いたものを優先する形なら、
837
- * 書いた人から見て「後ろが落ちる」 と読める。
838
- */
839
- function partsBudget(
840
- target: CdlDiagram,
841
- partsActors: ReadonlyArray<{ name: string; partId?: string }>,
842
- partsCatalog: Record<string, CdlDiagram>,
843
- ): Set<number> {
844
- // 名前ではなく **書かれた順番** で覚える。 名前で覚えると、同じ名前を 2 度書いた時に
845
- // 先の 1 件が入れた名前で後の 1 件まで採用扱いになる
846
- const accepted = new Set<number>();
847
- let used = countDiagramElements(target);
848
- partsActors.forEach((a, i) => {
849
- const part = lookupPart(partsCatalog, a.partId);
850
- if (part === undefined) return;
851
- const cost = countDiagramElements(part);
852
- if (used + cost > MAX_INPUT_ELEMENTS) return;
853
- used += cost;
854
- accepted.add(i);
855
- });
856
- return accepted;
857
- }
858
-
859
- /**
860
- * パーツを除いた図を組み立てて、その下端を返す (#2002)。 箱が 1 つも無ければ `undefined`。
861
- *
862
- * **宣言のままの箱は座標を持たない** (`CdlNode` に `cx` / `cy` が無い)。 下端を知るには
863
- * 一度組み立てる必要がある。 パーツを除いた図を組み立てるのは、パーツを含めて測ると
864
- * パーツを足すたびに下端が下がり、置き場所が自分自身に追随して逃げるため。
865
- *
866
- * ## 除く 3 つは、いまの呼び出し順では結果を変えない
867
- *
868
- * 箱 / 縦列 / 矢印の 3 つを除いているが、**この 3 つを残しても下端は 1 も動かない**
869
- * (変異試験で 3 つとも、また 3 つ同時でも落ちる検査は 0 件)。 この関数が走るのは
870
- * パーツを取り込む前で、その時点のパーツの仮の箱は書いた位置も縦列の段も反映しておらず、
871
- * 必ず本体の箱と同じ段に居るため。 図種 5 種 × 段の深さ 2 通り、位置を書いた深さ 3 通り、
872
- * 縦列に置いた段数 2 通りを実測して、1 件も差が出なかった。
873
- *
874
- * それでも残すのは、この関数が「パーツを除いた図を測る」 と読めることに意味があるため。
875
- * 呼び出し順が変わった時に黙って下端が動くより、除く形が書いてある方が直しやすい。
876
- */
877
- function baseDiagramBottom(
878
- target: CdlDiagram,
879
- baseNodes: readonly CdlDiagram["nodes"][number][],
880
- partsLaneIds: ReadonlySet<string>,
881
- ): number | undefined {
882
- // 箱が 1 つも無い図では組み立てない。 **返す値を変えるための分岐ではない**
883
- // (`partsBaseBottom` も箱 0 個で `undefined` を返す) = 組み立て 1 回ぶんを省くためだけの
884
- // 早い戻り。 変異試験でこの行を外しても落ちる検査は 0 件で、それが期待どおり
885
- if (baseNodes.length === 0) return undefined;
886
- const baseIds = new Set(baseNodes.map((n) => n.id));
887
- // 部品を外した並びの下端を測るだけで、この配置は残らない (#2312)
888
- return partsBaseBottom(
889
- 測るために配置する({
890
- ...target,
891
- lanes: target.lanes.filter((l) => !partsLaneIds.has(l.id)),
892
- nodes: [...baseNodes],
893
- edges: target.edges.filter((e) => baseIds.has(e.from) && baseIds.has(e.to)),
894
- }),
895
- );
896
- }
897
-
898
- /**
899
- * 位置を書かなかったパーツの、 merge に渡す座標。
900
- *
901
- * 格子の規則は `partsGridCenters` が持つ。 merge は矩形の中心を渡された座標に合わせるので、
902
- * 中心をそのまま渡す。
903
- */
904
- function partGridCenters(
905
- target: CdlDiagram,
906
- doc: DslDocument,
907
- partsCatalog: Record<string, CdlDiagram>,
908
- /** 取り込む見本の書かれた順番。 渡さなければ全部を並べる */
909
- accepted?: ReadonlySet<number>,
910
- /** 順番の元になった一覧 (本文に書かれた順) */
911
- acceptedFrom?: ReadonlyArray<{ name: string }>,
912
- ): Map<string, { cx: number; cy: number }> {
913
- const partsActors = doc.actors.filter((a) => a.partId !== undefined);
914
- // 格子に並ぶのは座標を 1 つも書かず相対でも書かなかった分だけ。
915
- //
916
- // merge 側は「縦横どちらも書かなかった時」 に格子へ落とす。 条件が食い違うと、 片方だけ
917
- // 書いたパーツが格子の枠を 1 つ消費して後続がずれる (実測 = 後続の中心が 200 から 720 に動いた)
918
- //
919
- // 縦列に置く部品も外す (#1980)。 格子の枠を使うと、縦列へ動かした後に後続の部品との間に
920
- // 空きが残る。 相対の位置を解く側 (`partBoxes`) も同じ関数を通るので、基準の位置は揃う
921
- const 縦列の部品 = 縦列に置く部品(target, doc).縦列;
922
- const autoActors = partsActors.filter(
923
- (a) =>
924
- a.posX === undefined &&
925
- a.posY === undefined &&
926
- a.posRel === undefined &&
927
- !縦列の部品.has(a.name),
928
- );
929
- if (autoActors.length === 0) return new Map();
930
- // パーツ自身の仮の箱は数えない。 この時点では未削除で残っており、 数えるとパーツを足すたびに
931
- // 置き場所が下へずれる。
932
- //
933
- // 名札 (`title`) だけを見ると、 順序図で 1 人につき作られる 3 つの箱のうち間隔用のものが
934
- // 漏れる (名札が空のため)。 パーツ 1 個につき 1 つ残り、 格子の起点が 1 段ぶん下がって
935
- // 画面側とずれていた (実測 = 縦が 840 = 3 段ぶん違った)。
936
- //
937
- // 属する列で特定するが、 列の id は名前を slug に変換して作るため名前とは一致しない
938
- // (実測 = `My Part` の列 id は `My-Part`)。 名前で引くと記号を含む名前だけ取りこぼす。
939
- // 列の `label` は slug の経路によらず名前の生値を持つので、 そちらで引く (§ merge の
940
- // 仮の箱の掃除が同じ方法を採っている)。
941
- //
942
- // どの列がパーツのものか決められない時は、 数から外さない。 外す側に倒すと本体の箱まで
943
- // 消えて、 パーツが本体の図に重なる (実測 = 同じ名前を本体とパーツの両方に書くと、
944
- // 上端が 1140 から 300 に飛んで本体の中に入った)。 外さなければ間隔が 1 段ぶん広がるだけで済む
945
- const otherActorNames = new Set(
946
- doc.actors.filter((a) => a.partId === undefined).map((a) => a.name),
947
- );
948
- // 本体にも同じ名前がある分は外さない。 名札でも列でも本体と区別できないため
949
- const partsActorNames = new Set(
950
- partsActors.map((a) => a.name).filter((n) => !otherActorNames.has(n)),
951
- );
952
- // 明示的に他の列へ張ったパーツは、 その列を専有していない (本体と共有している)
953
- const sharedLaneIds = new Set(
954
- partsActors.map((a) => a.lane).filter((l): l is string => l !== undefined),
955
- );
956
- const partsLaneIds = new Set<string>();
957
- for (const l of target.lanes) {
958
- if (l.label === undefined) continue;
959
- if (!partsActorNames.has(l.label)) continue;
960
- if (sharedLaneIds.has(l.id)) continue;
961
- partsLaneIds.add(l.id);
962
- }
963
- // 相対で書いた箱も数えない (#2041)。 位置が決まるのは格子を決めた後なので、数えると
964
- // **1 度目と 2 度目で起点が動く** = 基準の範囲を出す時 (`partBoxes`) は箱が自動配置の
965
- // 位置に居り、取り込む時 (`mergePartsFromActors`) には相対で解いた位置へ移っている。
966
- //
967
- // 横の間隔は書いたとおりに空くのに縦だけが外れる形で表に出る (実測 = 基準のパーツが
968
- // `cy=1118`、その右に置いた箱が `cy=654` で 464 離れた)。 効いたかを見る側
969
- // (`verifyPlacement`) も同じ `partBoxes` の値を期待値にするため、実物と期待値が同じ向きに
970
- // ずれて一致し、知らせを 1 件も出さないまま絵だけが崩れる。
971
- //
972
- // **座標で書いた箱は数える**。 こちらは格子より先に位置が決まっており、1 度目と 2 度目で
973
- // 動かない。 外すと、座標で下に置いた箱にパーツが重なる。
974
- const 相対で置く箱 = new Set(doc.actors.filter((a) => a.posRel !== undefined).map((a) => a.name));
975
- const baseNodes = target.nodes.filter(
976
- (n) => !partsActorNames.has(n.title) && !partsLaneIds.has(n.lane) && !相対で置く箱.has(n.title),
977
- );
978
- // 取り込まれない見本は格子の枠を使わない (#1015)。 枠を使うと、落とした見本の分だけ
979
- // 後続がずれる (実測 = 隣の見本の左端が 60 から 725 に動いた)
980
- const acceptedNames =
981
- accepted === undefined || acceptedFrom === undefined
982
- ? undefined
983
- : new Set(acceptedFrom.filter((_, i) => accepted.has(i)).map((a) => a.name));
984
- const placedActors = autoActors.filter(
985
- (a) =>
986
- lookupPart(partsCatalog, a.partId) !== undefined &&
987
- (acceptedNames === undefined || acceptedNames.has(a.name)),
988
- );
989
- const extents = new Map<string, { w: number; h: number; dx: number; dy: number }>();
990
- for (const a of placedActors) {
991
- const part = lookupPart(partsCatalog, a.partId)!;
992
- // 格子は図枠で決める。 画面側も図枠をそのまま置くので、 同じ物差しで並べれば
993
- // 2 経路の置き場所が揃う (#937)
994
- const t = partTargetSize(part, a.posW, a.posH, a.scale);
995
- extents.set(a.name, partFrameExtent(part, t.w, t.h));
996
- }
997
- const centers = partsGridCenters(
998
- baseDiagramBottom(target, baseNodes, partsLaneIds),
999
- placedActors.map((a) => ({ id: a.name, ...extents.get(a.name)! })),
1000
- );
1001
- // merge に渡すのは段の中心。 矩形の中心とのずれを引く。 引かないと、 段ごとに箱の高さが
1002
- // 違うパーツで段内の上端が揃わない (実測 = 対称なパーツの上端 520 に対して 507.5)
1003
- const out = new Map<string, { cx: number; cy: number }>();
1004
- for (const [name, c] of centers) {
1005
- const e = extents.get(name)!;
1006
- out.set(name, { cx: c.cx - e.dx, cy: c.cy - e.dy });
1007
- }
1008
- return out;
1009
- }
1010
-
1011
- /**
1012
- * パーツごとの外接矩形 (catalog 由来)。 相対指定を解く時に自分の大きさとして使う。
1013
- *
1014
- * `dx` / `dy` は矩形の中心と merge に渡す座標のずれ。 狙った中心から引いて座標にする。
1015
- */
1016
- export function partSizes(
1017
- doc: DslDocument,
1018
- partsCatalog: Record<string, CdlDiagram>,
1019
- ): Map<string, { w: number; h: number; dx: number; dy: number }> {
1020
- const out = new Map<string, { w: number; h: number; dx: number; dy: number }>();
1021
- for (const a of doc.actors) {
1022
- if (a.partId === undefined) continue;
1023
- const part = lookupPart(partsCatalog, a.partId);
1024
- if (part) {
1025
- const t = partTargetSize(part, a.posW, a.posH, a.scale);
1026
- out.set(a.name, partExtent(part, t.w, t.h));
1027
- }
1028
- }
1029
- return out;
1030
- }
1031
-
1032
- /**
1033
- * パーツの箱 (中心と大きさ)。 相対指定を解く時の基準として使う。
1034
- *
1035
- * 大きさは catalog の図から求める。 組み立て前の図に残っている仮の箱を測ると、 実際に
1036
- * 描かれる大きさと違う値で間隔を計算することになる。
1037
- */
1038
- export function partBoxes(
1039
- target: CdlDiagram,
1040
- doc: DslDocument,
1041
- partsCatalog: Record<string, CdlDiagram>,
1042
- ): Map<string, AnchorBox> {
1043
- const grid = partGridCenters(target, doc, partsCatalog);
1044
- const out = new Map<string, AnchorBox>();
1045
- for (const a of doc.actors) {
1046
- if (a.partId === undefined) continue;
1047
- const part = lookupPart(partsCatalog, a.partId);
1048
- if (!part) continue;
1049
- const t = partTargetSize(part, a.posW, a.posH, a.scale);
1050
- const size = partExtent(part, t.w, t.h);
1051
- const placed =
1052
- a.posX !== undefined && a.posY !== undefined ? { cx: a.posX, cy: a.posY } : grid.get(a.name);
1053
- // 相対で書いた分はここでは決まらない (解決側が後で埋める)
1054
- if (!placed) continue;
1055
- // 渡す座標は段の中心。 矩形の中心はそこからずれる
1056
- out.set(a.name, { cx: placed.cx + size.dx, cy: placed.cy + size.dy, w: size.w, h: size.h });
1057
- }
1058
- return out;
1059
- }
1060
-
1061
- /**
1062
- * 部品へ引いた矢印を、部品の図の要素へ繋ぎ直す手順を作る (#1979)。
1063
- *
1064
- * 部品を置くと組み立ては仮の箱を消し、部品の図の要素を `{名前}__{要素の id}` で足す。 仮の箱へ
1065
- * 引いた矢印は繋ぎ先を失うため、以前は知らせも無く消えていた。
1066
- *
1067
- * | 部品 | 繋ぎ先 |
1068
- * |---|---|
1069
- * | 要素 1 つ | その要素 |
1070
- * | 要素 2 つ以上 | 矢印の行に書いた `fromPartNode` / `toPartNode` の要素 |
1071
- *
1072
- * **要素が 2 つ以上ある部品で 1 つを自動で選ばない**。 カタログの部品は外枠を持たず同格の要素が並ぶ
1073
- * (信号の 3 灯、星 5 つ、棒 5 本) ため、最初の要素や一番大きい要素を選ぶと「その 1 つだけ」 を
1074
- * 指す矢印に見える。 名指しが無い時、名指しした要素が無い時、部品を取り込まなかった時 (`part` が
1075
- * `undefined`) は矢印を外し、矢印ごとに 1 件知らせる。
1076
- *
1077
- * **繋ぐのは書いた矢印だけ** = 矢印を書いた行の端の名前が、この部品の名前と一致する時。 静止した
1078
- * `type: flow` は行を書かなくても箱を並び順で繋ぐが、その矢印は書き手が部品へ引いたものではないので
1079
- * 従来どおり知らせずに外す。 繋ぐと、部品を本文から抜いて図の上に重ねる編集画面 (行が部品を指す
1080
- * 本文だけを抜かずに描く) と絵が食い違う (実測 = 箱と部品を並べただけの本文に、組み立て側だけ矢印が出た)。
1081
- */
1082
- function 部品の要素へ繋ぐ(
1083
- 部品の名前: string,
1084
- partId: string,
1085
- part: CdlDiagram | undefined,
1086
- edgeSteps: Map<string, DslStep> | undefined,
1087
- onNotice: ((notice: CompileNotice) => void) | undefined,
1088
- ): (edge: CdlEdge, 端: { from: boolean; to: boolean }) => CdlEdge | undefined {
1089
- const 要素 = (part?.nodes ?? []).map((n) => n.id);
1090
- const 見せる数 = 8;
1091
- const 要素の一覧 = `${要素.slice(0, 見せる数).map(truncateForMessage).join(", ")}${要素.length > 見せる数 ? ` ほか ${要素.length - 見せる数} 件` : ""}`;
1092
- return (edge, 端) => {
1093
- const s = edgeSteps?.get(edge.id);
1094
- const 繋いだ: CdlEdge = { ...edge };
1095
- for (const 側 of ["from", "to"] as const) {
1096
- if (!端[側]) continue;
1097
- // 書いていない矢印 (並び順で作られた矢印) は知らせずに外す
1098
- if (s === undefined || s[側] !== 部品の名前) return undefined;
1099
- const 欄 = 側 === "from" ? "fromPartNode" : "toPartNode";
1100
- const 名指し = s[欄];
1101
- let 理由: { message: string; hint: string } | undefined;
1102
- if (part === undefined) {
1103
- 理由 = {
1104
- message: "を図に取り込まなかった",
1105
- hint: "部品が図に入らないため、矢印の端にできません",
1106
- };
1107
- } else if (名指し !== undefined) {
1108
- if (要素.includes(名指し)) 繋いだ[側] = `${部品の名前}__${名指し}`;
1109
- else
1110
- 理由 = {
1111
- message: `の中に ${欄} に書いた "${truncateForMessage(名指し)}" という要素が無い`,
1112
- hint: `この部品の要素 = ${要素の一覧}`,
1113
- };
1114
- } else if (要素.length === 1) {
1115
- 繋いだ[側] = `${部品の名前}__${要素[0]}`;
1116
- } else {
1117
- 理由 =
1118
- 要素.length === 0
1119
- ? {
1120
- message: "の中に繋げる要素が無い",
1121
- hint: "この部品は要素を持たないため、矢印の端にできません",
1122
- }
1123
- : {
1124
- message: "の中のどの要素に繋ぐかが決まらない",
1125
- hint: `要素が 2 つ以上ある部品は、矢印に ${欄}: <要素の id> を書いて繋ぐ要素を選ぶ (要素 = ${要素の一覧})`,
1126
- };
1127
- }
1128
- if (理由 === undefined) continue;
1129
- const 矢印 = `"${truncateForMessage(s.from)}" から "${truncateForMessage(s.to)}" への矢印`;
1130
- onNotice?.({
1131
- kind: "part-edge-dropped",
1132
- actor: 部品の名前,
1133
- line: s.pos.line,
1134
- message: `${矢印}は、"${truncateForMessage(部品の名前)}" (${truncateForMessage(partId)}) ${理由.message}ため外しました`,
1135
- hint: 理由.hint,
1136
- });
1137
- return undefined;
1138
- }
1139
- return 繋いだ;
1140
- };
1141
- }
1142
-
1143
- /**
1144
- * 矢印に書いた部品の要素の名指し (`fromPartNode` / `toPartNode`) が効かないことを知らせる (#1979)。
1145
- *
1146
- * 名指しは部品の端でだけ読む。 部品でない箱の端に書くと値はどこにも届かず、黙って捨てると
1147
- * 「書いたのに繋ぎ先が変わらない」 が手掛かりなしで起きる。
1148
- *
1149
- * 順序図 (`sequence`) は板の言づてが縦の線に届き、部品の要素へは繋がないため、
1150
- * 部品の端に書いても効かない。 部品の一覧に無い部品は組み立てが部品を引いてから知らせる。
1151
- */
1152
- export function reportPartNodeNotHonored(
1153
- doc: DslDocument,
1154
- onNotice?: (notice: CompileNotice) => void,
1155
- ): void {
1156
- if (!onNotice) return;
1157
- const 順序図 = doc.type === "sequence";
1158
- const 部品 = new Set(doc.actors.filter((a) => a.partId !== undefined).map((a) => a.name));
1159
- const 居る = new Set(doc.actors.map((a) => a.name));
1160
- for (const s of doc.flow) {
1161
- for (const [側, 欄] of [
1162
- ["from", "fromPartNode"],
1163
- ["to", "toPartNode"],
1164
- ] as const) {
1165
- const 値 = s[欄];
1166
- if (値 === undefined) continue;
1167
- const 名 = s[側];
1168
- // 居ない名前は `flow-actor-missing` が知らせる
1169
- if (!居る.has(名)) continue;
1170
- if (!順序図 && 部品.has(名)) continue;
1171
- onNotice({
1172
- kind: "part-node-ignored",
1173
- actor: 名,
1174
- line: s.pos.line,
1175
- message: 順序図
1176
- ? `順序図の言づては "${truncateForMessage(名)}" の縦の線に届くため、${欄} に書いた "${truncateForMessage(値)}" は効きません`
1177
- : `"${truncateForMessage(名)}" は部品ではないため、${欄} に書いた "${truncateForMessage(値)}" は効きません`,
1178
- hint: 順序図
1179
- ? "部品の中の要素へ矢印を繋ぐのは、矢印を箱の間に引く図種 (flow / swimlane など)"
1180
- : "部品の中の要素の名指しは、kind に部品の名前を書いた箱の端にだけ効く",
1181
- });
1182
- }
1183
- }
1184
- }
1185
-
1186
- /**
1187
- * 部品のために作られた仮の箱の id かを見分ける。
1188
- *
1189
- * 仮の箱の id は部品の名前の slug か、それを頭に持つ形 (`a-header` / `s0-a`) で作られる。
1190
- * 仮の箱を消す側 (`cleanupPlaceholderActor`) と、部品を置く縦列を探す側 (`縦列に置く部品`) が
1191
- * 同じ判定を使う = 別々に持つと、消した箱と縦列を探した箱が食い違う。
1192
- *
1193
- * 素の登場人物が持つ id は仮の箱に数えない (#1466)。 名前が重なった素の登場人物は
1194
- * `重なりを解く` 側で `a-c40bf6` のような id に作り替えられるため、同じ頭で始まり
1195
- * **本体の箱まで巻き込む** (実測で `flow` / `swimlane` / `er` / `state` / `topology` /
1196
- * `class` / `c4` の 7 図種すべてで素の箱が黙って消えていた)。
1197
- *
1198
- * 名前の頭が自分と重なる別の登場人物 (`設備` に対する `設備-予備`) の仮の箱も数えない (#1980)。
1199
- * 数えると `設備` の仮の箱が 2 本の縦列にまたがって見え、`設備` を消す時に `設備-予備` の
1200
- * 仮の箱と縦列まで消す。 部品同士でも起き、後から組み込む `設備-予備` が消えた縦列を指して
1201
- * 配置が止まっていた (`cdl layout: node "設備-予備__ind" の lane "設備-予備" が定義されていない`)。
1202
- */
1203
- function 仮の箱のidか(doc: DslDocument, name: string): (id: string) => boolean {
1204
- const aliasSlug = slugify(name);
1205
- const 素の箱のid = new Set(
1206
- doc.actors.filter((x) => x.partId === undefined).map((x) => slugify(x.name)),
1207
- );
1208
- const 頭が重なるid = [
1209
- ...new Set(
1210
- doc.actors
1211
- .filter((x) => x.name !== name)
1212
- .map((x) => slugify(x.name))
1213
- .filter((s) => s.startsWith(`${aliasSlug}-`) || s.endsWith(`-${aliasSlug}`)),
1214
- ),
1215
- ];
1216
- // sequence step anchor = `s{N}-{slug}` pattern
1217
- const 自分の形か = (id: string, slug: string): boolean =>
1218
- id === slug || id.startsWith(`${slug}-`) || (/^s\d+-/.test(id) && id.endsWith(`-${slug}`));
1219
- return (id: string): boolean => {
1220
- if (素の箱のid.has(id)) return false;
1221
- if (頭が重なるid.some((s) => 自分の形か(id, s))) return false;
1222
- return 自分の形か(id, aliasSlug);
1223
- };
1224
- }
1225
-
1226
- /**
1227
- * 縦列の中に置く部品と、その縦列の id (#1980)。
1228
- *
1229
- * 位置を書かない部品は格子に並ぶが、格子は縦列の位置を読まない。 部品が縦列に属する図では、
1230
- * 部品の要素だけが縦列に属し、描く位置は別の縦列の上になっていた (実測 = 縦列 b 480〜890 に
1231
- * 属する部品が 60〜420 に描かれた)。
1232
- *
1233
- * 縦列に置くのは、位置 (`posX` / `posY` / `位置:`) を書かない部品のうち次のどちらかに当たるもの。
1234
- *
1235
- * | 形 | 縦列 |
1236
- * |---|---|
1237
- * | `lane:` に図にある縦列を書いた | 書いた縦列 |
1238
- * | 仮の箱が自分だけの縦列を持つ (`swimlane` / `state` / `er` / `class` の作り方) | その縦列 |
1239
- *
1240
- * **自分だけの縦列は図の形で見分ける**。 図種の名前で分けると、縦列の作り方を変えた図種で
1241
- * 黙って外れる。 仮の箱が入った縦列に他の登場人物の箱が無く、図に縦列が 2 本以上ある時に
1242
- * 限る = 全員が 1 本の縦列を共有する図種 (`flow` / `topology` / `c4`) は格子のまま残る。
1243
- * 仮の箱が作られない形 (流れに現れない `swimlane` の登場人物) は、名札が部品の名前の空の縦列で引く。
1244
- *
1245
- * **順序図 (`sequence`) は対象にしない**。 1 枚の板で描き、縦列を持たない (#1466)。
1246
- *
1247
- * **他の箱の位置の基準になっている部品は縦列に置かない**。 相対の位置 (`位置: 印 の右 200`) は
1248
- * 組み立ての前に格子の位置を基準に解くため、基準の部品だけを縦列へ動かすと書いた位置関係が
1249
- * 崩れる。 外した部品は `基準のため外した` に入れ、呼出側が知らせる。
1250
- */
1251
- function 縦列に置く部品(
1252
- target: CdlDiagram,
1253
- doc: DslDocument,
1254
- ): { 縦列: Map<string, string>; 基準のため外した: Map<string, string> } {
1255
- const 縦列 = new Map<string, string>();
1256
- const 基準のため外した = new Map<string, string>();
1257
- if (doc.type === "sequence") return { 縦列, 基準のため外した };
1258
- const 基準の名前 = new Set(
1259
- doc.actors.map((a) => a.posRel?.anchor).filter((n): n is string => n !== undefined),
1260
- );
1261
- const 縦列のid = new Set(target.lanes.map((l) => l.id));
1262
- for (const a of doc.actors) {
1263
- if (a.partId === undefined) continue;
1264
- // 同じ名前を 2 度書いた時は先の 1 件を使う (組み立て側の他の判定と同じ)
1265
- if (縦列.has(a.name) || 基準のため外した.has(a.name)) continue;
1266
- if (a.posX !== undefined || a.posY !== undefined || a.posRel !== undefined) continue;
1267
- const 置く縦列 = 部品の縦列(target, doc, a, 縦列のid);
1268
- if (置く縦列 === undefined) continue;
1269
- if (基準の名前.has(a.name)) 基準のため外した.set(a.name, 置く縦列);
1270
- else 縦列.set(a.name, 置く縦列);
1271
- }
1272
- return { 縦列, 基準のため外した };
1273
- }
1274
-
1275
- /** 部品を置く縦列。 書いた縦列か、仮の箱が自分だけで使う縦列。 どちらも無ければ `undefined` */
1276
- function 部品の縦列(
1277
- target: CdlDiagram,
1278
- doc: DslDocument,
1279
- a: DslActor,
1280
- 縦列のid: ReadonlySet<string>,
1281
- ): string | undefined {
1282
- if (a.lane !== undefined) return 縦列のid.has(a.lane) ? a.lane : undefined;
1283
- if (target.lanes.length < 2) return undefined;
1284
- const 仮の箱か = 仮の箱のidか(doc, a.name);
1285
- const 仮の箱の縦列 = new Set(target.nodes.filter((n) => 仮の箱か(n.id)).map((n) => n.lane));
1286
- // 2 本にまたがる入力は見つかっていない (実測 = 縦列に置く 7 図種とも仮の箱は登場人物ごとに 1 つ。
1287
- // 名前の頭が重なる登場人物の箱は `仮の箱のidか` が外す)。 またがった時にどちらかの縦列を選ぶと
1288
- // 他の縦列を部品が占めるため、格子に残す
1289
- if (仮の箱の縦列.size > 1) return undefined;
1290
- const [入っていた] = [...仮の箱の縦列];
1291
- const 候補 =
1292
- 入っていた ??
1293
- target.lanes.find((l) => l.label === a.name && !target.nodes.some((n) => n.lane === l.id))?.id;
1294
- if (候補 === undefined) return undefined;
1295
- // 他の登場人物の箱が同じ縦列に居れば、自分だけの縦列ではない
1296
- return target.nodes.some((n) => n.lane === 候補 && !仮の箱か(n.id)) ? undefined : 候補;
1297
- }
1298
-
1299
- /**
1300
- * パーツ用に作られた仮の箱 / 線 / 列を掃除する (#1015 で helper 化)。
1301
- *
1302
- * 取り込む時だけでなく **落とす時にも呼ぶ**。 落とした時に残すと、格子から外した後続の見本と
1303
- * 重なる (実測で 64,000 の重なりが出た)。
1304
- */
1305
- function cleanupPlaceholderActor(
1306
- target: CdlDiagram,
1307
- doc: DslDocument,
1308
- a: { name: string; lane?: string },
1309
- /**
1310
- * 仮の箱に繋がっていた矢印を、部品の図の要素へ繋ぎ直す (#1979)。 仮の箱の側の端を受け取り、
1311
- * 繋ぎ直した矢印を返す。 `undefined` を返した矢印は外す。 渡さなければ全て外す
1312
- */
1313
- 繋ぎ直す?: (edge: CdlEdge, 端: { from: boolean; to: boolean }) => CdlEdge | undefined,
1314
- ): void {
1315
- const aliasSlug = slugify(a.name);
1316
- const ownedLaneIds = new Set<string>();
1317
- if (doc.type === "sequence") {
1318
- for (const l of target.lanes) {
1319
- // 明示 lane mapping (a.lane) 先は part の張替え先で actor 専用 lane ではないため除外
1320
- if (a.lane !== undefined && l.id === a.lane) continue;
1321
- if (l.label === a.name) ownedLaneIds.add(l.id);
1322
- }
1323
- }
1324
- const ownedNodeIds = new Set<string>();
1325
- for (const n of target.nodes) {
1326
- if (ownedLaneIds.has(n.lane)) ownedNodeIds.add(n.id);
1327
- }
1328
- // actor 専用 lane を引き当てられない経路 (flow / topology 等の共有 lane preset) は従来どおり dragon
1329
- // slug の prefix match に fallback する。 これらは 1 actor = 1 node (id = slug) の生成規則。
1330
- const matchesAliasSlug = 仮の箱のidか(doc, a.name);
1331
- const relatedToActor = (id: string): boolean =>
1332
- ownedLaneIds.size > 0 ? ownedNodeIds.has(id) : matchesAliasSlug(id);
1333
- // 仮の箱が入っていた縦列。 仮の箱を消して空になった縦列は下で消す (#1973)
1334
- const 仮の箱の縦列 = new Set(target.nodes.filter((n) => relatedToActor(n.id)).map((n) => n.lane));
1335
- target.nodes = target.nodes.filter((n) => !relatedToActor(n.id));
1336
- // 仮の箱に繋がっていた矢印は、部品の要素へ繋ぎ直せたものだけ残す (#1979)。 以前は全て消しており、
1337
- // 部品へ引いた矢印が知らせも無く図から消えていた。 繋ぎ直した矢印は id を変えないので、段の
1338
- // 点灯 (`activate`) からも外さない。 外した矢印の id は `activate` に残ると存在しない参照になるため回収する
1339
- const removedEdgeIds = new Set<string>();
1340
- const 残す矢印: CdlEdge[] = [];
1341
- for (const e of target.edges) {
1342
- const 端 = { from: relatedToActor(e.from), to: relatedToActor(e.to) };
1343
- if (!端.from && !端.to) {
1344
- 残す矢印.push(e);
1345
- continue;
1346
- }
1347
- const 繋いだ = 繋ぎ直す?.(e, 端);
1348
- if (繋いだ) 残す矢印.push(繋いだ);
1349
- else removedEdgeIds.add(e.id);
1350
- }
1351
- target.edges = 残す矢印;
1352
- // lane も削除 = sequence preset は parts actor 用に lane (id = aliasSlug、 label = actor 名) を
1353
- // 生成する。 node/edge だけ消して lane を残すと、 merge 後の part 側 lane (label = alias) と 2 本が
1354
- // 同じ label を lane-label として描画し二重表示になる (actor ラベル二重表示 bug の root cause)。
1355
- //
1356
- // 削除は順序図 (sequence = compileSequence 経由) に限定する。 これらは
1357
- // 1 actor = 1 lane (lane.label === a.name、 lane.id は actor 名の slug) の生成規則が成立し、
1358
- // parts actor 用 lane を安全に削除できる。 他 preset (flow / topology / class / pie 等) は複数
1359
- // actor が共有 lane (id = "main" 等) を参照するため、 一致 lane を消すと通常 actor の node が
1360
- // 削除済 lane を参照する不正 diagram になる。
1361
- //
1362
- // leftover lane の特定は lane.label === a.name を第一に使う。 seq-like preset は非 animate 経路
1363
- // (cdl preset の slugify) と animate 経路 (dragon の slugify) で lane.id の slug 規則が異なり
1364
- // (`_`/全角の扱い等)、 aliasSlug (dragon slugify) と lane.id が不一致になる actor 名がある。 lane.label
1365
- // は両経路とも a.name 生値なので slug 差の影響を受けず確実に一致する。 id === aliasSlug は
1366
- // label 未設定 preset への fallback (exact match のみ、 prefix は false match risk のため付けない)。
1367
- /*
1368
- * **図種で分けない** (#1466)。 以前は順序図系だけを掃除していたが、順序図が板になって
1369
- * 面ごとの縦列を作らなくなり、この分岐は誰も通らなくなった。 一方で縦列を作る他の図種
1370
- * (`swimlane` 等) では見本の仮の縦列が空のまま残っていた (実測)。
1371
- *
1372
- * **中身が残っている縦列は消さない**。 1 本の縦列を全員で共有する図種 (`flow` / `topology`)
1373
- * では、その縦列の名札がたまたま登場人物の名前と一致することがある。 消すと本体の箱が
1374
- * 行き場を失う。
1375
- */
1376
- const 残る箱を持つ = new Set(target.nodes.map((n) => n.lane));
1377
- target.lanes = target.lanes.filter((l) => {
1378
- // 明示 lane mapping (a.lane) 先は part の張替え先なので保持する。
1379
- if (a.lane !== undefined && l.id === a.lane) return true;
1380
- if (残る箱を持つ.has(l.id)) return true;
1381
- if (l.label === a.name) return false;
1382
- if (l.id === aliasSlug) return false;
1383
- // 図種が自動で作った縦列 (`flow` 等) に部品の箱しか無かった図では、仮の箱を消すと縦列が
1384
- // 空のまま残る (実測 = 部品 1 つだけの図に中身の無い `flow` の縦列)。 書き手が `lanes:` に
1385
- // 書いた縦列は、空でも書いたとおりに残す (#1973)
1386
- if (仮の箱の縦列.has(l.id) && !Object.hasOwn(doc.lanes ?? {}, l.id)) return false;
1387
- return true;
1388
- });
1389
- // 削除された node / edge を activate 参照している既存 phase の cleanup (node 削除と同じ判定経路
1390
- // = 取りこぼすと存在しない id が activate に残り dangling 参照になる、 #873)
1391
- for (const phase of target.phases) {
1392
- phase.activate = phase.activate.filter((id) => !relatedToActor(id) && !removedEdgeIds.has(id));
1393
- }
1394
- }
1395
-
1396
- /**
1397
- * 種類に書いた名前が箱の種類にも部品の一覧にも無いことを、書いた行で伝える (#2113)。
1398
- *
1399
- * 記法は箱の種類に無い名前を部品の名前とみなすため、箱の種類の綴り違い (`evnet`) もここに来る。
1400
- * 図は種類を書かなかった箱になる。 `console.warn` だけに出すと編集画面に理由が見えないので、
1401
- * 知らせと `console.warn` の両方へ同じ文を出す。
1402
- *
1403
- * 案内には、箱の種類と一覧の部品の名前から綴りの近い名前を 1 つ出す。 部品の名前は `parts-` の
1404
- * 前置きを外した形で勧める = どちらの形でも引けるので、短い方を書けば足りる。
1405
- *
1406
- * **部品を 1 つも持たない一覧では知らせない**。 比べる部品の名前が無く、部品を書いたのか綴りを
1407
- * 間違えたのかを決められない (一覧を渡さない時と同じ)。 編集画面は部品を読み込み終わる前と
1408
- * 読み込みに失敗した後に空の一覧を渡すため、知らせると正しい部品の名前にも注意が出る。
1409
- * 図は一覧を渡さない時と同じなので、知らせの有無だけを揃える。
1410
- */
1411
- function 部品が一覧に無いことを伝える(
1412
- actor: DslActor,
1413
- partId: string,
1414
- partsCatalog: Record<string, CdlDiagram>,
1415
- onNotice?: (notice: CompileNotice) => void,
1416
- ): void {
1417
- const 部品の名前 = new Set(Object.keys(partsCatalog).map((k) => k.replace(/^parts-/, "")));
1418
- const message = `"${truncateForMessage(actor.name)}" の種類 "${truncateForMessage(partId)}" は、箱の種類にも部品の一覧にもありません (種類を書かなかった箱として描きます)`;
1419
- if (typeof console !== "undefined" && console.warn) console.warn(`[dragon] ${message}`);
1420
- if (部品の名前.size === 0) return;
1421
- // 書いた側の `parts-` も外して比べる = 前置きの 6 文字が距離に入ると、どの部品も遠くなる
1422
- const 近い = 近い名前(partId.replace(/^parts-/, ""), [...NODE_KIND_VALID, ...部品の名前]);
1423
- onNotice?.({
1424
- kind: "part-not-found",
1425
- actor: actor.name,
1426
- line: actor.pos?.line ?? 0,
1427
- message,
1428
- hint:
1429
- 近い === undefined
1430
- ? "kind には、箱の種類か、部品の一覧にある部品の名前を書いてください"
1431
- : `近い名前は ${近い} (${NODE_KIND_VALID.has(近い) ? "箱の種類" : "部品"}) です`,
1432
- });
1433
- }
1434
-
1435
- /**
1436
- * CAR-1657 = doc.actors 中の partId set actor を検出、 partsCatalog から CdlDiagram を lookup、
1437
- * mergePartIntoDiagram で target に prefix 付き統合する。 diagram render は継続 (壊さない設計)。
1438
- *
1439
- * 部品の一覧を渡さない時は `console.warn` だけに出し、知らせない (#2113)。 一覧が無いと、書いた名前が
1440
- * 部品なのか箱の種類の書き間違いなのかを決められない。 一覧を渡して名前が無かった時は
1441
- * `部品が一覧に無いことを伝える` が決める (部品を持つ一覧なら書いた行で知らせる)。
1442
- */
1443
- export function mergePartsFromActors(
1444
- target: CdlDiagram,
1445
- doc: DslDocument,
1446
- partsCatalog?: Record<string, CdlDiagram>,
1447
- onNotice?: (notice: CompileNotice) => void,
1448
- derivedSourceLines?: Map<string, number[]>,
1449
- /** 矢印の id から、その矢印を書いた行。 行に書いた要素の名指しを読む (#1979) */
1450
- edgeSteps?: Map<string, DslStep>,
1451
- ): CdlDiagram {
1452
- const partsActors = doc.actors.filter((a) => a.partId !== undefined);
1453
- if (partsActors.length === 0) return target;
1454
- // 値と状態の名前に使う前置きを **書いた順に 1 度だけ** 決める (#1189)。 見本ごとに作ると
1455
- // 同じ形に潰れた時の番号が揃わず、後から重ねた見本が先の名前空間を踏む
1456
- const 値の前置き = 値の前置きを作る(partsActors.map((a) => a.name));
1457
- if (!partsCatalog) {
1458
- if (typeof console !== "undefined" && console.warn) {
1459
- const names = partsActors.map((a) => `${a.name} (kind: ${a.partId ?? "?"})`).join(", ");
1460
- console.warn(
1461
- `[dragon] 部品の一覧 (partsCatalog) を渡していないため、次の箱を部品として描けません (種類を書かなかった箱として描きます): ${names}`,
1462
- );
1463
- }
1464
- return target;
1465
- }
1466
- // 位置を書かなかったパーツの置き場所は `partGridCenters` が決める。
1467
- //
1468
- // 以前はここで格子を組んでいたが、 相対指定を解く側も同じ位置を知る必要がある。
1469
- // 別々に計算すると、 解決側が想定した位置と実際の置き場所がずれる。 規則を共有する。
1470
- // 取り込んでよい合計を先に決める (#1015)。 1 件ずつ上限以下でも、同じ見本を別名で何度も
1471
- // 参照すれば合計は上限を超える (実測 = 1,001 要素の見本を 3 名で参照して 3,005 要素になった)。
1472
- // 本体の分を引いた残りを予算として、順に配って超えた分を落とす。
1473
- //
1474
- // 格子より先に決める。 後にすると、落とす見本が格子の枠を消費して後続がずれる
1475
- const budget = partsBudget(target, partsActors, partsCatalog);
1476
- const gridCenters = partGridCenters(target, doc, partsCatalog, budget, partsActors);
1477
- // 縦列の中に置く部品 (#1980)。 仮の箱を消す前に探す = 消した後は自分だけの縦列を見分けられない
1478
- const { 縦列: 縦列の部品, 基準のため外した } = 縦列に置く部品(target, doc);
1479
- const 縦列に置いた: 縦列に置いた部品[] = [];
1480
- const 自分の縦列に置いた: 自分の縦列に置いた部品[] = [];
1481
- /** 図に取り込んだ部品。 段の `focus` に書いた部品の名前を要素へ広げる時に引く (#2150) */
1482
- const 取り込んだ部品 = new Map<string, CdlDiagram>();
1483
-
1484
- for (const [actorIndex, actor] of partsActors.entries()) {
1485
- const partId = actor.partId;
1486
- // 部品の一覧は外から渡される = 自分が持つ key だけを見る (`Object.hasOwn`)。
1487
- // 見ないと `__proto__` のような受け継いだ名前が部品として当たり、渡していない中身が図に入る。
1488
- // `parts-` で始まる名前の経路も同じ確認を通す。
1489
- if (typeof partId !== "string" || partId.length === 0) continue;
1490
- const found = lookupPartRaw(partsCatalog, partId);
1491
- // 見つかっても大きすぎる図は取り込まない (#1015)。 黙って落とすと「書いたのに出ない」 に
1492
- // なるため、見つからなかった時と分けて知らせる。
1493
- // 1 件では収まっても合計で超える分も同じく落とす
1494
- if (found !== undefined && !budget.has(actorIndex)) {
1495
- const overOne = !partIsMeasurable(found);
1496
- onNotice?.({
1497
- kind: "part-not-drawn",
1498
- actor: actor.name,
1499
- line: 0,
1500
- message: `"${actor.name}" (${partId}) は大きすぎるため取り込みません。`,
1501
- hint: overOne
1502
- ? `要素数が上限 (${MAX_INPUT_ELEMENTS}) を超えています`
1503
- : `図全体の要素数が上限 (${MAX_INPUT_ELEMENTS}) を超えます`,
1504
- });
1505
- // 落とす時も仮の箱を掃除する。 残すと格子から外した後続の見本と重なる。
1506
- // 部品が図に入らないので、部品へ引いた矢印も繋ぎ先が無い = 矢印ごとに知らせて外す
1507
- cleanupPlaceholderActor(
1508
- target,
1509
- doc,
1510
- actor,
1511
- 部品の要素へ繋ぐ(actor.name, partId, undefined, edgeSteps, onNotice),
1512
- );
1513
- continue;
1514
- }
1515
- const part = found;
1516
- if (!part) {
1517
- 部品が一覧に無いことを伝える(actor, partId, partsCatalog, onNotice);
1518
- // 一覧に無い部品は仮の箱のまま描かれ、矢印も仮の箱に繋がる。 要素の名指しは効かない
1519
- for (const s of doc.flow) {
1520
- for (const [側, 欄] of [
1521
- ["from", "fromPartNode"],
1522
- ["to", "toPartNode"],
1523
- ] as const) {
1524
- const 値 = s[欄];
1525
- if (値 === undefined || s[側] !== actor.name) continue;
1526
- // 順序図は部品の有無に依らず名指しが効かない。 そちらの知らせと 2 度出さない
1527
- if (doc.type === "sequence") continue;
1528
- onNotice?.({
1529
- kind: "part-node-ignored",
1530
- actor: actor.name,
1531
- line: s.pos.line,
1532
- message: `"${truncateForMessage(actor.name)}" (${truncateForMessage(partId)}) は部品の一覧に無いため、${欄} に書いた "${truncateForMessage(値)}" は効きません`,
1533
- hint: "kind に部品の名前を書く (綴りを確かめる)",
1534
- });
1535
- }
1536
- }
1537
- continue;
1538
- }
1539
- // 型 (`sequence` 等) が作った、部品の登場人物に由来する箱と線を別名も辿って全て消す。
1540
- // `sequence` は `{slug}-header` / `-spacer` / `-footer` / `s{N}-{slug}` を作るので、
1541
- // slug で始まる名前をまとめて対象にする。
1542
- //
1543
- // sweep に使う slug は 2 系統ある (#873)。 dragon の slugify は `_` / 全角を保持するが、 非 animate
1544
- // sequence の node は cdl preset 側の slugify (`_` → `-` 置換、 NFKC なし) で生成される
1545
- // ため、 dragon slug だけで sweep すると `arc_one` → 実 id `arc-one-header` を取りこぼし、 header /
1546
- // footer (title = actor 名) が残って actor 名が多重表示される。
1547
- //
1548
- // seq-like preset は「actor 専用 lane に属する node」 を exact set で特定する経路を使う。
1549
- // lane.label === actor.name で lane を引き当て (label は両 slug 経路とも actor.name 生値)、 その
1550
- // lane に属する node (header / spacer / footer / step anchor は全て actor lane 所属) を node.lane で
1551
- // 厳密収集する。 slug の prefix 推測を挟まないため、 slug 実装差の取りこぼしと、 別 actor を巻き込む
1552
- // 誤削除 (parts actor `a_b` の lane id `a-b` が actor `a-b-c` の `a-b-c-header` に prefix match する)
1553
- // の両方を同時に排除する。
1554
- //
1555
- // 仮の箱に繋がっていた矢印は、部品の図の要素へ繋ぎ直す (#1979)。
1556
- // 縦列に置く部品は、仮の箱の縦列を消さずに残す (#1980)。 部品用の縦列 (`名前__l`) を足すと、
1557
- // 縦列が書いた順ではなく仮に置いた横位置の順に並ぶ (実測 = 3 人目に書いた部品が真ん中になった)
1558
- const 置く縦列 = 縦列の部品.get(actor.name);
1559
- cleanupPlaceholderActor(
1560
- target,
1561
- doc,
1562
- 置く縦列 !== undefined ? { name: actor.name, lane: 置く縦列 } : actor,
1563
- 部品の要素へ繋ぐ(actor.name, partId, part, edgeSteps, onNotice),
1564
- );
1565
- const 外した縦列 = 基準のため外した.get(actor.name);
1566
- if (外した縦列 !== undefined) {
1567
- onNotice?.({
1568
- kind: "part-lane-ignored",
1569
- actor: actor.name,
1570
- line: actor.pos?.line ?? 0,
1571
- message: `"${actor.name}" は他の箱の位置の基準になっているため、縦列 "${target.lanes.find((l) => l.id === 外した縦列)?.label ?? 外した縦列}" には置かず図の下に並べました`,
1572
- hint: `縦列に置きたい時は、"${actor.name}" を基準にした位置 (位置: ${actor.name} の右 200 など) を座標で書く`,
1573
- });
1574
- }
1575
- const merged = applyColorHex(part, actor.colorHex, actor.stateOverride ?? {});
1576
- // 色番号は部品の色の状態へ入れる。 塗りを図形に直接書いた部品 (`arc-gauge` 等) は入れる先が
1577
- // 無く、書いても絵が変わらない。 黙って既定の色で描くと手掛かりが残らないので知らせる (#1973)
1578
- if (actor.colorHex && !(part.states ?? []).some((st) => isColorValue(st.initial))) {
1579
- onNotice?.({
1580
- kind: "part-color-ignored",
1581
- actor: actor.name,
1582
- line: actor.pos?.line ?? 0,
1583
- message: `"${actor.name}" (${partId}) は色を変えられる状態を持たないため、色番号 ${actor.colorHex} は効きません`,
1584
- hint: "色を変えられる部品は、初期値が色番号の状態を持つもの (例 = state-indicator)",
1585
- });
1586
- }
1587
- // 色の名前は箱の色にしか効かず、部品の色の状態は色番号しか受けない。 名前の色は描く側の
1588
- // 配色の変数 (`--cdl-tone-*`) で決まり固定の色番号を持たないため、置き換えて入れることもしない
1589
- if (actor.partColorName) {
1590
- onNotice?.({
1591
- kind: "part-color-ignored",
1592
- actor: actor.name,
1593
- line: actor.pos?.line ?? 0,
1594
- // 残っている名前は読み替えた後の正規の名前 (`成功` なら `success`) なので文には出さない
1595
- message: `"${actor.name}" (${partId}) の色は色の名前では変わりません`,
1596
- hint: '部品の色は色番号で書く (例 = color: "#d9534f")',
1597
- });
1598
- }
1599
- // 上書きが読むのは部品の状態の名前と `phase` (段を外す) だけで、他の名前は何も変えない。
1600
- // 綴り違いや、外した欄 (`nodes`、#1976) を書いた時に、効いていない値を黙って持たない
1601
- const 部品の状態 = new Set((part.states ?? []).map((st) => st.id));
1602
- const 無い状態 = Object.keys(actor.stateOverride ?? {}).filter(
1603
- (k) => k !== "phase" && !部品の状態.has(k),
1604
- );
1605
- if (無い状態.length > 0) {
1606
- onNotice?.({
1607
- kind: "part-state-missing",
1608
- actor: actor.name,
1609
- line: actor.pos?.line ?? 0,
1610
- message: `"${actor.name}" (${partId}) は ${無い状態.map((k) => `"${truncateForMessage(k)}"`).join(" / ")} という状態を持たないため、書いた値は効きません`,
1611
- hint:
1612
- 部品の状態.size > 0
1613
- ? `この部品の状態 = ${[...部品の状態].join(", ")}`
1614
- : "この部品は状態を持たないため、値を書いても変わりません",
1615
- });
1616
- }
1617
- // 位置を書いていないパーツは格子に並べる。 書いてあればその位置を使う
1618
- let placeX = actor.posX;
1619
- let placeY = actor.posY;
1620
- // 格子に落とすのは縦横どちらも書かなかった時だけ。 片方だけ書いた時に残りを格子で
1621
- // 埋めると、 書いた値と格子が混ざった位置になる (従来の条件をそのまま保つ)
1622
- if (置く縦列 !== undefined) {
1623
- // 縦列の位置は図を配置するまで決まらない。 ここでは座標で置く形にだけして、
1624
- // 全部の部品を組み込んだ後に縦列へ寄せる (`縦列に置いた部品を揃える`)
1625
- placeX = 0;
1626
- placeY = 0;
1627
- } else if (placeX === undefined && placeY === undefined) {
1628
- const center = gridCenters.get(actor.name);
1629
- placeX = center?.cx;
1630
- placeY = center?.cy;
1631
- }
1632
- // `倍率` / `scale` は図形の倍率として予約した (#1026)。 同じ名前の状態を持つ見本では、
1633
- // 予約する前は状態の上書きとして効いていた。 黙って意味が変わると気付けないので知らせる
1634
- // 判定は **書かれた名前** で行う。 読めた値で判定すると `scale: x` のように値が
1635
- // 読めない形で知らせが消え、逆に `scale` を書いて見本が `倍率` の状態を持つだけの
1636
- // 組合せ (元から衝突していない) にも知らせてしまう
1637
- const written = new Set(actor.scaleKeys ?? []);
1638
- if (written.size > 0) {
1639
- const clashed = (part.states ?? []).find((st) => written.has(String(st.id ?? "")));
1640
- if (clashed) {
1641
- onNotice?.({
1642
- kind: "scale-reserved",
1643
- actor: actor.name,
1644
- line: actor.pos?.line ?? 0,
1645
- message: `"${clashed.id}" は見本の大きさを変える項目として扱いました (${clashed.id} という名前の状態は変えていません)`,
1646
- hint: `状態を変えたい時は \`state: { ${clashed.id}: ... }\` と書く`,
1647
- });
1648
- }
1649
- }
1650
- const t = partTargetSize(part, actor.posW, actor.posH, actor.scale);
1651
- const 組み込む前の箱の数 = target.nodes.length;
1652
- const 作った縦列 = mergePartIntoDiagram(
1653
- target,
1654
- part,
1655
- actor.name,
1656
- merged,
1657
- 置く縦列 ?? actor.lane,
1658
- placeX,
1659
- placeY,
1660
- t.w,
1661
- t.h,
1662
- onNotice,
1663
- actor.pos?.line ?? 0,
1664
- derivedSourceLines,
1665
- 値の前置き.get(actor.name),
1666
- 置く縦列 !== undefined,
1667
- );
1668
- const 要素 = new Set(target.nodes.slice(組み込む前の箱の数).map((n) => n.id));
1669
- 取り込んだ部品.set(actor.name, part);
1670
- if (置く縦列 !== undefined) {
1671
- 縦列に置いた.push({ 縦列: 置く縦列, 要素 });
1672
- } else if (作った縦列.length > 0) {
1673
- 自分の縦列に置いた.push({ 名前: actor.name, 縦列: 作った縦列, 要素 });
1674
- }
1675
- }
1676
- // 部品用の縦列を先に固定する。 縦列に置く部品は配置した縦列の位置に合わせるので、固定する前に
1677
- // 揃えると、揃えた時の縦列 (部品用の縦列も詰める送りに加わる) と描いた図の縦列が食い違う
1678
- 部品の縦列を部品に固定する(target, 自分の縦列に置いた);
1679
- 縦列に置いた部品を揃える(target, 縦列に置いた);
1680
- 部品の名前で光らせる(target, doc, 取り込んだ部品, onNotice);
1681
- return target;
1682
- }
1683
-
1684
- /**
1685
- * 段の `focus` に書いた部品の名前を、部品の要素と部品の中の線に広げる (#2150)。
1686
- *
1687
- * 組み立ては部品の名前を仮の箱として置き、段の `focus` を仮の箱の id に解決する。 部品を取り込む時に
1688
- * 仮の箱を消し、段の光らせる相手からも外す (`cleanupPlaceholderActor`)。 外すだけでは、部品の名前を
1689
- * 書いた段が何も光らせない (実測 = 部品を繋いで動かす頁の 5 見本で、部品の名前を書いた 13 段が
1690
- * 1 つも部品を光らせていなかった)。
1691
- *
1692
- * **仮の箱を消す所で置き換えず、書いた `focus` を読み直す**。 仮の箱の id の形は図種で違い
1693
- * (順序図は縦列の上端 / 下端 / 手順箱、フローは箱 1 つ)、置き換えると図種ごとに規則が要る。
1694
- * 書いた名前から引けば、図種に依らず同じ規則で足せる。
1695
- *
1696
- * | 書いた相手 | 足すもの |
1697
- * |---|---|
1698
- * | 部品の名前 (か、他の名前と重ならない slug の形) | 部品の要素 (`{部品の名前}__{要素}`) と部品の中の線 (`{部品の名前}__{線}`) |
1699
- * | `{部品の名前}__{要素}` / `{部品の名前}__{中の線}` (#2151) | その要素か線だけ。 部品に無ければ知らせる |
1700
- * | それ以外 (箱の名前 / 矢印) | 足さない。 図種ごとの解決が既に光らせている |
1701
- *
1702
- * 要素の名指しの前半は、取り込んだ部品の名前のうち **一番長く一致するもの** で決める。 部品の名前
1703
- * そのものが `__` を含む時 (`印` と `印__2`)、短い名前に当てると `印__2__inP` を `印` の要素 `2__inP` と
1704
- * 読み、無い要素として知らせてしまう。 登場人物の名前と完全に一致する名前は、箱の名前として読む。
1705
- *
1706
- * 部品に無い名前はここで知らせる。 組み立ての入口 (`reportMissingFocusTargets`) は部品の一覧を持たず、
1707
- * 前半が部品の名前なら中身を確かめずに通す = 知らせる所をここ 1 か所にし、同じ名前に 2 件出さない。
1708
- *
1709
- * 書いた段と組み立てた段は題 (`title` は書いた段の名前) で先頭から順に突き合わせる。 部品が宿主より
1710
- * 多くの段を持つと組み立てた段が後ろに増えるが、書いた段は同じ順に並ぶ。 同じ id は 2 度入れない。
1711
- */
1712
- function 部品の名前で光らせる(
1713
- target: CdlDiagram,
1714
- doc: DslDocument,
1715
- 取り込んだ: ReadonlyMap<string, CdlDiagram>,
1716
- onNotice?: (notice: CompileNotice) => void,
1717
- ): void {
1718
- const 書いた段 = doc.animate?.phases ?? [];
1719
- if (書いた段.length === 0 || 取り込んだ.size === 0) return;
1720
- const 箱 = new Set(target.nodes.map((n) => n.id));
1721
- const 線 = new Set(target.edges.map((e) => e.id));
1722
- const 部品の相手 = new Map<string, string[]>();
1723
- /** 部品ごとに、図に入った要素と中の線の元の id。 名指しの照合と、無い名前を知らせる時の案内に使う */
1724
- const 部品の中 = new Map<string, { 要素: string[]; 線: string[] }>();
1725
- for (const [名前, part] of 取り込んだ) {
1726
- const 中 = {
1727
- 要素: part.nodes.map((n) => n.id).filter((id) => 箱.has(`${名前}__${id}`)),
1728
- 線: part.edges.map((e) => e.id).filter((id) => 線.has(`${名前}__${id}`)),
1729
- };
1730
- 部品の中.set(名前, 中);
1731
- 部品の相手.set(
1732
- 名前,
1733
- [...中.要素, ...中.線].map((id) => `${名前}__${id}`),
1734
- );
1735
- }
1736
- const 長い名前から = [...取り込んだ.keys()].sort((a, b) => b.length - a.length);
1737
- const 見せる数 = 8;
1738
- const 並べる = (ids: readonly string[]): string =>
1739
- ids.length === 0
1740
- ? "なし"
1741
- : `${ids.slice(0, 見せる数).map(truncateForMessage).join(", ")}${ids.length > 見せる数 ? ` ほか ${ids.length - 見せる数} 件` : ""}`;
1742
- // 図種ごとの解決は、名前が見つからない時に slug の形でも探す (`slugLookup`)。 同じ書き方で
1743
- // 部品だけが光らない状態を作らないよう揃える。 2 つ以上の名前が同じ slug になる時は引かない
1744
- const 書いた名前 = new Set(doc.actors.map((a) => a.name));
1745
- const slugの数 = new Map<string, number>();
1746
- for (const 名前 of 書いた名前)
1747
- slugの数.set(slugify(名前), (slugの数.get(slugify(名前)) ?? 0) + 1);
1748
- const slugから = new Map<string, string[]>();
1749
- for (const [名前, 相手] of 部品の相手) {
1750
- if (slugの数.get(slugify(名前)) === 1) slugから.set(slugify(名前), 相手);
1751
- }
1752
- let 次に見る = 0;
1753
- for (const 段 of 書いた段) {
1754
- // 光らせる相手を先に決める。 段の突き合わせに外れても、部品に無い名前は知らせる
1755
- const 足す = (段.highlight ?? []).flatMap((書いた) => {
1756
- const entry = parseFocusEntry(書いた, 書いた名前);
1757
- if (entry.kind !== "node") return [];
1758
- const 部品全体 = 部品の相手.get(entry.name);
1759
- if (部品全体 !== undefined) return 部品全体;
1760
- if (書いた名前.has(entry.name)) return [];
1761
- const slugで引いた = slugから.get(entry.name);
1762
- if (slugで引いた !== undefined) return slugで引いた;
1763
- const 名前 = 長い名前から.find((p) => entry.name.startsWith(`${p}__`));
1764
- if (名前 === undefined) return [];
1765
- const 中 = 部品の中.get(名前)!;
1766
- const 名指し = entry.name.slice(名前.length + 2);
1767
- if (中.要素.includes(名指し) || 中.線.includes(名指し)) return [entry.name];
1768
- onNotice?.({
1769
- kind: "focus-target-missing",
1770
- actor: 書いた,
1771
- line: 段.pos.line,
1772
- message: `光らせる相手が部品 "${truncateForMessage(名前)}" の中にありません: "${truncateForMessage(書いた)}"`,
1773
- hint: `この部品の要素 = ${並べる(中.要素)} / 中の線 = ${並べる(中.線)}`,
1774
- });
1775
- return [];
1776
- });
1777
- const 番目 = target.phases.findIndex((p, i) => i >= 次に見る && p.title === 段.name);
1778
- if (番目 < 0) continue;
1779
- 次に見る = 番目 + 1;
1780
- if (足す.length === 0) continue;
1781
- const 組み立てた段 = target.phases[番目]!;
1782
- 組み立てた段.activate = [...new Set([...組み立てた段.activate, ...足す])];
1783
- }
1784
- }
1785
-
1786
- type 自分の縦列に置いた部品 = {
1787
- /** 登場人物の名前。 名札に使う */
1788
- 名前: string;
1789
- /** 部品用に作った縦列。 部品の縦列の順 */
1790
- 縦列: readonly 部品用の縦列[];
1791
- 要素: ReadonlySet<string>;
1792
- };
1793
-
1794
- type 部品用の縦列 = {
1795
- id: string;
1796
- /** 部品自身が縦列に書いた名札 */
1797
- 部品の名札: string | undefined;
1798
- };
1799
-
1800
- /** 部品用の縦列の、部品の要素の上端から縦列の上端までの高さ。 名札 (縦列の上端から 26 下) を収める */
1801
- const 部品の縦列の名札の高さ = 60;
1802
-
1803
- /**
1804
- * 部品用の縦列を部品の位置に固定し、登場人物の名前の名札を部品のすぐ上に 1 つだけ出す (#1990)。
1805
- *
1806
- * 部品の要素は座標で置くが、縦列は描画側が左から詰めて並べ直す。 格子の 2 段目の部品の縦列は
1807
- * 1 段目の縦列と重なるため右へ送られ、名札が別の部品の上に出て、送った分だけ図が横に伸びた
1808
- * (実測 = 部品 4 つのフローで `稼働2` の名札が `稼働3` の上、図の右に 875 の空き)。 部品が 1 つでも
1809
- * 名札は図の上端に出て、フローの途中に置くと先頭の箱の見出しに見えた。
1810
- *
1811
- * 描画側は `posX` と `posY` を両方持つ縦列を置いた位置に置き、詰める送りも幅の自動拡張もしない。
1812
- * 幅は `posW` が無ければ縦列の `width` を使うので、横は位置だけを書く。
1813
- *
1814
- * | 向き | 固定する範囲 |
1815
- * |---|---|
1816
- * | 横 | 組み込みで決めた位置と幅のまま (部品が書いた縦列を、置いた位置へ写したもの。 #879 / #880) |
1817
- * | 縦 | 部品全体の上端より名札の分 (60) 上から、部品全体の下端まで。 部品の縦列は全て同じ |
1818
- *
1819
- * | 縦列 | 名札 |
1820
- * |---|---|
1821
- * | 左端が最も左の縦列 (同じなら部品の縦列の順で先) | 部品が書いた名札、無ければ登場人物の名前 |
1822
- * | 他の縦列 | 部品が書いた名札だけ |
1823
- *
1824
- * 縦を部品全体に揃えるのは、部品の頁の絵と同じく縦列の名札を 1 列に並べるため (部品の頁では
1825
- * 縦列が全て同じ上端から始まる)。 縦列ごとの要素に合わせると、左の縦列の要素が他より低い部品で
1826
- * 名前の名札が下がり、右の縦列の要素と重なる。
1827
- *
1828
- * **下に余白を足さない**。 縦列の枠は塗りも線も持たず絵に出ないが、図の大きさには数えられる。
1829
- * 足すと、部品が一番下にある図だけ図の下の空きが広がる (実測 = 箱だけの図と直す前の図は 60、
1830
- * 下に 25 足すと 85)。 高さを書かないと縦列は名札の分 (40) に縮み、部品の要素が縦列の外に出る。
1831
- *
1832
- * **要素の高さは配置しないと分からない** (種類ごとの既定値で描画側が決める)。 全ての部品を
1833
- * 組み込んだ後に 1 度だけ配置して測る。 座標で置いた要素は配置で動かないので、測った範囲に
1834
- * 固定しても要素は動かない。
1835
- *
1836
- * 座標で置かない要素を持つ部品は固定しない。 そういう要素は縦列の中心に描かれるため、縦列の
1837
- * 詰める送りを止めると要素の位置も変わる。 **この分岐はテストで覆えていない** = 本文から入力を
1838
- * 作れない。 格子の部品は必ず格子の中心を持ち、位置を書いた部品は縦横の両方を持つ (片方だけの
1839
- * 位置と基準が見つからない位置は読む段階で止まる)。 どちらも座標で置かれる (`shouldForcePos`)。
1840
- *
1841
- * 配置できない図 (描画側が止める図) では固定しない。 描画でも同じ所で止まる。
1842
- */
1843
- function 部品の縦列を部品に固定する(
1844
- target: CdlDiagram,
1845
- 部品たち: readonly 自分の縦列に置いた部品[],
1846
- ): void {
1847
- const 座標で置いた = (id: string): boolean => {
1848
- const n = target.nodes.find((x) => x.id === id);
1849
- return n !== undefined && n.posX !== undefined && n.posY !== undefined;
1850
- };
1851
- const 対象 = 部品たち.filter((p) => [...p.要素].every(座標で置いた));
1852
- if (対象.length === 0) return;
1853
- let laid: LaidDiagram;
1854
- try {
1855
- // 部品の要素の上下端を測るだけで、この配置は残らない (#2312)
1856
- laid = 測るために配置する(target);
1857
- } catch {
1858
- return;
1859
- }
1860
- for (const p of 対象) {
1861
- const 箱 = laid.nodes.filter((n) => p.要素.has(n.id));
1862
- if (箱.length === 0) continue;
1863
- const 上端 = Math.min(...箱.map((n) => n.cy - n.h / 2));
1864
- const 下端 = Math.max(...箱.map((n) => n.cy + n.h / 2));
1865
- const 縦列たち = p.縦列.flatMap((l) => {
1866
- const 縦列 = target.lanes.find((x) => x.id === l.id);
1867
- return 縦列 ? [{ ...l, 縦列 }] : [];
1868
- });
1869
- let 名札の縦列: (typeof 縦列たち)[number] | undefined;
1870
- for (const l of 縦列たち) {
1871
- if (名札の縦列 === undefined || (l.縦列.x ?? 0) < (名札の縦列.縦列.x ?? 0)) 名札の縦列 = l;
1872
- }
1873
- for (const l of 縦列たち) {
1874
- l.縦列.posX = l.縦列.x ?? 0;
1875
- l.縦列.posY = 上端 - 部品の縦列の名札の高さ;
1876
- l.縦列.posH = 下端 - 上端 + 部品の縦列の名札の高さ;
1877
- const 名札 = l === 名札の縦列 ? (l.部品の名札 ?? p.名前) : l.部品の名札;
1878
- if (名札 === undefined) delete l.縦列.label;
1879
- else l.縦列.label = 名札;
1880
- }
1881
- }
1882
- }
1883
-
1884
- /**
1885
- * 縦列に置く部品の 2 本目以降の縦列を、図の縦列として宿主の縦列のすぐ右に差し込む (#2145)。
1886
- *
1887
- * 以前は部品の縦列を全て宿主の縦列 1 本にまとめていた。 部品の要素は座標で置くので横並びは
1888
- * 保たれるが、同じ縦列の中に中心の違う箱が並び、図の検査が揃いの誤りを出していた
1889
- * (実測 = `split-router` を `swimlane` に置いて 5 件、`split-router` から `queue-depth` へ繋いで 9 件)。
1890
- *
1891
- * | 部品の縦列 | 要素を入れる縦列 |
1892
- * |---|---|
1893
- * | 1 本目 (頁で一番左) | 宿主の縦列。 画面側が「部品用の縦列の外にある要素」 で縦列に置いた部品を見分けるため残す |
1894
- * | 2 本目以降 | 宿主の縦列の右に、部品の縦列の順で差し込んだ縦列 |
1895
- *
1896
- * **差し込む縦列は座標 (`posX` / `posY`) で固定しない普通の縦列にする**。 座標で固定した縦列は、
1897
- * 描画側が並べる処理と矢印の札のために縦列の間を広げる処理の両方から外れる。 試作では部品どうしを
1898
- * 繋ぐ矢印の札が宿主の縦列の境を貫いた (`lane-border-clearance`)。 普通の縦列なら並びも札の間も
1899
- * 描画側が決め、要素を縦列の中心へ寄せるのは `縦列に置いた部品を揃える` が縦列ごとに行う。
1900
- * 並びの鍵になる横位置 (`x`) だけは、宿主が横位置を持つ時に `差し込む縦列の横位置` が決める (#2147)。
1901
- *
1902
- * **名札を付けない**。 宿主の縦列の名札 (登場人物の名前) が部品全体の見出しになり、2 本目以降に
1903
- * 部品の縦列の名札 (`出口` 等) を出すと、図の縦列の見出しの行に登場人物と部品の中の名前が混ざる。
1904
- *
1905
- * 同じ縦列に置いた部品どうしは、同じ番目の縦列を共有する (幅は広い方)。 要素は縦列の中心に
1906
- * 揃うので、積んだ部品の 2 本目どうしも同じ中心に並ぶ。
1907
- *
1908
- * 返り値は、部品の縦列の id から要素を入れる図の縦列の id。 1 本目と要素を持たない縦列は入れない。
1909
- * 要素を持つ縦列が 1 本以下の部品は何も差し込まず `undefined` を返す。
1910
- */
1911
- function 部品の縦列を図に差し込む(
1912
- target: CdlDiagram,
1913
- part: CdlDiagram,
1914
- 宿主: string,
1915
- 置き方: 部品の置き方,
1916
- 倍率: number,
1917
- ): Map<string, string> | undefined {
1918
- const 要素を持つ = new Set(part.nodes.map((n) => n.lane));
1919
- const 列 = part.lanes
1920
- .map((l, 書いた順) => ({ l, 書いた順, x: 置き方.縦列.get(l.id)?.x ?? l.x ?? 0 }))
1921
- .filter((c) => 要素を持つ.has(c.l.id))
1922
- .sort((p, q) => p.x - q.x || p.書いた順 - q.書いた順);
1923
- if (列.length < 2) return undefined;
1924
- const 行き先 = new Map<string, string>();
1925
- let 前 = 宿主;
1926
- for (const [番目, c] of 列.entries()) {
1927
- if (番目 === 0) continue;
1928
- const id = `${宿主}__列${番目 + 1}`;
1929
- const 幅 = (置き方.縦列.get(c.l.id)?.w ?? c.l.width) * 倍率;
1930
- const 既に = target.lanes.find((l) => l.id === id);
1931
- if (既に) {
1932
- 既に.width = Math.max(既に.width, 幅);
1933
- } else {
1934
- const 前の縦列 = target.lanes.find((l) => l.id === 前);
1935
- const x = 前の縦列 === undefined ? undefined : 差し込む縦列の横位置(target, 前の縦列);
1936
- const 前の位置 = 前の縦列 === undefined ? -1 : target.lanes.indexOf(前の縦列);
1937
- target.lanes.splice(前の位置 < 0 ? target.lanes.length : 前の位置 + 1, 0, {
1938
- id,
1939
- width: 幅,
1940
- ...(x === undefined ? {} : { x }),
1941
- });
1942
- }
1943
- 行き先.set(c.l.id, id);
1944
- 前 = id;
1945
- }
1946
- return 行き先;
1947
- }
1948
-
1949
- /** 描画側 (`layoutLanes`) が縦列の間に空ける既定の幅。 描画側の `DEFAULT_LANE_GAP` と同じ (外へ出していない) */
1950
- const 描画側の縦列の間 = 80;
1951
-
1952
- /**
1953
- * 差し込む縦列の横位置 (#2147)。 `undefined` は横位置を持たせないこと。
1954
- *
1955
- * 描画側は縦列を横位置の小さい順に並べ、同じ横位置の縦列は書いた順に並べる。 横位置を持たない
1956
- * 縦列は、書いた順で 1 つ前の縦列の右端から間を空けた位置になる。 差し込む縦列は書いた順で `前` の
1957
- * すぐ後ろに入るので、`前` が横位置を持たなければ、横位置を持たないまま `前` のすぐ右に並ぶ。
1958
- *
1959
- * `前` が横位置を持つ時 (`lanes:` に `x` を書いた縦列) に横位置を持たせないと、見積もった位置が
1960
- * 他の縦列の横位置を越え、その縦列より右に回る (実測 = `x: 0` を書いた `左` / `右` に分岐を置くと、
1961
- * `左__列2` が `右` の右に描かれた)。
1962
- *
1963
- * | 描く順で `前` の次にある縦列 | 横位置 |
1964
- * |---|---|
1965
- * | 無い | 持たない = `前` の右端から並ぶ |
1966
- * | 横位置を持たず、書いた順で `前` より後ろ | 持たない = 差し込むと、その縦列ごと右へずれる |
1967
- * | 上記以外 | `前` と次の縦列の真ん中 (同じ横位置なら `前` と同じ) |
1968
- *
1969
- * **真ん中にする**。 描画側は縦列の中心の間隔を全ての組で一番広い組に揃える。 `前` と同じ横位置に
1970
- * すると、差し込んだ縦列と次の縦列の間がそのまま全体の間隔になり、`x: 0` と `x: 900` を書いた図で
1971
- * 右の縦列の左端が 1,900 まで押し出された (真ん中なら 980)。 縦列の幅にも依らないので、後で
1972
- * `縦列に置いた部品を揃える` が縦列を広げても並びは変わらない。
1973
- *
1974
- * 座標 (`posX` / `posY`) で固定した縦列は描画側の並べる処理から外れるので、並びの相手にしない。
1975
- */
1976
- function 差し込む縦列の横位置(
1977
- target: CdlDiagram,
1978
- 前: CdlDiagram["lanes"][number],
1979
- ): number | undefined {
1980
- if (前.x === undefined) return undefined;
1981
- const 間 = target.viewport?.laneGap ?? target.viewport?.gap ?? 描画側の縦列の間;
1982
- const 前の順 = target.lanes.indexOf(前);
1983
- let 次: { x: number; 後ろで動く: boolean } | undefined;
1984
- // 横位置を持たない縦列の位置は、描画側と同じく書いた順に積んで見積もる
1985
- let 見積もり = 0;
1986
- for (const [順, l] of target.lanes.entries()) {
1987
- if (l.posX !== undefined && l.posY !== undefined) continue;
1988
- const x = l.x ?? 見積もり;
1989
- 見積もり = x + l.width + 間;
1990
- if (l === 前 || x < 前.x || (x === 前.x && 順 < 前の順)) continue;
1991
- // 同じ横位置の縦列は書いた順に並ぶので、先に見つけた方が次になる
1992
- if (次 === undefined || x < 次.x) 次 = { x, 後ろで動く: l.x === undefined && 順 > 前の順 };
1993
- }
1994
- if (次 === undefined || 次.後ろで動く) return undefined;
1995
- return (前.x + 次.x) / 2;
1996
- }
1997
-
1998
- type 縦列に置いた部品 = { 縦列: string; 要素: ReadonlySet<string> };
1999
-
2000
- /** 部品が縦列より広い時に、縦列の左右に残す余白。 描画側が縦列の中の箱に取る最小の余白と同じ */
2001
- const 部品の縦列の余白 = 25;
2002
-
2003
- /**
2004
- * 縦列に置いた部品を、縦列の中心と図の下へ動かす (#1980)。
2005
- *
2006
- * 部品は座標で置く (部品の中の要素の並びを保つため)。 座標で置いた箱は描画側の配置が動かさないので、
2007
- * 縦列の位置を知るには図を 1 度配置する。
2008
- *
2009
- * **配置は 1 度で足りる**。 縦列の横位置は縦列の幅・中の箱の幅・矢印の札で決まり、座標で置いた箱の
2010
- * 座標には依らない。 部品を動かしても縦列は動かない。
2011
- *
2012
- * | 向き | 置き方 |
2013
- * |---|---|
2014
- * | 横 | 要素を入れた縦列ごとに、その要素の中心を縦列の中心に合わせる。 要素が縦列より広ければ縦列を広げ、配置し直す |
2015
- * | 縦 | 部品以外の箱の一番下から `PARTS_GAP` 空ける。 同じ縦列の部品は書いた順に下へ積む。 部品の要素は全て同じだけ動かす |
2016
- *
2017
- * 縦列を 2 本以上持つ部品は、要素が宿主の縦列と差し込んだ縦列に分かれている
2018
- * (`部品の縦列を図に差し込む`、#2145)。 横を部品全体の中心で合わせると、差し込んだ縦列の要素が
2019
- * 自分の縦列の中心からずれるので、縦列ごとに合わせる。
2020
- *
2021
- * **縦列の段には入れない**。 段の高さは縦列をまたいで共有され、背の高い部品を段に入れると隣の
2022
- * 縦列の箱まで伸びる (実測 = `受付` の高さが 68 から 380 になった)。
2023
- *
2024
- * 配置できない図 (描画側が止める図) では動かさない。 描画でも同じ所で止まるので、ここで
2025
- * 別の知らせは足さない。
2026
- */
2027
- function 縦列に置いた部品を揃える(target: CdlDiagram, 置いた: readonly 縦列に置いた部品[]): void {
2028
- if (置いた.length === 0) return;
2029
- // 揃える前の並びを測るだけで、この配置は残らない。 知らせは呼ぶ側で受け取る (#2312)
2030
- const 配置する = (): LaidDiagram | undefined => {
2031
- try {
2032
- return 測るために配置する(target);
2033
- } catch {
2034
- return undefined;
2035
- }
2036
- };
2037
- let laid = 配置する();
2038
- if (!laid) return;
2039
- const 範囲 = (d: LaidDiagram, 要素: ReadonlySet<string>) => {
2040
- const 箱 = d.nodes.filter((n) => 要素.has(n.id));
2041
- if (箱.length === 0) return undefined;
2042
- return {
2043
- x0: Math.min(...箱.map((n) => n.cx - n.w / 2)),
2044
- x1: Math.max(...箱.map((n) => n.cx + n.w / 2)),
2045
- y0: Math.min(...箱.map((n) => n.cy - n.h / 2)),
2046
- y1: Math.max(...箱.map((n) => n.cy + n.h / 2)),
2047
- };
2048
- };
2049
- // 部品の要素を、入れた縦列ごとに分ける。 縦列を 1 本しか持たない部品は宿主の縦列 1 つになる
2050
- const 縦列ごと = (要素: ReadonlySet<string>): Map<string, Set<string>> => {
2051
- const 出力 = new Map<string, Set<string>>();
2052
- for (const n of target.nodes) {
2053
- if (!要素.has(n.id)) continue;
2054
- const 組 = 出力.get(n.lane) ?? new Set<string>();
2055
- 組.add(n.id);
2056
- 出力.set(n.lane, 組);
2057
- }
2058
- return 出力;
2059
- };
2060
- // 要素より狭い縦列を広げる。 描画側は縦列を中の箱 1 つの幅までしか広げず、同じ縦列の中で
2061
- // 要素を横に並べた部品は隣の縦列へはみ出す
2062
- let 広げた = false;
2063
- for (const p of 置いた) {
2064
- for (const [縦列のid, 組] of 縦列ごと(p.要素)) {
2065
- const r = 範囲(laid, 組);
2066
- const 縦列 = target.lanes.find((l) => l.id === 縦列のid);
2067
- const 描いた縦列 = laid.lanes.find((l) => l.id === 縦列のid);
2068
- if (!r || !縦列 || !描いた縦列) continue;
2069
- const 要る幅 = r.x1 - r.x0 + 部品の縦列の余白 * 2;
2070
- if (要る幅 > 描いた縦列.width && 要る幅 > 縦列.width) {
2071
- 縦列.width = 要る幅;
2072
- 広げた = true;
2073
- }
2074
- }
2075
- }
2076
- if (広げた) {
2077
- laid = 配置する();
2078
- if (!laid) return;
2079
- }
2080
- // 動かした後に配置し直して確かめ、狙いと合うまで繰り返す。 座標で置いた箱が他の箱と重なると
2081
- // 描画側は他の箱を下げるため、仮の位置 (図の上端) のまま測った下端は実際より下にずれる
2082
- // (実測 = 狙った上端 316 が 400 になった)。 部品を図の下へ動かせば重ならなくなり、2 回目で合う
2083
- const 部品の要素 = new Set(置いた.flatMap((p) => [...p.要素]));
2084
- for (let 回 = 0; 回 < 部品を揃え直す上限; 回 += 1) {
2085
- // 下端は部品以外の箱 (格子に並べた部品を含む) と縦列の名札で測る。 箱を囲む縦列 (`contain`) は
2086
- // 仮に置いた部品まで囲んでいるため数えない = 中の箱は箱として数えている
2087
- const 他の箱の下端 = Math.max(
2088
- ...laid.nodes.filter((n) => !部品の要素.has(n.id)).map((n) => n.cy + n.h / 2),
2089
- ...laid.lanes.filter((l) => !l.contain).map((l) => l.y + l.height),
2090
- );
2091
- const 次の上端 = new Map<string, number>();
2092
- let 動かした = false;
2093
- for (const p of 置いた) {
2094
- const r = 範囲(laid, p.要素);
2095
- if (!r || !laid.lanes.some((l) => l.id === p.縦列)) continue;
2096
- const 上端 = 次の上端.get(p.縦列) ?? 他の箱の下端 + PARTS_GAP;
2097
- const dy = 上端 - r.y0;
2098
- 次の上端.set(p.縦列, 上端 + (r.y1 - r.y0) + PARTS_GAP);
2099
- for (const [縦列のid, 組] of 縦列ごと(p.要素)) {
2100
- const 組の範囲 = 範囲(laid, 組);
2101
- const 縦列 = laid.lanes.find((l) => l.id === 縦列のid);
2102
- if (!組の範囲 || !縦列) continue;
2103
- const dx = 縦列.x + 縦列.width / 2 - (組の範囲.x0 + 組の範囲.x1) / 2;
2104
- if (Math.abs(dx) <= PLACEMENT_TOLERANCE && Math.abs(dy) <= PLACEMENT_TOLERANCE) continue;
2105
- for (const n of target.nodes) {
2106
- if (!組.has(n.id) || n.posX === undefined || n.posY === undefined) continue;
2107
- n.posX += dx;
2108
- n.posY += dy;
2109
- }
2110
- 動かした = true;
2111
- }
2112
- }
2113
- if (!動かした) return;
2114
- laid = 配置する();
2115
- if (!laid) return;
2116
- }
2117
- }
2118
-
2119
- /** 縦列に置いた部品を配置し直して確かめる回数の上限。 実測では 2 回目で動かなくなる */
2120
- const 部品を揃え直す上限 = 3;
2121
-
2122
- /**
2123
- * 状態の初期値に上書きを当てた結果と、 色として読めないため捨てたかどうか。
2124
- *
2125
- * **元の値が色の状態は、 上書きも色に限る** (#1004)。 状態の値は `fill` に入るため、
2126
- * `url(https://example.invalid/x)` のような外部を指す値を通すと、 図を開いた人の環境から
2127
- * その URL へ要求が飛ぶ。 書き出した SVG を配布しても同じことが起きる。
2128
- * 色として読めない上書きは捨てて元の色を残す = 図は出るが外部は指さない。
2129
- *
2130
- * 元の値が色でない状態 (数値 / 文字列) は制限しない。 色として描かれないため、
2131
- * 一律に弾くとゲージの値や説明文の差し替えという正当な用途を壊す。
2132
- *
2133
- * 捨てたことは呼出側が知らせる。 黙って捨てると、 書いた人は色が変わらない理由
2134
- * (書き間違い / 拒否 / 描画不具合) を区別できない。
2135
- */
2136
- function resolveStateOverride(
2137
- original: number | string,
2138
- override: number | string | boolean | undefined,
2139
- ): { initial: number | string; rejected: boolean } {
2140
- if (override === undefined) return { initial: original, rejected: false };
2141
- if (isColorValue(original) && !isColorValue(override))
2142
- return { initial: original, rejected: true };
2143
- return { initial: override as number | string, rejected: false };
2144
- }
2145
-
2146
- /**
2147
- * `色:` に書かれた色番号を、 パーツが持つ色の状態に入れる。
2148
- *
2149
- * 色を保持する状態の名前はパーツごとに違う (`bg` / `stFill` / `gFill` / `hue` など)。 名前を
2150
- * 決め打ちすると、 別の名前を使うパーツで色を書いても何も起きない。
2151
- *
2152
- * パーツの状態のうち初期値が色番号のものを探して、 そこに入れる。 複数あれば全部に入れる
2153
- * (`cpuC` / `memC` / `netC` のように系統ごとに分かれている場合、 1 つだけ変えるとちぐはぐになる)。
2154
- */
2155
- function applyColorHex(
2156
- part: CdlDiagram,
2157
- colorHex: string | undefined,
2158
- stateOverride: Record<string, number | string | boolean>,
2159
- ): Record<string, number | string | boolean> {
2160
- if (!colorHex) return stateOverride;
2161
- const colorStates = part.states.filter((st) => isColorValue(st.initial));
2162
- if (colorStates.length === 0) return stateOverride;
2163
- const out = { ...stateOverride };
2164
- for (const st of colorStates) {
2165
- // 名前を指定して書いた値が優先。 `色:` はまとめて塗る指定
2166
- if (out[st.id] === undefined) out[st.id] = colorHex;
2167
- }
2168
- return out;
2169
- }
2170
-
2171
- /**
2172
- * 本文に書いた状態の上書きと色番号を、部品の図そのものに当てて返す (#1973)。
2173
- *
2174
- * 編集画面の本文欄は、部品を図から抜いて別に重ねて描く。 重ねる側が部品の図をそのまま
2175
- * 描くと、`state: { lvl: 0.4 }` や `color: "#d9534f"` がカタログの絵にだけ効き、
2176
- * 編集画面では既定の値で描かれる。 値の決め方は組み立て側と同じ 2 つの関数を通す。
2177
- *
2178
- * `phase: false` は組み立て側と同じく部品の段を外す。 段の数は残し、中身 (点灯と値の変化)
2179
- * だけを空にする = 段を消すと、重ねた部品だけ段の進みが止まらない形になる。
2180
- *
2181
- * 色として読めない上書きは組み立て側と同じく捨てる。 知らせは組み立て側が出すため、
2182
- * ここでは出さない (同じ本文で 2 度出さない)。
2183
- */
2184
- export function 部品に上書きを当てる(
2185
- part: CdlDiagram,
2186
- actor: Pick<DslActor, "stateOverride" | "colorHex">,
2187
- ): CdlDiagram {
2188
- const 上書き = applyColorHex(part, actor.colorHex, actor.stateOverride ?? {});
2189
- const 段を外す = 上書き["phase"] === false;
2190
- if (Object.keys(上書き).length === 0) return part;
2191
- return {
2192
- ...part,
2193
- states: part.states.map((st) => ({
2194
- ...st,
2195
- initial: resolveStateOverride(st.initial, 上書き[st.id]).initial,
2196
- })),
2197
- phases: 段を外す
2198
- ? part.phases.map((ph) => ({ ...ph, activate: [], tweens: [], sets: [] }))
2199
- : part.phases,
2200
- };
2201
- }
2202
-
2203
- /**
2204
- * 値と状態の名前に使う前置きを、登場人物の名前から作る (#1189)。
2205
- *
2206
- * `{名前}` に書ける字種は engine が 1 箇所で決めており (`template-name.ts`)、英数字と `_` に
2207
- * 限る。 **読む側 (置き換え) と書ける側 (式) の両方がその定義を使う** ため、記法の側だけ
2208
- * 広げることはできない。
2209
- *
2210
- * この記法では日本語の名前が普通なので、名前をそのまま前置きにすると値が 1 つも届かない。
2211
- * 実測 = `受付 1` に見本を重ねると、状態は表に載るのに箱の `{受付 1__v}` が置き換わらず、
2212
- * 見本の中の式は識別子として読めずに止まる (`value-unresolved`)。
2213
- *
2214
- * **箱 / 縦列 / 矢印の id は変えない**。 これらは `{名前}` の対象ではなく、画面側が id から
2215
- * 登場人物の名前を取り出す経路があるため、変えると別の場所が壊れる。
2216
- *
2217
- * ## 同じ形に潰れる名前
2218
- *
2219
- * `受付 1` と `受付-1` はどちらも英数字だけにすると同じ形になる。 潰れたまま使うと 2 つの
2220
- * 見本が同じ名前空間を共有し、片方の値がもう片方を上書きする。
2221
- *
2222
- * そこで **書いた順に番号を足して分ける**。 先に書いた方が番号なしを取り、後から同じ形に
2223
- * なった方が `_2` / `_3` と続く。 英数字の名前しか無い図では 1 つも番号が付かないため、
2224
- * 既存の図の名前は変わらない。
2225
- */
2226
- function 値の前置きを作る(名前たち: readonly string[]): Map<string, string> {
2227
- const 出力 = new Map<string, string>();
2228
- const 使用中 = new Set<string>();
2229
- const 次の番号 = new Map<string, number>();
2230
- for (const 名前 of 名前たち) {
2231
- if (出力.has(名前)) continue;
2232
- let 素 = 名前
2233
- .normalize("NFKC")
2234
- .replace(/[^A-Za-z0-9_]+/g, "_")
2235
- .replace(/^_+|_+$/g, "");
2236
- // 空になる形 (記号だけの名前) と数字始まりは、そのままでは名前として使えない
2237
- if (素 === "" || /^[0-9]/.test(素)) 素 = `p${素}`;
2238
- // 接尾辞で分けた名前も使用済みとして扱う。 `p1` / `p1!` / `p1_2` の順では、
2239
- // base ごとの回数だけを見ると後ろ 2 つがどちらも `p1_2` になって再衝突する
2240
- let 候補 = 素;
2241
- let 番号 = 次の番号.get(素) ?? 2;
2242
- while (使用中.has(候補)) {
2243
- 候補 = `${素}_${番号}`;
2244
- 番号 += 1;
2245
- }
2246
- 次の番号.set(素, 番号);
2247
- 使用中.add(候補);
2248
- 出力.set(名前, 候補);
2249
- }
2250
- return 出力;
2251
- }
2252
-
2253
- /**
2254
- * CAR-1657 = parts CdlDiagram (単一 part 内容) を target CdlDiagram に prefix 付きで merge する。
2255
- * alias = user が書く actor 名 ('arc1')、 全 id を '{alias}__{origId}' で prefix、 lane 参照 rename、
2256
- * state initial は stateOverride で上書き可、 shape / subtitle / value 内の '{stateName}' template は
2257
- * 値の前置き ('{valueAlias}__{stateName}'、 #1189) に rewrite する。 見本が持つ他の値から決まる値
2258
- * (`derived`) も同じ前置きで閉じる (#1180)。 phase parallel merge (activate / tweens / sets の id 参照 rename)。
2259
- */
2260
- function mergePartIntoDiagram(
2261
- target: CdlDiagram,
2262
- part: CdlDiagram,
2263
- alias: string,
2264
- stateOverride: Record<string, number | string | boolean>,
2265
- laneMapping: string | undefined,
2266
- /**
2267
- * parts drop 位置 (drag-and-drop or click 追加時に呼出側が SVG viewBox 座標を書出す)。
2268
- * 未指定 = 従来 (lane.x = 0 baked-in で canvas 左端に描画)、 指定時 = parts 内部 lane の
2269
- * x / y に加算して drop 座標付近に描画。 D1 forensic (drop 座標乖離) の core fix。
2270
- */
2271
- offsetX?: number,
2272
- offsetY?: number,
2273
- /**
2274
- * parts 全体 resize 対応 (I2 forensic) = parts を Miro 相当の「1 unit」 として扱い、
2275
- * user が SE handle drag で拡大すると actor.posW/posH が書出される。 compile で受け取り、
2276
- * parts 全 sub-node の w / h と cx / cy 相対位置に scale 係数を適用して等比拡大する。
2277
- * 未指定 = 従来の parts 原寸 で描画 (scale なし)。
2278
- */
2279
- targetW?: number,
2280
- targetH?: number,
2281
- /** 書いたのに使わなかった上書きを知らせる口。 黙って捨てると理由を追えない (#1004) */
2282
- onNotice?: (notice: CompileNotice) => void,
2283
- /** 知らせに載せる行。 パーツを書いた行を指す。 行が取れない経路 (JSON) では 0 */
2284
- noticeLine = 0,
2285
- /** 見本から引き継いだ値の宣言元。 notice を見本を書いた行へ戻すために使う */
2286
- derivedSourceLines?: Map<string, number[]>,
2287
- /**
2288
- * 値と状態の名前に使う前置き (#1189)。 `{名前}` は英数字と `_` しか読めないため、
2289
- * 登場人物の名前をそのまま使えない。 渡されない経路では従来どおり名前をそのまま使う
2290
- */
2291
- valueAlias?: string,
2292
- /**
2293
- * 部品を図の縦列の中に置くか (#1980)。 置く時は、部品の 2 本目以降の縦列を図の縦列として
2294
- * 差し込む (`部品の縦列を図に差し込む`、#2145)。 位置を書いた部品は `laneMapping` を持っても
2295
- * 縦列の中に置かないので渡さない
2296
- */
2297
- 縦列の中に置く = false,
2298
- ): 部品用の縦列[] {
2299
- const prefix = (id: string): string => `${alias}__${id}`;
2300
- // 値と状態だけ別の前置きを使う (#1189)。 箱 / 縦列 / 矢印の id は `prefix` のまま
2301
- const 値前置き = valueAlias ?? alias;
2302
- const valuePrefix = (id: string): string => `${値前置き}__${id}`;
2303
- // 見本が自分で持つ名前。 **状態と、他の値から決まる値の両方** (#1180)。
2304
- //
2305
- // 値を含めないと、見本の中の `{決まる値}` が名前を付け替えられずに残り、重ねた先の同名の
2306
- // 値を指してしまう (見本どうしが互いの値を読む形になる)。
2307
- const ownIdSet = new Set([
2308
- ...part.states.map((s) => s.id),
2309
- ...(part.derived ?? []).map((d) => d.id),
2310
- ]);
2311
- const rewriteTemplate = (s: string | undefined): string | undefined => {
2312
- if (!s) return s;
2313
- return s.replace(/\{(\w+)\}/g, (m, name: string) => {
2314
- return ownIdSet.has(name) ? `{${valuePrefix(name)}}` : m;
2315
- });
2316
- };
2317
- // 値の式は見本の名前空間の中で閉じる。 見本が持つ名前だけを書き換えると、綴り違いの参照が
2318
- // 取り込み先の同名の値に偶然つながり、単体では止まる見本の意味が置いた場所で変わる。
2319
- //
2320
- // **どれが参照かは engine に決めさせる**。 engine は `{v}` と裸の `v` の両方を参照として
2321
- // 読み、関数名 (`min` / `Math.max` 等) は参照に数えない (実測)。 ここで関数の一覧を持つと
2322
- // 記法側 (`value-syntax.ts`) と engine に続く 3 つ目の写しになり、engine が関数を足した時に
2323
- // 静かにずれる。
2324
- const rewriteDerivedExpression = (expression: string): string => {
2325
- try {
2326
- return writeFormula(renameFormulaIdentifiers(parseFormula(expression), valuePrefix));
2327
- } catch {
2328
- // 読めない式は engine が止めて伝える (`value-unresolved`)。 書き換えられないので
2329
- // そのまま載せる = 名前は前置き無しのままだが、式自体が解けないため値は出ない
2330
- return expression;
2331
- }
2332
- };
2333
-
2334
- // 決定的 lane 参照 = user が書いた lane 指定を優先、 なければ parts 内部 lane を prefix 付きで作る
2335
- const targetLaneId = laneMapping;
2336
- const laneIdMap = new Map<string, string>();
2337
- // 部品用に作った縦列。 組み込んだ後に部品の位置へ固定する (#1990)
2338
- const 作った縦列: 部品用の縦列[] = [];
2339
- // parts lane の横位置。
2340
- // offset (drop / click 座標) 指定時 = part 中心を offsetX に合わせる = user が置いた位置に
2341
- // parts の中心が来る。 node は lane 中心 (lane.x + laneW/2) に描画されるため、 lane 左端を
2342
- // offsetX - laneW/2 に置くと node 中心 = offsetX となり cursor / viewport 中央に一致する
2343
- // (縦方向 offsetY と対称、 offsetY 側は要素全体の縦の中心で合わせる = `部品の置き方を読む`)。
2344
- // 従来の auto-adjust (max(offsetX, existingMax + gap) で既存 lane 右端へ強制右寄せ) は user
2345
- // directive で廃止 (2026-07-21)。 重なりは user の意図位置を優先し、 手動移動で回避する経路。
2346
- // 未指定 (座標なし fallback) 時のみ existingMax + gap で右外配置 (通常経路は drop/click で座標を渡す)。
2347
- const PARTS_LANE_GAP = 300;
2348
- const existingLaneMaxX =
2349
- target.lanes.length > 0 ? Math.max(...target.lanes.map((l) => (l.x ?? 0) + l.width)) : 0;
2350
- // parts 全体 resize (I2 forensic): user が SE handle drag で targetW/H 指定 = actor.posW/H。
2351
- // scale 基準は part 全体の bbox 幅 (全 lane の最左端〜最右端) にする。 lane[0] 幅だけを基準にすると
2352
- // multi-lane part (複数 lane を横に並べた part) で全体幅を過小評価し、 非先頭 lane の node が自 lane
2353
- // 中心からずれる (#880)。
2354
- //
2355
- // 縦列の位置と幅、要素の中心は部品の頁で配置した値を使う (`部品の置き方を読む`、#1992)。
2356
- // 書いた値で置くと、頁で描画側が広げた縦列の間と段の間が消え、要素が頁より詰まる。
2357
- // 伸縮の基準は画面側 (`partScaleFactor`) と同じ関数から取る
2358
- const 置き方 = 部品の置き方を読む(part);
2359
- const 伸縮の基準 = partScaleBase(part);
2360
- // 非有限は 1 に倒す。 桁が溢れた `大きさ:` (`Number()` が Infinity を返す長さ) を
2361
- // そのまま掛けると描けない座標になり、 大きさを見積る側 (`partTargetScale`) だけが
2362
- // 1 に倒していたため経路で食い違っていた (#1018)
2363
- const rawLaneScaleX = targetW !== undefined && targetW > 0 ? targetW / 伸縮の基準.w : 1;
2364
- const laneScaleX = Number.isFinite(rawLaneScaleX) && rawLaneScaleX > 0 ? rawLaneScaleX : 1;
2365
- // part 全体を「元 bbox 中心 → drop 座標」 の scale 変換で写す単一式 mapLaneX。 lane も node も同じ式で
2366
- // 変換し、 lane.x = mapLaneX(元 lane 左端) にすることで全 lane / 全 node が一貫して drop 座標を中心に
2367
- // scale 配置される (cc-codex #879 の mapPartX と同じ発想を lane push まで前倒し、 #880 root fix)。
2368
- // node 側は `部品の要素のずれ` が同じ中心と倍率で写す
2369
- const partOrigBboxCenterX = 置き方.左端 + 置き方.幅 / 2;
2370
- const dropCenterX =
2371
- offsetX !== undefined
2372
- ? offsetX
2373
- : existingLaneMaxX + PARTS_LANE_GAP + (置き方.幅 * laneScaleX) / 2;
2374
- const mapLaneX = (x: number): number => (x - partOrigBboxCenterX) * laneScaleX + dropCenterX;
2375
-
2376
- const 差し込んだ縦列 =
2377
- targetLaneId !== undefined && 縦列の中に置く
2378
- ? 部品の縦列を図に差し込む(target, part, targetLaneId, 置き方, laneScaleX)
2379
- : undefined;
2380
- for (const laneOrig of part.lanes) {
2381
- if (targetLaneId) {
2382
- laneIdMap.set(laneOrig.id, 差し込んだ縦列?.get(laneOrig.id) ?? targetLaneId);
2383
- } else {
2384
- const newLaneId = prefix(laneOrig.id);
2385
- laneIdMap.set(laneOrig.id, newLaneId);
2386
- // lane の左端を mapLaneX で変換 = 元 lane 左端 (x) を scale 変換後の位置に置く。 lane 幅も
2387
- // scale して lane 中心が mapLaneX(元 lane 中心) に一致する。 これで multi-lane でも各 lane が
2388
- // part 全体の scale 変換に沿って配置される。
2389
- const geom = 置き方.縦列.get(laneOrig.id) ?? { x: 0, w: 400 };
2390
- target.lanes.push({
2391
- ...laneOrig,
2392
- id: newLaneId,
2393
- label: laneOrig.label ?? alias,
2394
- x: mapLaneX(geom.x),
2395
- width: geom.w * laneScaleX,
2396
- });
2397
- 作った縦列.push({ id: newLaneId, 部品の名札: laneOrig.label });
2398
- }
2399
- }
2400
-
2401
- // parts drop 位置 fix (D1 + D2 root fix):
2402
- // D2 = parts の stack 番号 (0/1/2/…) が target sequence の stack と衝突すると
2403
- // CDL layout の rowH 計算で全 lane の同 row cy が拡張、 sequence footer 等が縦 shift。
2404
- // → 2 段防御 で分離する:
2405
- // (1) 全 parts node に posX/posY 明示 set (CDL layout の絶対配置経路 = stack 計算 skip)
2406
- // (2) parts の stack 番号を target 側 max stack + STACK_ISOLATION_OFFSET (1000) に shift
2407
- // = 万一 layout が rowH で参照しても sequence stack と重ならず影響 0 化
2408
- // D1 = drop 座標尊重 = 部品の頁で配置した要素の中心を、部品の中心が drop 座標に来るよう
2409
- // 写して描画する (#1992、`部品の要素のずれ`)。
2410
- //
2411
- const STACK_ISOLATION_OFFSET = 1000;
2412
- const shouldForcePos = offsetX !== undefined || offsetY !== undefined;
2413
- // target 側の現在 max stack + isolation offset で parts node の stack を shift、
2414
- // sequence の rowH 計算と完全分離 (D2 fix、 posX/posY 明示との 2 段防御)。
2415
- const targetMaxStack =
2416
- shouldForcePos && target.nodes.length > 0
2417
- ? Math.max(...target.nodes.map((n) => n.stack ?? 0))
2418
- : 0;
2419
- const stackShiftBase = shouldForcePos ? targetMaxStack + STACK_ISOLATION_OFFSET : 0;
2420
- // parts 全体 resize scale (I2 forensic 対応): targetW / targetH 指定時、 parts の元 total size
2421
- // に対する比率 = scale 係数、 全 sub-node の w / h + cx / cy 相対位置に scale 反映。
2422
- // scaleX は lane push と同じ part bbox 幅基準 (laneScaleX) を使う = multi-lane で lane と node の
2423
- // scale 係数が一致する (#880、 lane[0] 幅基準だと非先頭 lane の node がずれる)。
2424
- // 縦の基準は段の送り幅の合計 (`partScaleBase`)。 `大きさ:` の縦は、この合計に対する倍率で掛かる
2425
- const scaleX = laneScaleX;
2426
- const rawScaleY = targetH !== undefined && targetH > 0 ? targetH / 伸縮の基準.h : 1;
2427
- const scaleY = Number.isFinite(rawScaleY) && rawScaleY > 0 ? rawScaleY : 1;
2428
-
2429
- // 縦列を 2 本以上持つ部品を図の縦列に入れる時は、要素ごとに別の段番号を振る (#1980)。
2430
- // 位置を書いた部品は縦列を 1 本へまとめるので、元の段番号のままだと横に並んでいた要素が同じ段で
2431
- // 重なり、配置が止まる (実測 = `lane "b" の stack=1000 に node が重複`)。 縦列の中に置く部品は
2432
- // 2 本目以降を別の縦列に入れる (#2145) が、振り直しても重ならない側に倒れるだけなので分けない。
2433
- // 座標で置く要素の位置は段番号で決まらないので、振り直しても絵は変わらない
2434
- const 段を振り直す = targetLaneId !== undefined && shouldForcePos && part.lanes.length > 1;
2435
- // node merge = id prefix + lane 参照 rewrite + shape / subtitle / value 内 template rewrite
2436
- for (const [nodeIndex, nodeOrig] of part.nodes.entries()) {
2437
- const mappedLane = laneIdMap.get(nodeOrig.lane) ?? nodeOrig.lane;
2438
- // 形の中の文字は、入れ子の底まで辿って差し替える。
2439
- // 1 段だけ見る形だと `fill: { gradient: "{v}" }` のような書き方が置き換わらずに残る。
2440
- let newShape = nodeOrig.shape ? deepRewriteStrings(nodeOrig.shape, rewriteTemplate) : undefined;
2441
- // parts 全体 resize (I2 forensic): shape 内 radius / outerRadius / innerRadius / thickness に
2442
- // scale 反映 = user が SE handle drag で拡大すると shape の見た目も比例拡大される。 scaleX を採用
2443
- // (等比 scale 相当、 縦方向 scaleY と乖離する場合は近似)、 shape 内数値 field のうち幾何寸法系
2444
- // のみ scale 適用 (fill / stroke 色 field 等 non-numeric は影響なし)。
2445
- if (newShape && (scaleX !== 1 || scaleY !== 1)) {
2446
- const shapeScale = Math.min(scaleX, scaleY); // 等比 scale で circle 崩れ回避
2447
- const geomKeys = new Set(["radius", "outerRadius", "innerRadius", "thickness"]);
2448
- const scaleGeom = (obj: unknown): unknown => {
2449
- if (obj === null || typeof obj !== "object") return obj;
2450
- if (Array.isArray(obj)) return obj.map(scaleGeom);
2451
- const out: Record<string, unknown> = {};
2452
- for (const [k, v] of Object.entries(obj as Record<string, unknown>)) {
2453
- if (geomKeys.has(k) && typeof v === "number") {
2454
- out[k] = v * shapeScale;
2455
- } else if (typeof v === "object" && v !== null) {
2456
- out[k] = scaleGeom(v);
2457
- } else {
2458
- out[k] = v;
2459
- }
2460
- }
2461
- return out;
2462
- };
2463
- newShape = scaleGeom(newShape);
2464
- }
2465
- // parts drop 位置 offset 反映:
2466
- // - node.posX / posY set 済 (parts が座標を持つ) = 横は part 中心基準で scale 変換、縦は offsetY を足す
2467
- // - offset 指定時 (drop 経路) で未設定 = 部品の頁で配置した node の中心を同じ式で変換
2468
- // - offset なし (従来経路) は auto layout 継続 (posX / posY undefined)
2469
- //
2470
- // 明示 posX と頁の中心の両経路を、 lane push と同じ中心と倍率で写す (cc-codex #879 Round 2/3
2471
- // + #880)。 写す式は見積り (`partExtent`) と共有する `部品の要素のずれ` が持つため、
2472
- // lane.x != 0 でも multi-lane でも node 中心と自 lane 中心が一致し、格子が確保した場所とも一致する。
2473
- //
2474
- // offset なしの経路 (格子が場所を返さない部品だけが通る) では、node が書いた縦位置は
2475
- // `...nodeOrig` がそのまま残す。 足す offsetY が無いので写す必要が無い
2476
- const 頁 = 置き方.要素[nodeIndex] ?? { x: partOrigBboxCenterX, y: 0 };
2477
- const ずれ = 部品の要素のずれ(nodeOrig, 頁, 置き方, scaleX, scaleY);
2478
- const nodePosX =
2479
- shouldForcePos || nodeOrig.posX !== undefined ? dropCenterX + ずれ.dx : undefined;
2480
- const nodePosY = shouldForcePos ? (offsetY ?? 0) + ずれ.dy : undefined;
2481
- // parts sub-node の w / h に scale 適用 (I2 forensic 対応、 targetW/H 指定時のみ)
2482
- // catalog の値は呼出側が渡すので、 拡大しない時も数として通るか確かめる。 通さないと
2483
- // 座標が非有限になって図が描けない (実測 = 箱の中心が NaN になった)
2484
- const rawNodeW = nodeOrig.w !== undefined ? positiveOr(nodeOrig.w, 200) : undefined;
2485
- const rawNodeH = nodeOrig.h !== undefined ? positiveOr(nodeOrig.h, 200) : undefined;
2486
- const nodeW =
2487
- rawNodeW !== undefined && (scaleX !== 1 || scaleY !== 1) ? rawNodeW * scaleX : rawNodeW;
2488
- const nodeH =
2489
- rawNodeH !== undefined && (scaleX !== 1 || scaleY !== 1) ? rawNodeH * scaleY : rawNodeH;
2490
- target.nodes.push({
2491
- ...nodeOrig,
2492
- id: prefix(nodeOrig.id),
2493
- lane: mappedLane,
2494
- title: rewriteTemplate(nodeOrig.title) ?? nodeOrig.title,
2495
- subtitle: rewriteTemplate(nodeOrig.subtitle),
2496
- value: rewriteTemplate(nodeOrig.value),
2497
- // parts stack を target 側と分離 (D2 fix、 posX/posY 明示との 2 段防御)
2498
- stack: (段を振り直す ? nodeIndex : (nodeOrig.stack ?? 0)) + stackShiftBase,
2499
- ...(newShape ? { shape: newShape as CdlDiagram["nodes"][number]["shape"] } : {}),
2500
- ...(nodePosX !== undefined ? { posX: nodePosX } : {}),
2501
- ...(nodePosY !== undefined ? { posY: nodePosY } : {}),
2502
- ...(nodeW !== undefined ? { w: nodeW } : {}),
2503
- ...(nodeH !== undefined ? { h: nodeH } : {}),
2504
- });
2505
- }
2506
-
2507
- // state merge = id prefix + initial override
2508
- for (const stateOrig of part.states) {
2509
- const { initial, rejected } = resolveStateOverride(
2510
- stateOrig.initial,
2511
- stateOverride[stateOrig.id],
2512
- );
2513
- if (rejected) {
2514
- onNotice?.({
2515
- kind: "state-override-rejected",
2516
- actor: alias,
2517
- line: noticeLine,
2518
- message: `"${alias}" の ${stateOrig.id} に書いた値は色として読めないため使いません`,
2519
- hint: "色は `#ff0000` のような色番号か、 `red` のような色名で書く",
2520
- });
2521
- }
2522
- target.states.push({ id: valuePrefix(stateOrig.id), initial });
2523
- }
2524
-
2525
- // 見本が持つ「他の値から決まる値」 を引き継ぐ (#1180)。
2526
- //
2527
- // 引き継がないと、見本の中で書いた関係が重ねた先で解かれず、その値を読む箱に `{名前}` の
2528
- // 生の形が出る。 名前は状態と同じ規則で前置きを付ける = 見本を 2 つ重ねても互いの値を
2529
- // 読まない。 式の中の参照は、未定義の名前も含めて見本の名前空間へ閉じ込める。
2530
- for (const derivedOrig of part.derived ?? []) {
2531
- if (!target.derived) target.derived = [];
2532
- const id = valuePrefix(derivedOrig.id);
2533
- target.derived.push({
2534
- id,
2535
- expression: rewriteDerivedExpression(derivedOrig.expression),
2536
- });
2537
- recordDerivedSourceLine(derivedSourceLines, id, noticeLine);
2538
- }
2539
-
2540
- // edge merge = id / from / to prefix (parts 内 edge は稀だが対応)
2541
- for (const edgeOrig of part.edges) {
2542
- target.edges.push({
2543
- ...edgeOrig,
2544
- id: prefix(edgeOrig.id),
2545
- from: prefix(edgeOrig.from),
2546
- to: prefix(edgeOrig.to),
2547
- });
2548
- }
2549
-
2550
- // 数値を表示する部品 (`percent-ring` / `sparkline` / `donut` / KPI 等) は、箱と値だけでは描かれず
2551
- // `readouts` が要る。 取り込む時は全ての `readouts` に名前の前置きを付け、
2552
- // 参照先 (`source` / `historySource` / `*Source`) の差し込み文字も書き換える。
2553
- if (part.readouts && part.readouts.length > 0) {
2554
- if (!target.readouts) target.readouts = [];
2555
- for (const readoutOrig of part.readouts) {
2556
- const rewritten = deepRewriteStrings(
2557
- readoutOrig,
2558
- rewriteTemplate,
2559
- ) as CdlDiagram["readouts"] extends readonly (infer R)[] ? R : never;
2560
- // id は shape 全 walk で rewrite されないので個別に prefix
2561
- target.readouts.push({
2562
- ...(rewritten as { id: string }),
2563
- id: prefix((rewritten as { id: string }).id),
2564
- } as CdlDiagram["readouts"] extends readonly (infer R)[] ? R : never);
2565
- }
2566
- }
2567
-
2568
- // 部品の縦列は、取り込み先の縦列の後ろに足すのではなく同じ順番の縦列に重ねる (既定は同時進行)。
2569
- // 長さは長い方を採り、光らせる対象と動きと値の設定は両方を合わせる。
2570
- // 本文が `phase: false` と書いている時は、部品側の縦列を捨てる。
2571
- const phaseOptOut = stateOverride["phase"] === false;
2572
- if (phaseOptOut) {
2573
- return 作った縦列; // parts phase を破棄、 activate / tweens / sets の rewrite 不要
2574
- }
2575
- if (target.phases.length === 0) {
2576
- // target に phase なし = parts phase をそのまま追加 (prefix 付き)
2577
- for (const phaseOrig of part.phases) {
2578
- target.phases.push({
2579
- ...phaseOrig,
2580
- id: prefix(phaseOrig.id),
2581
- activate: phaseOrig.activate.map(prefix),
2582
- tweens: phaseOrig.tweens.map((t) => ({ ...t, stateId: valuePrefix(t.stateId) })),
2583
- sets: phaseOrig.sets.map((s) => ({ ...s, stateId: valuePrefix(s.stateId) })),
2584
- });
2585
- }
2586
- } else {
2587
- // parallel merge = 各 target phase に対応する parts phase を index-wise で合成 (min の phase 数まで)、
2588
- // 残 parts phase は追加 append (target より parts phase 数が多い場合)
2589
- const targetLen = target.phases.length;
2590
- const partsLen = part.phases.length;
2591
- const commonLen = Math.min(targetLen, partsLen);
2592
- for (let i = 0; i < commonLen; i++) {
2593
- const targetPhase = target.phases[i]!;
2594
- const partPhase = part.phases[i]!;
2595
- targetPhase.duration = Math.max(targetPhase.duration, partPhase.duration);
2596
- targetPhase.activate = [...targetPhase.activate, ...partPhase.activate.map(prefix)];
2597
- targetPhase.tweens = [
2598
- ...targetPhase.tweens,
2599
- ...partPhase.tweens.map((t) => ({ ...t, stateId: valuePrefix(t.stateId) })),
2600
- ];
2601
- targetPhase.sets = [
2602
- ...targetPhase.sets,
2603
- ...partPhase.sets.map((s) => ({ ...s, stateId: valuePrefix(s.stateId) })),
2604
- ];
2605
- }
2606
- // parts phase 余剰は append (target より parts が長い場合)
2607
- for (let i = commonLen; i < partsLen; i++) {
2608
- const phaseOrig = part.phases[i]!;
2609
- target.phases.push({
2610
- ...phaseOrig,
2611
- id: prefix(phaseOrig.id),
2612
- activate: phaseOrig.activate.map(prefix),
2613
- tweens: phaseOrig.tweens.map((t) => ({ ...t, stateId: valuePrefix(t.stateId) })),
2614
- sets: phaseOrig.sets.map((s) => ({ ...s, stateId: valuePrefix(s.stateId) })),
2615
- });
2616
- }
2617
- }
2618
- return 作った縦列;
2619
- }
2620
-
2621
- /**
2622
- * 式の中の名前を付け替える (#1180)。
2623
- *
2624
- * **字句ではなく木を経由する**。 engine の式は `{v}` / 裸の `v` / 数字始まり / `$` 入りと
2625
- * 参照の書き方が複数あり、正規表現で追うと書き方が 1 つ増えるたびに漏れる (review が 3 round
2626
- * 続けて別の漏れを見つけた)。 木は識別子をそのまま持つので、字句を網羅しなくてよい。
2627
- *
2628
- * 関数呼び出し (`min` / `Math.max`) は木の上で別の種類なので、名前と取り違えない。
2629
- */
2630
- function renameFormulaIdentifiers(ast: FormulaAst, rename: (name: string) => string): FormulaAst {
2631
- switch (ast.type) {
2632
- case "number":
2633
- return ast;
2634
- case "identifier":
2635
- return { type: "identifier", name: rename(ast.name) };
2636
- case "unaryOp":
2637
- return { ...ast, operand: renameFormulaIdentifiers(ast.operand, rename) };
2638
- case "binaryOp":
2639
- return {
2640
- ...ast,
2641
- left: renameFormulaIdentifiers(ast.left, rename),
2642
- right: renameFormulaIdentifiers(ast.right, rename),
2643
- };
2644
- case "ternary":
2645
- return {
2646
- type: "ternary",
2647
- condition: renameFormulaIdentifiers(ast.condition, rename),
2648
- whenTrue: renameFormulaIdentifiers(ast.whenTrue, rename),
2649
- whenFalse: renameFormulaIdentifiers(ast.whenFalse, rename),
2650
- };
2651
- case "call":
2652
- return { ...ast, args: ast.args.map((a) => renameFormulaIdentifiers(a, rename)) };
2653
- }
2654
- }
2655
-
2656
- /**
2657
- * 数を、engine の読み手が受け付ける形で書く (#1180)。
2658
- *
2659
- * **指数表記を出さない**。 `0.0000001` は JavaScript の既定では `"1e-7"` になるが、engine の
2660
- * 読み手は指数表記を読めない (実測 = `unexpected token after expression`)。 そのまま書くと、
2661
- * 元は解けていた式が書き換えた後だけ止まる。
2662
- *
2663
- * 展開は桁をずらすだけで、丸めない。 `String` が返す最短の形をそのまま使うため、値は変わらない。
2664
- * 有限でない数は書けないので投げる (呼出側が元の式のまま載せる)。
2665
- */
2666
- function writeNumber(value: number): string {
2667
- if (!Number.isFinite(value)) throw new Error(`cannot write non-finite number: ${String(value)}`);
2668
- const s = String(value);
2669
- if (!/[eE]/.test(s)) return s;
2670
- const m = /^(-?)(\d+)(?:\.(\d+))?[eE]([+-]?\d+)$/.exec(s);
2671
- if (!m) throw new Error(`cannot write number: ${s}`);
2672
- const sign = m[1] ?? "";
2673
- const int = m[2] ?? "";
2674
- const frac = m[3] ?? "";
2675
- const digits = int + frac;
2676
- // 小数点の位置。 元の整数部の桁数を指数のぶんだけずらす
2677
- const point = int.length + Number(m[4] ?? "0");
2678
- if (point <= 0) return `${sign}0.${"0".repeat(-point)}${digits}`;
2679
- if (point >= digits.length) return `${sign}${digits}${"0".repeat(point - digits.length)}`;
2680
- return `${sign}${digits.slice(0, point)}.${digits.slice(point)}`;
2681
- }
2682
-
2683
- /**
2684
- * 式の木を文字列へ戻す (#1180)。
2685
- *
2686
- * **括弧を全て付ける**。 演算子の優先順位を再現しようとすると engine の表を写すことになり、
2687
- * 表がずれた時に式の意味が静かに変わる。 括弧が増えても解いた結果は変わらない。
2688
- */
2689
- function writeFormula(ast: FormulaAst): string {
2690
- switch (ast.type) {
2691
- case "number":
2692
- return writeNumber(ast.value);
2693
- case "identifier":
2694
- // **裸で書ける形とそうでない形がある**。 engine は裸の名前を `[A-Za-z_$][\w$]*` で読む
2695
- // 一方、波括弧の中は `\w+` なので数字始まりの名前は波括弧付きでしか書けない。
2696
- // 名前は前置きで変わる (`1p__v` のように数字始まりになりうる) ため、書ける方を選ぶ
2697
- return /^[A-Za-z_$][\w$]*$/.test(ast.name) ? ast.name : `{${ast.name}}`;
2698
- case "unaryOp":
2699
- // 空白は挟まない。 読み手は負の数を字面として持たず (`-5` は単項 `-` と `5` の木になる)、
2700
- // `--5` も単項の 2 段として読む (実測)。 挟んでも挟まなくても意味が同じなので足さない
2701
- return `(${ast.op}${writeFormula(ast.operand)})`;
2702
- case "binaryOp":
2703
- return `(${writeFormula(ast.left)} ${ast.op} ${writeFormula(ast.right)})`;
2704
- case "ternary":
2705
- return `(${writeFormula(ast.condition)} ? ${writeFormula(ast.whenTrue)} : ${writeFormula(ast.whenFalse)})`;
2706
- case "call":
2707
- return `${ast.fn}(${ast.args.map(writeFormula).join(", ")})`;
2708
- }
2709
- }