@cardenelabs/dragon 0.7.0 → 0.9.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.
@@ -0,0 +1,289 @@
1
+ /**
2
+ * 記法と JSON を画面で色分けするための分解器 (#1310)。
3
+ *
4
+ * 本文を受けて `[種類, 開始, 終わり]` の並びを返す。 **色は知らない**。 実際の色は
5
+ * 使う側 (画面) が種類名に当てる = 埋め込み先ごとに自分の配色を使えるようにするため。
6
+ *
7
+ * ## なぜ package 側に置くか
8
+ *
9
+ * 「記法をどう切るか」 は parser と同じ知識で、色名の別名と矢印の書き方を `keywords.ts`、
10
+ * 正規の色名を `@cardenelabs/cdl` から直接引ける。 画面側に置くと語彙を写すことになり、
11
+ * 語が増えた時に片方だけ古くなる (#1293 / #1304 / #1306 で 3 度踏んだ形)。
12
+ *
13
+ * ## 何を切るか
14
+ *
15
+ * | 種類 | 例 | 図での意味 |
16
+ * |---|---|---|
17
+ * | `項目名` | `title:` / `kind:` / `- 名前:` | 構造。 色ではなく太さで示す |
18
+ * | `説明文` | `"ログイン"` / `: 検索` | 箱と矢印に出る文字 |
19
+ * | `矢印` | `->` / `→` | 繋がりそのもの |
20
+ * | `色名` | `success` / `成功` / `dotted-flow` | **その色で描く** (`色` に解決後の名前が入る) |
21
+ * | `値の参照` | `{count}` | 段で動く値 |
22
+ * | `注記` | `# ...` | 読み飛ばしてよい部分 |
23
+ *
24
+ * 色名だけ `色` を返すのは、画面が「その色名が図で引く色」 で描けるようにするため。
25
+ * 本文の `(成功)` と図の緑が一致すると、どの語がどの見た目に効くかが読める。
26
+ *
27
+ * ## 重なりを作らない
28
+ *
29
+ * 返す並びは開始位置の昇順で、**互いに重ならない**。 使う側は並びをそのまま
30
+ * `<span>` や装飾に変換するだけでよく、重なりの解決を持たなくて済む。
31
+ *
32
+ * 値の参照は説明文の中にも出る (`value: "{count}"`)。 その場合は説明文を参照の前後で
33
+ * 分割して返す = 参照だけ別の色にできる。
34
+ */
35
+ import { TONE_ALIAS, ARROW_PATTERNS } from "./keywords";
36
+ import { TONES } from "@cardenelabs/cdl";
37
+ import type { Tone } from "@cardenelabs/cdl";
38
+
39
+ /** 分解した部分の種類 (#1310) */
40
+ export type トークンの種類 = "項目名" | "説明文" | "矢印" | "色名" | "値の参照" | "注記";
41
+
42
+ /** 分解した部分 1 つ。 位置は本文の先頭からの文字数 (`slice` にそのまま渡せる) */
43
+ export type トークン = {
44
+ 種類: トークンの種類;
45
+ 開始: number;
46
+ 終わり: number;
47
+ /** `色名` の時だけ入る、解決後の色の名前。 画面はこれで「その色」 を引く */
48
+ 色?: Tone;
49
+ };
50
+
51
+ /**
52
+ * 線種の一覧 (#1310)。 `v05/parser.ts` の `STYLE_VALID` と同じ値を持つ。
53
+ *
54
+ * あちらを import すると parser 全体を引き込むため、分解器では持ち直す。 **2 箇所に
55
+ * 分かれるので検査で突き合わせる** (`tokenize.test.ts`)。
56
+ */
57
+ const 線種 = ["solid", "dotted-flow"] as const;
58
+
59
+ /** 色名として読める語 (小文字で引く)。 正規の色名と別名の両方 */
60
+ const 色名の表 = new Map<string, Tone>([
61
+ ...TONES.map((t) => [t.toLowerCase(), t] as [string, Tone]),
62
+ ...Object.entries(TONE_ALIAS).map(([k, v]) => [k.toLowerCase(), v] as [string, Tone]),
63
+ ]);
64
+
65
+ /**
66
+ * 矢印の書き方。 並べ替えない。
67
+ *
68
+ * `->` と `->>` は同じ位置から始まるため、どちらを取るかを決める必要がある。 それは
69
+ * `重なりを外す` が「同じ開始なら長い方」 として 1 箇所で決める。 ここでも長い順に
70
+ * 並べると **同じ規則が 2 箇所に分かれ、片方を外しても検査が通る** (実測)。
71
+ */
72
+ const 矢印の表 = [...new Set(ARROW_PATTERNS)];
73
+
74
+ /**
75
+ * 色名として読める語なら、解決後の名前を返す。
76
+ *
77
+ * 判定は `resolveTone` と同じ (別名を引き、小文字に寄せる) が、引用符は外さない =
78
+ * 分解器は本文の位置を返すため、外した後の文字列を作ってはいけない。
79
+ */
80
+ function 色名として読む(語: string): Tone | undefined {
81
+ return 色名の表.get(語.toLowerCase());
82
+ }
83
+
84
+ /** 線種として読める語か */
85
+ function 線種として読むか(語: string): boolean {
86
+ return (線種 as readonly string[]).includes(語.toLowerCase());
87
+ }
88
+
89
+ /*
90
+ * 箱の種類 (`service` / `database` 等) は色分けしない (#1310 review r1-f1)。
91
+ *
92
+ * 一度は種類名を `項目名` として扱ったが、**箱の名前まで巻き込む**。 受理する 108 種には
93
+ * `user` / `api` / `actor` / `card` / `state` / `service` が含まれ、`- User` や `- API` の
94
+ * ような普通の名前と衝突する (実測で両方とも項目名になった)。
95
+ *
96
+ * 正しく分けるには「種類の位置に書かれているか」 を見る必要があり、それは 5 種
97
+ * (項目名 / 説明文 / 矢印 / 色名 / 値の参照) の外側になる。 語だけで判定できないものは
98
+ * 色分けしない。
99
+ */
100
+
101
+ /** 重なりを持たない並びに整える。 先に入れたものを優先し、後から重なる分は捨てる */
102
+ function 重なりを外す(候補: トークン[]): トークン[] {
103
+ const out: トークン[] = [];
104
+ for (const t of [...候補].sort((a, b) => a.開始 - b.開始 || b.終わり - a.終わり)) {
105
+ if (t.終わり <= t.開始) continue;
106
+ const 直前 = out[out.length - 1];
107
+ if (直前 !== undefined && t.開始 < 直前.終わり) continue;
108
+ out.push(t);
109
+ }
110
+ return out;
111
+ }
112
+
113
+ /** `{名前}` を拾う。 名前は英数字と `_` だけ (描画側が置き換える時に見る範囲と揃える) */
114
+ function 値の参照を拾う(src: string, 起点: number, 本文: string, 積む: (t: トークン) => void): void {
115
+ for (const m of 本文.matchAll(/\{[A-Za-z_][\w]*\}/g)) {
116
+ 積む({ 種類: "値の参照", 開始: 起点 + m.index, 終わり: 起点 + m.index + m[0].length });
117
+ }
118
+ }
119
+
120
+ /**
121
+ * 引用符で囲まれた部分を拾い、中に値の参照があれば前後で割る。
122
+ *
123
+ * 割らずに 1 つの説明文として返すと、`value: "{count}"` の参照に色を当てられない。
124
+ */
125
+ function 説明文を拾う(src: string, 起点: number, 本文: string, 積む: (t: トークン) => void): void {
126
+ for (const m of 本文.matchAll(/"[^"]*"|'[^']*'/g)) {
127
+ const 開始 = 起点 + m.index;
128
+ const 終わり = 開始 + m[0].length;
129
+ const 参照: トークン[] = [];
130
+ 値の参照を拾う(src, 開始, m[0], (t) => 参照.push(t));
131
+ if (参照.length === 0) {
132
+ 積む({ 種類: "説明文", 開始, 終わり });
133
+ continue;
134
+ }
135
+ let 位置 = 開始;
136
+ for (const r of 参照.sort((a, b) => a.開始 - b.開始)) {
137
+ if (r.開始 > 位置) 積む({ 種類: "説明文", 開始: 位置, 終わり: r.開始 });
138
+ 積む(r);
139
+ 位置 = r.終わり;
140
+ }
141
+ if (位置 < 終わり) 積む({ 種類: "説明文", 開始: 位置, 終わり });
142
+ }
143
+ }
144
+
145
+ /**
146
+ * 記法を分解する (#1310)。
147
+ *
148
+ * 行ごとに見る。 記法は行単位で意味が決まるため、行をまたぐ状態を持たない。
149
+ */
150
+ export function 記法を分解する(src: string): トークン[] {
151
+ const 候補: トークン[] = [];
152
+ const 積む = (t: トークン): void => {
153
+ 候補.push(t);
154
+ };
155
+ let 起点 = 0;
156
+ for (const 行 of src.split("\n")) {
157
+ 分解する1行(src, 起点, 行, 積む);
158
+ 起点 += 行.length + 1;
159
+ }
160
+ return 重なりを外す(候補);
161
+ }
162
+
163
+ function 分解する1行(
164
+ src: string,
165
+ 起点: number,
166
+ 行: string,
167
+ 積む: (t: トークン) => void,
168
+ ): void {
169
+ const 字下げ = 行.length - 行.trimStart().length;
170
+ const 本体 = 行.trimStart();
171
+ if (本体 === "") return;
172
+
173
+ // 注記は行ごと。 `#` を色番号 (`#f59e0b`) と取り違えないよう、行の頭だけを見る
174
+ if (本体.startsWith("#")) {
175
+ 積む({ 種類: "注記", 開始: 起点 + 字下げ, 終わり: 起点 + 行.length });
176
+ return;
177
+ }
178
+
179
+ // 説明文と値の参照を先に取る = 引用符の中の `->` や色名を拾わないため
180
+ 説明文を拾う(src, 起点, 行, 積む);
181
+ 値の参照を拾う(src, 起点, 行, 積む);
182
+
183
+ // 項目名 = 行頭 (`- ` の後も含む) の `名前:`
184
+ const 見出し = 本体.match(/^(-\s*)?([^\s:{}[\]"']+)\s*:/);
185
+ if (見出し) {
186
+ const 前 = 見出し[1]?.length ?? 0;
187
+ const 名 = 見出し[2] ?? "";
188
+ const 開始 = 起点 + 字下げ + 前;
189
+ 積む({ 種類: "項目名", 開始, 終わり: 開始 + 名.length + 1 });
190
+ }
191
+
192
+ // 矢印
193
+ for (const 記号 of 矢印の表) {
194
+ let i = 行.indexOf(記号);
195
+ while (i >= 0) {
196
+ 積む({ 種類: "矢印", 開始: 起点 + i, 終わり: 起点 + i + 記号.length });
197
+ i = 行.indexOf(記号, i + 記号.length);
198
+ }
199
+ }
200
+
201
+ // 色名と線種。 語の切れ目で区切って 1 語ずつ見る = 説明文の一部を拾わないよう、
202
+ // 引用符の中は上で説明文として先に取ってある (重なりを外す時に落ちる)
203
+ for (const m of 行.matchAll(/[^\s(),:{}[\]"']+/g)) {
204
+ const 語 = m[0];
205
+ const 色 = 色名として読む(語);
206
+ if (色 !== undefined) {
207
+ 積む({ 種類: "色名", 開始: 起点 + m.index, 終わり: 起点 + m.index + 語.length, 色 });
208
+ continue;
209
+ }
210
+ if (線種として読むか(語)) {
211
+ 積む({ 種類: "色名", 開始: 起点 + m.index, 終わり: 起点 + m.index + 語.length });
212
+ }
213
+ }
214
+ }
215
+
216
+ /**
217
+ * JSON を分解する (#1310)。
218
+ *
219
+ * 記法と違い鍵は英語で、記法の語彙は出ない。 したがって **鍵 / 文字列 / 数を汎用の色で塗る**。
220
+ *
221
+ * 例外は `tone` と `color` の値で、こちらは記法と同じ語彙を持つため色名として扱う。
222
+ * 同じ `"success"` が入口によって別の色になる状態を作らないため。
223
+ */
224
+ export function JSONを分解する(src: string): トークン[] {
225
+ const 候補: トークン[] = [];
226
+ // 鍵 (`"name":`) と、その値が色名を取る欄かを見る
227
+ for (const m of src.matchAll(/"((?:[^"\\]|\\.)*)"(\s*:)?/g)) {
228
+ const 開始 = m.index;
229
+ const 終わり = 開始 + m[0].length;
230
+ if (m[2] !== undefined) {
231
+ 候補.push({ 種類: "項目名", 開始, 終わり });
232
+ continue;
233
+ }
234
+ 候補.push({ 種類: "説明文", 開始, 終わり });
235
+ }
236
+ // 数値も文字列と同じ「値」の色にする。 JSON の数値文法に合わせ、符号・小数・指数を拾う。
237
+ // 文字列の中に現れた数字は、先に積んだ説明文と重なるため `重なりを外す` で落ちる。
238
+ for (const m of src.matchAll(/-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/g)) {
239
+ 候補.push({ 種類: "説明文", 開始: m.index, 終わり: m.index + m[0].length });
240
+ }
241
+ // 色名を取る欄の値を色名に格上げする
242
+ for (const m of src.matchAll(/"(tone|color)"\s*:\s*"((?:[^"\\]|\\.)*)"/g)) {
243
+ const 値の開始 = m.index + m[0].lastIndexOf('"' + m[2] + '"');
244
+ const 値 = m[2] ?? "";
245
+ const 色 = 色名として読む(値);
246
+ if (色 === undefined) continue;
247
+ const i = 候補.findIndex((t) => t.開始 === 値の開始);
248
+ if (i >= 0) 候補[i] = { 種類: "色名", 開始: 値の開始, 終わり: 値の開始 + 値.length + 2, 色 };
249
+ }
250
+ // 値の参照は文字列の中に出る (`"{count}"`)
251
+ const 参照: トークン[] = [];
252
+ 値の参照を拾う(src, 0, src, (t) => 参照.push(t));
253
+ for (const r of 参照) {
254
+ const i = 候補.findIndex((t) => t.開始 < r.開始 && r.終わり <= t.終わり && t.種類 === "説明文");
255
+ const 元 = i < 0 ? undefined : 候補[i];
256
+ if (元 === undefined) continue;
257
+ 候補.splice(
258
+ i,
259
+ 1,
260
+ { 種類: "説明文", 開始: 元.開始, 終わり: r.開始 },
261
+ r,
262
+ { 種類: "説明文", 開始: r.終わり, 終わり: 元.終わり },
263
+ );
264
+ }
265
+ return 重なりを外す(候補);
266
+ }
267
+
268
+ /**
269
+ * 分解した並びを、本文を覆う区間の列に広げる (#1310)。
270
+ *
271
+ * 分解器は色が付く部分だけを返すため、間の文字が抜ける。 画面はそのまま並べると本文が
272
+ * 欠けるので、**色の付かない部分も種類なしの区間として挟む**。
273
+ *
274
+ * 返す区間は本文を過不足なく覆う (連結すると元の本文に戻る)。
275
+ */
276
+ export function 区間に広げる(
277
+ src: string,
278
+ tokens: トークン[],
279
+ ): Array<{ 文字: string; 種類?: トークンの種類; 色?: Tone }> {
280
+ const out: Array<{ 文字: string; 種類?: トークンの種類; 色?: Tone }> = [];
281
+ let 位置 = 0;
282
+ for (const t of tokens) {
283
+ if (t.開始 > 位置) out.push({ 文字: src.slice(位置, t.開始) });
284
+ out.push({ 文字: src.slice(t.開始, t.終わり), 種類: t.種類, 色: t.色 });
285
+ 位置 = t.終わり;
286
+ }
287
+ if (位置 < src.length) out.push({ 文字: src.slice(位置) });
288
+ return out;
289
+ }
package/src/types.ts CHANGED
@@ -17,6 +17,12 @@ export type PresetType =
17
17
  | "gantt"
18
18
  | "class"
19
19
  | "pie"
20
+ | "bar"
21
+ | "line"
22
+ | "funnel"
23
+ | "tree"
24
+ | "journey"
25
+ | "quadrant"
20
26
  | "c4"
21
27
  | "mind";
22
28
 
@@ -50,9 +56,42 @@ export type LayoutMode = "auto" | "manual";
50
56
  export type DslDocument = {
51
57
  title: string;
52
58
  type: PresetType;
59
+ /**
60
+ * 図全体を 1 箱にする図種 (`pie` / `bar` / `line` / `funnel` / `tree` / `journey` /
61
+ * `quadrant` / `mind` / `gantt`) で、 その箱の上に出す小見出し (#1247)。
62
+ *
63
+ * これらの図種は箱を 1 つしか作らないため「どの箱の小見出しか」 が決まる。 箱ごとに
64
+ * 分かれる図種では決まらないので、 書かれていたら組み立て側が知らせる。
65
+ */
66
+ eyebrow?: string;
67
+ /**
68
+ * `eyebrow` を書いた行 (#1247)。 知らせの行番号に使う。
69
+ *
70
+ * 図の `pos` は常に 1 行目を指すため、 そこを使うと「10 行目に書いた `eyebrow` が効かない」
71
+ * を 1 行目として知らせることになり、 書いた場所に辿り着けない。
72
+ */
73
+ eyebrowPos?: Position;
74
+ /**
75
+ * 2 軸で仕分ける図 (`type: quadrant`) の軸の名前 (#1251)。
76
+ *
77
+ * 書かないと「小さい / 大きい」 のままになり、何を判断する図か読めない。
78
+ * 区画の名前 (`右上` 等) は軸の名前から `{上} × {右}` の形で決まる。
79
+ *
80
+ * 他の図種には軸が無いため、書かれていたら組み立て側が知らせる。
81
+ */
82
+ axes?: DslAxes;
83
+ /** `axes` を書いた行 (#1251)。 知らせの行番号に使う */
84
+ axesPos?: Position;
53
85
  actors: DslActor[];
54
86
  flow: DslStep[];
55
87
  animate?: DslAnimate;
88
+ /**
89
+ * 他の値から自動で決まる値 (`values:`)。 時間を持たず、 参照した値が動けば常に追随する。
90
+ *
91
+ * `states` (初期値だけを持つ) と `animation` (いつ何を動かすか) の間に置く層で、
92
+ * 値どうしの関係を書く場所。 書かなければ今までと同じ挙動。
93
+ */
94
+ values?: DslValue[];
56
95
  /** v0.5+ 拡張 ... viewport / lanes / groups */
57
96
  viewport?: DslViewport;
58
97
  lanes?: Record<string, DslLane>;
@@ -65,6 +104,12 @@ export type DslDocument = {
65
104
  pos: Position;
66
105
  };
67
106
 
107
+ /** 2 軸で仕分ける図の軸の名前 (#1251) */
108
+ export type DslAxes = {
109
+ x?: { left?: string; right?: string };
110
+ y?: { bottom?: string; top?: string };
111
+ };
112
+
68
113
  /** 登場人物 (v0.5+ ... inline option 拡張) */
69
114
  export type DslActor = {
70
115
  name: string;
@@ -83,6 +128,35 @@ export type DslActor = {
83
128
  eyebrow?: string;
84
129
  value?: string;
85
130
  rows?: string[];
131
+ /**
132
+ * 工程の並び (`type: gantt`) で、その工程の担当 (#1251)。
133
+ *
134
+ * 他の図種では相手が無いため、書かれていたら組み立て側が知らせる。
135
+ */
136
+ owner?: string;
137
+ /**
138
+ * 工程の並び (`type: gantt`) で、その工程が終わる時期 (#1251)。
139
+ *
140
+ * 書かなければ始まりと同じ時期に終わる (帯が 1 コマ)。 `{名前}` を書くと状態から取り、
141
+ * 段で帯が伸び縮みする様子を見せられる。
142
+ *
143
+ * 他の図種では相手が無いため、書かれていたら組み立て側が知らせる。
144
+ */
145
+ end?: string;
146
+ /**
147
+ * 体験の道筋 (`type: journey`) で、その段階が起きる場所 (#1251)。
148
+ *
149
+ * 「どこで起きたか」 を段の下に出す。 他の図種では相手が無いため、書かれていたら
150
+ * 組み立て側が知らせる。
151
+ */
152
+ touchpoint?: string;
153
+ /**
154
+ * 体験の道筋 (`type: journey`) で、その段階の改善の余地 (#1251)。
155
+ *
156
+ * 気持ちが落ちる段に「何を直せるか」 を添える。 他の図種では相手が無いため、
157
+ * 書かれていたら組み立て側が知らせる。
158
+ */
159
+ opportunity?: string;
86
160
  lane?: string;
87
161
  stack?: number;
88
162
  initial?: boolean;
@@ -189,6 +263,8 @@ export type DslStep = {
189
263
  cardinality?: string;
190
264
  labelOffsetX?: number;
191
265
  labelOffsetY?: number;
266
+ /** true で説明文を矢印の線の上に重ねる。 分岐図の条件ラベル用。 */
267
+ overlay?: boolean;
192
268
  /**
193
269
  * canvas pivot (CAR-1693 Phase 1) DSL 表面 `pos: {x, y}` 由来の layout offset。 step の edge
194
270
  * label 位置を auto layout compute から (dx, dy) shift する。 未指定は auto、 set 済は Phase 2 で適用。
@@ -259,6 +335,55 @@ export type DslState = {
259
335
  pos: Position;
260
336
  };
261
337
 
338
+ /**
339
+ * 値が動き出すきっかけ (#1161 段 2)。
340
+ *
341
+ * `step` は段が始まった時、 `reaches` は別の値が境目を越えた時。 どちらも「成り立った瞬間の
342
+ * 出来事」 で、 常に成り立つ関係を表す式とは別物 (spec § 2.3)。
343
+ */
344
+ export type DslValueTrigger =
345
+ | { kind: "step"; step: string }
346
+ | {
347
+ kind: "reaches";
348
+ /** 見張る相手の値の名前 */
349
+ source: string;
350
+ op: ">=" | ">" | "<=" | "<" | "==" | "!=";
351
+ threshold: number;
352
+ };
353
+
354
+ /**
355
+ * 記法の `values:` の 1 件。 形は 2 つある。
356
+ *
357
+ * | 形 | 持つもの | 意味 |
358
+ * |---|---|---|
359
+ * | 式 | `expression` | 常に成り立つ関係。 時間を持たない |
360
+ * | きっかけ | `trigger` / `to` / `durationMs` | きっかけから `to` まで動く。 時間を持つ |
361
+ *
362
+ * 両方を持つ形は無い (parser が弾く)。 きっかけ形は組み立ての時点で段の時計を読む式へ畳まれる
363
+ * ため、 図に載る時にはどちらも `derived` になる。
364
+ */
365
+ export type DslValue = {
366
+ name: string;
367
+ pos: Position;
368
+ } & (
369
+ | {
370
+ /** 式そのもの。 評価は描画側が毎 frame 行う */
371
+ expression: string;
372
+ trigger?: never;
373
+ to?: never;
374
+ durationMs?: never;
375
+ }
376
+ | {
377
+ expression?: never;
378
+ /** 動き出すきっかけ */
379
+ trigger: DslValueTrigger;
380
+ /** 動いた先の値 */
381
+ to: number;
382
+ /** 動く長さ (ms) */
383
+ durationMs: number;
384
+ }
385
+ );
386
+
262
387
  /** ステップ (phase) */
263
388
  export type DslPhase = {
264
389
  name: string;
@@ -268,6 +393,25 @@ export type DslPhase = {
268
393
  sets?: DslSet[]; // state 即時遷移
269
394
  body?: string;
270
395
  badge?: string;
396
+ /**
397
+ * その段で左の起点から描くもの (`draw: line`、 #1312)。
398
+ *
399
+ * 受ける語は今のところ `line` だけ。 折れ線 (`type: line`) の図で、その段の間に線が
400
+ * 左端から右へ伸び、点と値の数字は線の先が届いた時に現れる。
401
+ *
402
+ * **最上位ではなく段に置く**。 記法は動き (`tween` / `set` / `focus` / `badge`) を段に、
403
+ * 静的な性質 (`eyebrow` / `axes`) を最上位に書く分け方を既に持つ。 線が伸びるのは動き。
404
+ *
405
+ * 相手の名前は書かせない = 折れ線は図全体を 1 箱で描くため相手が 1 つに決まる。
406
+ */
407
+ draw?: string;
408
+ /**
409
+ * `draw` を書いた行 (#1312)。 知らせの行番号に使う。
410
+ *
411
+ * 段の `pos` は `- step:` の行を指すため、そこを使うと「段の 3 行目に書いた `draw` が
412
+ * 効かない」 を段の先頭行として知らせることになる。
413
+ */
414
+ drawPos?: Position;
271
415
  pos: Position;
272
416
  };
273
417