@cardenelabs/dragon 0.36.4 → 0.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -2
- package/dist/cli.cjs +1126 -220
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1118 -212
- package/dist/cli.js.map +1 -1
- package/dist/export.cjs +1043 -165
- package/dist/export.cjs.map +1 -1
- package/dist/export.d.cts +1 -1
- package/dist/export.d.ts +1 -1
- package/dist/export.js +1038 -160
- package/dist/export.js.map +1 -1
- package/dist/index.cjs +1133 -218
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +77 -3
- package/dist/index.d.ts +77 -3
- package/dist/index.js +1132 -219
- package/dist/index.js.map +1 -1
- package/dist/player.global.js +2 -2
- package/dist/{schema-9VLameDG.d.cts → schema-B2MTRv66.d.cts} +11 -2
- package/dist/{schema-9VLameDG.d.ts → schema-B2MTRv66.d.ts} +11 -2
- package/package.json +1 -1
- package/src/field-messages.ts +4 -0
- package/src/figure-text.ts +21 -0
- package/src/index.ts +8 -1
- package/src/layout/architecture.ts +1 -1
- package/src/layout/scene.ts +78 -8
- package/src/layout/sequence.ts +1 -1
- package/src/layout/state.ts +387 -0
- package/src/look/values.ts +40 -5
- package/src/messages.ts +14 -0
- package/src/parser.ts +45 -14
- package/src/printer.ts +6 -1
- package/src/render/icons.ts +4 -0
- package/src/render/parts.ts +170 -23
- package/src/render/svg.ts +78 -8
- package/src/schema.ts +22 -6
- package/src/schemas/diagram.schema.json +30 -4
- package/src/schemas/validate-diagram.js +1 -1
- package/src/timeline.ts +47 -0
- package/src/words.ts +8 -1
package/src/render/icons.ts
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* 箱と区画の印の形。どれも (0, 0) を中心に 20 四方へ収める。
|
|
3
3
|
*
|
|
4
4
|
* 見本帳 (`docs/design/base/engine.js` の `G`) と同じ形を持つ。`block` だけは見本帳が絵を手で描いていたので、ここで足した。
|
|
5
|
+
* `undo` (戻る矢印) と `yen` (円の印) は、注文の試作 (`docs/design/base/scenes/order.js`) が返品と返金に足した形 (#3171)。
|
|
6
|
+
* package の入口からも出し、見本帳の部品「絵」の頁がこの表から使える絵柄の一覧を作る。1 つずつの絵は `iconSvg` が描く (#3172)。
|
|
5
7
|
*/
|
|
6
8
|
export const ICONS: Readonly<Record<string, string>> = {
|
|
7
9
|
app: "M -9 -7 h18 a2 2 0 0 1 2 2 v10 a2 2 0 0 1 -2 2 h-18 a2 2 0 0 1 -2 -2 v-10 a2 2 0 0 1 2 -2 z M -11 -2 h22",
|
|
@@ -34,6 +36,8 @@ export const ICONS: Readonly<Record<string, string>> = {
|
|
|
34
36
|
branch: "M -4 -4.5 v9 M 5 0.5 c0 4 -9 3 -9 6.5 M -6.2 -7 a2.2 2.2 0 1 0 4.4 0 a2.2 2.2 0 1 0 -4.4 0 M -6.2 7 a2.2 2.2 0 1 0 4.4 0 a2.2 2.2 0 1 0 -4.4 0 M 2.8 -2 a2.2 2.2 0 1 0 4.4 0 a2.2 2.2 0 1 0 -4.4 0",
|
|
35
37
|
cloud: "M -7 6 h13 a4.5 4.5 0 0 0 0.5 -9 a6 6 0 0 0 -11.5 -1 a4.5 4.5 0 0 0 -2 10 z",
|
|
36
38
|
refresh: "M 7 -3 a7.5 7.5 0 1 0 0.5 5 M 7.5 -8 v5 h-5",
|
|
39
|
+
undo: "M -7 -2 h10 a5 5 0 0 1 0 10 h-6 M -3 -6 l-4 4 l4 4",
|
|
40
|
+
yen: "M -6 -8 l6 7 l6 -7 M 0 -1 v9 M -5 1.5 h10 M -5 5 h10",
|
|
37
41
|
layers: "M 0 -8 l9 4.5 l-9 4.5 l-9 -4.5 z M -9 2.5 l9 4.5 l9 -4.5",
|
|
38
42
|
code: "M -4 -6 l-5 6 l5 6 M 4 -6 l5 6 l-5 6",
|
|
39
43
|
paint: "M -8 8 c 0 -3 2 -5 4.5 -5 l 9 -9 l 3.5 3.5 l -9 9 c 0 2.5 -2 4.5 -5 4.5 z",
|
package/src/render/parts.ts
CHANGED
|
@@ -5,7 +5,21 @@
|
|
|
5
5
|
* 色は色の役の変数 (`--c` と `--ct`) だけを渡し、値は色味の表が持つ。
|
|
6
6
|
*/
|
|
7
7
|
import { hasWide, textWidth } from "../layout/measure";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
along,
|
|
10
|
+
type Point,
|
|
11
|
+
pathLength,
|
|
12
|
+
type SceneAction,
|
|
13
|
+
type SceneBadge,
|
|
14
|
+
type SceneBox,
|
|
15
|
+
type SceneEdge,
|
|
16
|
+
type SceneEnd,
|
|
17
|
+
type SceneLabel,
|
|
18
|
+
type ScenePath,
|
|
19
|
+
type SceneRegion,
|
|
20
|
+
type SceneSide,
|
|
21
|
+
type SceneStrip,
|
|
22
|
+
} from "../layout/scene";
|
|
9
23
|
import { type ColorKey, EASE, LOOK, MOTION, TYPE } from "../look/values";
|
|
10
24
|
import type { CarrierTime, EdgeTime, LabelTime, Span, Timeline } from "../timeline";
|
|
11
25
|
import { ICONS } from "./icons";
|
|
@@ -22,7 +36,36 @@ export function glyph(name: string | undefined, x: number, y: number, scale: num
|
|
|
22
36
|
return d ? `<path class="${cls}" transform="translate(${r1(x)} ${r1(y)}) scale(${scale})" d="${d}"/>` : "";
|
|
23
37
|
}
|
|
24
38
|
|
|
25
|
-
|
|
39
|
+
/**
|
|
40
|
+
* 絵柄を 1 つだけ描いた SVG (#3172)。見本帳の部品「絵」の頁が、使える絵柄を並べる時に使う。
|
|
41
|
+
* 線の太さは箱の印と同じ割合 (印の線の太さ × 印の縮み) で、色は置いた所の字の色 (`currentColor`) を使う。知らない名前は `undefined`
|
|
42
|
+
*/
|
|
43
|
+
export function iconSvg(name: string): string | undefined {
|
|
44
|
+
if (!Object.hasOwn(ICONS, name)) return undefined;
|
|
45
|
+
const width = LOOK.stroke.glyph * LOOK.box.iconScale;
|
|
46
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="-12 -12 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="${width}" stroke-linecap="round" stroke-linejoin="round"><path d="${ICONS[name]}"/></svg>`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** 点を辿る線。曲がる所は共通の値の表の半径 (`LOOK.edge.bend`) で丸め、直角の角を作らない (#3171)。 */
|
|
50
|
+
export function pathOf(points: Point[]): string {
|
|
51
|
+
const pts = points.filter((p, i) => i === 0 || p[0] !== (points[i - 1] as Point)[0] || p[1] !== (points[i - 1] as Point)[1]);
|
|
52
|
+
const [x0, y0] = pts[0] as Point;
|
|
53
|
+
let d = `M ${r1(x0)} ${r1(y0)}`;
|
|
54
|
+
for (let i = 1; i < pts.length - 1; i++) {
|
|
55
|
+
const [ax, ay] = pts[i - 1] as Point;
|
|
56
|
+
const [bx, by] = pts[i] as Point;
|
|
57
|
+
const [cx, cy] = pts[i + 1] as Point;
|
|
58
|
+
const l1 = Math.hypot(bx - ax, by - ay);
|
|
59
|
+
const l2 = Math.hypot(cx - bx, cy - by);
|
|
60
|
+
const a = Math.min(LOOK.edge.bend, l1 / 2, l2 / 2);
|
|
61
|
+
d += ` L ${r1(bx - ((bx - ax) / l1) * a)} ${r1(by - ((by - ay) / l1) * a)} Q ${r1(bx)} ${r1(by)} ${r1(bx + ((cx - bx) / l2) * a)} ${r1(by + ((cy - by) / l2) * a)}`;
|
|
62
|
+
}
|
|
63
|
+
if (pts.length > 1) {
|
|
64
|
+
const [lx, ly] = pts[pts.length - 1] as Point;
|
|
65
|
+
d += ` L ${r1(lx)} ${r1(ly)}`;
|
|
66
|
+
}
|
|
67
|
+
return d;
|
|
68
|
+
}
|
|
26
69
|
|
|
27
70
|
/** 矢じり。線の最後の 2 点の向きに開く。 */
|
|
28
71
|
export function arrowHead(points: Point[]): string {
|
|
@@ -95,11 +138,14 @@ function cardFrame(box: SceneBox, ctx: Ctx, act: string | undefined, y: number,
|
|
|
95
138
|
return `${act ? `<g class="dg-a" style="${act}">${rect("dg-halo")}</g>` : ""}${rect("dg-st", ` ${ctx.sh}`)}${act ? `<g class="dg-a" style="${act}">${rect("dg-on")}</g>` : ""}`;
|
|
96
139
|
}
|
|
97
140
|
|
|
98
|
-
/**
|
|
99
|
-
|
|
141
|
+
/**
|
|
142
|
+
* 箱。動く段は縁に色を付け、外側に淡い光を足す。下の小さな字は起きたことに合わせて書き換わる。
|
|
143
|
+
* `lit` を渡すと、動く段ではなくその間だけ光らせる (状態遷移図の、運ぶ物が乗っている状態)。
|
|
144
|
+
*/
|
|
145
|
+
export function card(box: SceneBox, ctx: Ctx, on: number[], changes: SubChange[], lit?: Span[]): string {
|
|
100
146
|
const { icon, iconRadius, iconScale, iconY, nameY, subY } = LOOK.box;
|
|
101
147
|
const subs = subLines(box, ctx.m, changes, box.cy + subY);
|
|
102
|
-
const act = on.length > 0 ? activeKf(ctx, on) : undefined;
|
|
148
|
+
const act = lit ? (lit.length > 0 ? rangeKf(ctx.m, lit.map((s) => [s.start, s.end])) : undefined) : on.length > 0 ? activeKf(ctx, on) : undefined;
|
|
103
149
|
const mark =
|
|
104
150
|
box.icon && ICONS[box.icon]
|
|
105
151
|
? `<rect class="dg-ic" x="${r1(box.cx - icon / 2)}" y="${r1(box.cy + iconY - icon / 2)}" width="${icon}" height="${icon}" rx="${iconRadius}"/>${glyph(box.icon, box.cx, box.cy + iconY, iconScale)}`
|
|
@@ -121,6 +167,59 @@ export function headCard(box: SceneBox, ctx: Ctx, on: number[], changes: SubChan
|
|
|
121
167
|
return `<g style="${cs(box.color)}">${cardFrame(box, ctx, act, pad, h)}<text class="dg-sn" x="${r1(box.cx)}" y="${r1(pad + nameY)}" text-anchor="middle">${esc(box.name)}</text>${subs}</g>`;
|
|
122
168
|
}
|
|
123
169
|
|
|
170
|
+
/** 状態遷移図の、どの段も辿り着かない状態。点線の縁の札で、絵柄を左に、名前と下の字を右に並べる (#3171)。光らず、字も書き換わらない */
|
|
171
|
+
export function ghostCard(box: SceneBox): string {
|
|
172
|
+
const { icon, iconRadius, iconScale, radius } = LOOK.box;
|
|
173
|
+
const G = LOOK.state.ghost;
|
|
174
|
+
const ix = box.x + G.pad + icon / 2;
|
|
175
|
+
const tx = box.x + G.pad + icon + G.gap;
|
|
176
|
+
const mark = box.icon && ICONS[box.icon] ? `<rect class="dg-ic" x="${r1(ix - icon / 2)}" y="${r1(box.cy - icon / 2)}" width="${icon}" height="${icon}" rx="${iconRadius}"/>${glyph(box.icon, ix, box.cy, iconScale)}` : "";
|
|
177
|
+
const nameY = box.sub ? G.nameY : LOOK.baseline.mark;
|
|
178
|
+
const sub = box.sub ? `<text class="dg-ss" x="${r1(tx)}" y="${r1(box.cy + G.subY)}" text-anchor="start">${esc(box.sub)}</text>` : "";
|
|
179
|
+
return (
|
|
180
|
+
`<g data-ghost="${esc(box.id)}" style="${cs(box.color)}"><rect class="dg-ghost" x="${r1(box.x)}" y="${r1(box.y)}" width="${r1(box.w)}" height="${box.h}" rx="${radius}"/>` +
|
|
181
|
+
`${mark}<text class="dg-sn" x="${r1(tx)}" y="${r1(box.cy + nameY)}" text-anchor="start">${esc(box.name)}</text>${sub}</g>`
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** 状態遷移図の、出た状態の右上の済んだ印。`at` に弾んで出て、繰り返す前に消える */
|
|
186
|
+
export function doneMark(box: SceneBox, at: number, ctx: Ctx): string {
|
|
187
|
+
const { r, inset } = LOOK.state.done;
|
|
188
|
+
const x = box.x + box.w - inset;
|
|
189
|
+
const y = box.y + inset;
|
|
190
|
+
return `<g class="dg-popc dg-a" data-done="${esc(box.id)}" style="${cs(box.color)};${ctx.m.pop(at)}"><circle class="dg-done" cx="${r1(x)}" cy="${r1(y)}" r="${r}"/>${glyph("check", x, y, 0.5, "dg-done-ck")}</g>`;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** 状態遷移図の通らない道。点線で道と向きだけを見せ、運ぶ物を走らせない */
|
|
194
|
+
export function path(p: ScenePath, color: ColorKey): string {
|
|
195
|
+
const label = p.label
|
|
196
|
+
? `<text class="dg-path-t${p.label.family === "sans" ? " dg-path-tj" : ""}" x="${r1(p.label.x + LOOK.label.padX)}" y="${r1(p.label.y + p.label.h / 2 + LOOK.baseline.label)}">${esc(p.label.text)}</text>`
|
|
197
|
+
: "";
|
|
198
|
+
return `<g data-path="${esc(`${p.from}->${p.to}`)}" style="${cs(color)}"><path class="dg-path" d="${pathOf(p.points)}"/><path class="dg-path-h" d="${arrowHead(p.points)}"/>${label}</g>`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** 状態遷移図の脇道の囲み。左上に囲みの名前を、その下に添え書きを 1 行ずつ置く */
|
|
202
|
+
export function side(s: SceneSide, color: ColorKey): string {
|
|
203
|
+
const { pad } = LOOK.state.side;
|
|
204
|
+
const title = s.y + pad + TYPE.regionName.size;
|
|
205
|
+
const notes = s.notes.map((note, k) => `<text class="dg-side-n" x="${r1(s.x + pad)}" y="${r1(title + LOOK.region.line * (k + 1))}">${esc(note)}</text>`).join("");
|
|
206
|
+
return (
|
|
207
|
+
`<g style="${cs(color)}"><rect class="dg-side" x="${r1(s.x + 0.5)}" y="${r1(s.y + 0.5)}" width="${r1(s.w - 1)}" height="${r1(s.h - 1)}" rx="${LOOK.region.radius}"/>` +
|
|
208
|
+
`<text class="dg-side-t" x="${r1(s.x + pad)}" y="${r1(title)}">${esc(s.title)}</text>${notes}</g>`
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** 状態遷移図の始まりと終わりの印。始まりは塗った丸、終わりは輪の中の丸で、脇道の中の終わりは点線の輪 */
|
|
213
|
+
export function endMark(e: SceneEnd, color: ColorKey): string {
|
|
214
|
+
const { end } = LOOK.state;
|
|
215
|
+
const text = e.text === "" ? "" : `<text class="dg-mark" x="${r1(e.x + LOOK.state.text)}" y="${r1(e.y + LOOK.baseline.mark)}">${esc(e.text)}</text>`;
|
|
216
|
+
const x = r1(e.x);
|
|
217
|
+
const y = r1(e.y);
|
|
218
|
+
if (e.kind === "start") return `<g data-end="start"><circle class="dg-start" cx="${x}" cy="${y}" r="${e.r}"/>${text}</g>`;
|
|
219
|
+
if (e.ghost) return `<g data-end="finish" style="${cs(color)}"><circle class="dg-end-x" cx="${x}" cy="${y}" r="${e.r}"/><circle class="dg-end-xi" cx="${x}" cy="${y}" r="${end.inner}"/>${text}</g>`;
|
|
220
|
+
return `<g data-end="finish"><circle class="dg-end" cx="${x}" cy="${y}" r="${e.r}"/><circle class="dg-end-in" cx="${x}" cy="${y}" r="${end.inner}"/>${text}</g>`;
|
|
221
|
+
}
|
|
222
|
+
|
|
124
223
|
export function lifeline(x: number, y1: number, y2: number): string {
|
|
125
224
|
return `<line class="dg-ll" x1="${r1(x)}" y1="${r1(y1)}" x2="${r1(x)}" y2="${r1(y2)}"/>`;
|
|
126
225
|
}
|
|
@@ -128,20 +227,27 @@ export function lifeline(x: number, y1: number, y2: number): string {
|
|
|
128
227
|
/**
|
|
129
228
|
* 線。点線で道を見せておき、その段で色の線を引いて、段の終わりまで流れる点で向きを見せる。
|
|
130
229
|
* 済んだ段は薄く残し、最後の段の後に通った道を全て濃くし直す。
|
|
230
|
+
* `past` を立てると (状態遷移図、#3171)、段が終わった後は色の線を消して灰の実線に替え、最後の段まで残す。
|
|
131
231
|
*/
|
|
132
|
-
export function edge(e: SceneEdge, time: EdgeTime, ctx: Ctx): { base: string; lit: string } {
|
|
232
|
+
export function edge(e: SceneEdge, time: EdgeTime, ctx: Ctx, past = false): { base: string; lit: string } {
|
|
133
233
|
const { m } = ctx;
|
|
134
234
|
const d = pathOf(e.points);
|
|
135
235
|
const head = arrowHead(e.points);
|
|
136
236
|
const { start: a, drawn: b, end } = time;
|
|
137
|
-
const tail: Frame[] =
|
|
138
|
-
[
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
237
|
+
const tail: Frame[] = past
|
|
238
|
+
? [
|
|
239
|
+
[end, { opacity: 1 }, EASE.inOut],
|
|
240
|
+
[end + 0.4, { opacity: 0 }],
|
|
241
|
+
[m.total, { opacity: 0 }],
|
|
242
|
+
]
|
|
243
|
+
: [
|
|
244
|
+
[end, { opacity: 1 }, EASE.inOut],
|
|
245
|
+
[end + 0.4, { opacity: 0.45 }],
|
|
246
|
+
[ctx.end + 0.3, { opacity: 0.45 }, EASE.inOut],
|
|
247
|
+
[ctx.end + 1, { opacity: 0.95 }],
|
|
248
|
+
[m.last, { opacity: 0.95 }, EASE.inOut],
|
|
249
|
+
[m.total, { opacity: 0 }],
|
|
250
|
+
];
|
|
145
251
|
const line = m.kf([
|
|
146
252
|
[0, { opacity: 0, "stroke-dashoffset": 1 }],
|
|
147
253
|
[a, { opacity: 0, "stroke-dashoffset": 1 }, EASE.inOut],
|
|
@@ -161,17 +267,21 @@ export function edge(e: SceneEdge, time: EdgeTime, ctx: Ctx): { base: string; li
|
|
|
161
267
|
[end + 0.2, { opacity: 0, "stroke-dashoffset": run }],
|
|
162
268
|
[m.total, { opacity: 0, "stroke-dashoffset": run }],
|
|
163
269
|
]);
|
|
270
|
+
const grey = past ? m.kf([[0, { opacity: 0 }], [end, { opacity: 0 }, EASE.inOut], [end + 0.4, { opacity: 1 }], [m.last, { opacity: 1 }, EASE.inOut], [m.total, { opacity: 0 }]]) : "";
|
|
164
271
|
return {
|
|
165
|
-
base: `<path class="dg-e" d="${d}"/><path class="dg-eh" d="${head}"
|
|
272
|
+
base: `<path class="dg-e" d="${d}"/><path class="dg-eh" d="${head}"/>${past ? `<path class="dg-e-past dg-a" d="${d}" style="${grey}"/><path class="dg-eh-past dg-a" d="${head}" style="${grey}"/>` : ""}`,
|
|
166
273
|
lit: `<g style="${cs(e.color)}"><path class="dg-e-on dg-a" pathLength="1" d="${d}" style="${line}"/><path class="dg-eh-on dg-a" d="${head}" style="${hk}"/><path class="dg-flow dg-a" d="${d}" style="${flow}"/></g>`,
|
|
167
274
|
};
|
|
168
275
|
}
|
|
169
276
|
|
|
170
|
-
/**
|
|
277
|
+
/** 線の名前の札。その段の間だけ出し、段の終わりまでに消え終わる。時刻の表が `after` を持つ時は、その間だけ札を外した灰の字で薄く残す (#3171)。 */
|
|
171
278
|
export function label(l: SceneLabel, time: LabelTime, ctx: Ctx): string {
|
|
172
279
|
const ja = l.family === "sans" ? " dg-capj" : "";
|
|
173
280
|
const { radius } = LOOK.label;
|
|
174
|
-
|
|
281
|
+
const x = r1(l.x + l.w / 2);
|
|
282
|
+
const y = r1(l.y + l.h / 2 + LOOK.baseline.label);
|
|
283
|
+
const past = time.after ? `<g class="dg-cap-past${l.family === "sans" ? " dg-cap-pastj" : ""} dg-a" style="${ctx.m.vis(time.after.start, time.after.end)}"><text x="${x}" y="${y}" text-anchor="middle">${esc(l.text)}</text></g>` : "";
|
|
284
|
+
return `${past}<g class="dg-cap${ja} dg-a" style="${cs(l.color)};${ctx.m.rise(time.start, time.end, true)}"><rect x="${r1(l.x)}" y="${r1(l.y)}" width="${r1(l.w)}" height="${l.h}" rx="${radius}"/><text x="${x}" y="${y}" text-anchor="middle">${esc(l.text)}</text></g>`;
|
|
175
285
|
}
|
|
176
286
|
|
|
177
287
|
/** 時系列図の箱の中の処理の札。その段の間だけ生存線の上に出す。 */
|
|
@@ -215,8 +325,11 @@ export function carrier(e: SceneEdge, time: CarrierTime, ctx: Ctx): string {
|
|
|
215
325
|
return `<g class="dg-a" style="${m.move(pts, m.blink(from + 0.02, gone, fade))}">${carrierShape(e, ctx.sh)}</g>`;
|
|
216
326
|
}
|
|
217
327
|
|
|
218
|
-
/**
|
|
219
|
-
|
|
328
|
+
/**
|
|
329
|
+
* 段の番号。まだ来ていない段は出さず、今の段は塗り、済んだ段は縁取りに変えて番号を読めるまま残す。
|
|
330
|
+
* `grey` を立てると (状態遷移図、#3171)、済んだ段の縁取りを線と同じ灰にする。
|
|
331
|
+
*/
|
|
332
|
+
export function badge(b: SceneBadge, ctx: Ctx, grey = false): string {
|
|
220
333
|
const { m } = ctx;
|
|
221
334
|
const span = ctx.t.steps[b.step] as Span;
|
|
222
335
|
const a = span.start;
|
|
@@ -244,7 +357,7 @@ export function badge(b: SceneBadge, ctx: Ctx): string {
|
|
|
244
357
|
const no = b.step + 1;
|
|
245
358
|
return (
|
|
246
359
|
`<g class="dg-bd" data-step="${b.step}" style="${cs(b.color)}"><circle class="dg-bdr dg-popc dg-a" cx="${x}" cy="${y}" r="${rr}" style="${ring}"/>` +
|
|
247
|
-
`<g class="dg-a" style="${past}"><circle class="dg-bdp" cx="${x}" cy="${y}" r="${rr}"/><text class="dg-bdpt" x="${x}" y="${ty}" text-anchor="middle">${no}</text></g>` +
|
|
360
|
+
`<g class="dg-a" style="${past}"><circle class="${grey ? "dg-bdp-g" : "dg-bdp"}" cx="${x}" cy="${y}" r="${rr}"/><text class="${grey ? "dg-bdpt-g" : "dg-bdpt"}" x="${x}" y="${ty}" text-anchor="middle">${no}</text></g>` +
|
|
248
361
|
`<g class="dg-popc dg-a" style="${fill}"><circle class="dg-bdf" cx="${x}" cy="${y}" r="${r}"/><text class="dg-bdft" x="${x}" y="${ty}" text-anchor="middle">${no}</text></g></g>`
|
|
249
362
|
);
|
|
250
363
|
}
|
|
@@ -255,9 +368,16 @@ export interface FieldChange {
|
|
|
255
368
|
value?: string;
|
|
256
369
|
tag?: string;
|
|
257
370
|
color: ColorKey;
|
|
371
|
+
/** 変わった段と、その段の終わり */
|
|
372
|
+
step: number;
|
|
373
|
+
until: number;
|
|
258
374
|
}
|
|
259
375
|
|
|
260
|
-
|
|
376
|
+
/**
|
|
377
|
+
* 中身の帯。`now` を立てると (状態遷移図、#3171)、段の中で変わった値をその段の終わりまで線の色 (今の状態の色) で描き、
|
|
378
|
+
* 欄の名前の横に同じ色の点を置く。帯の通った状態の欄は、`arrivals` が持つ着いた時刻に点を塗る。
|
|
379
|
+
*/
|
|
380
|
+
export function strip(s: SceneStrip, changes: Map<string, FieldChange[]>, ctx: Ctx, now = false, arrivals = new Map<string, number>()): string {
|
|
261
381
|
const { m, sh } = ctx;
|
|
262
382
|
const S = LOOK.strip;
|
|
263
383
|
const my = s.y + S.h / 2;
|
|
@@ -279,8 +399,19 @@ export function strip(s: SceneStrip, changes: Map<string, FieldChange[]>, ctx: C
|
|
|
279
399
|
values.forEach((c, k) => {
|
|
280
400
|
const next = values[k + 1]?.at;
|
|
281
401
|
const value = c.value as string;
|
|
282
|
-
const
|
|
283
|
-
|
|
402
|
+
const cls = hasWide(value) ? "dg-fvj" : "dg-fv";
|
|
403
|
+
const textOf = (c: string): string => `<text class="${c}" x="${r1(field.x)}" y="${r1(field.y + S.valueY)}">${esc(value)}</text>`;
|
|
404
|
+
const text = textOf(cls);
|
|
405
|
+
if (!now) {
|
|
406
|
+
const anim = next === undefined ? m.rise(c.at) : m.rise(c.at, next - MOTION.untilFade);
|
|
407
|
+
parts.push(`<g class="dg-a" style="${anim}">${text}</g>`);
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
// 変わった段の間は今の状態の色で、段が終わると墨に戻す
|
|
411
|
+
const until = Math.min(c.until, next ?? c.until);
|
|
412
|
+
const dot = `<circle class="dg-fl-now" cx="${r1(field.x + textWidth(field.name, TYPE.fieldName) + S.now.gap)}" cy="${r1(field.y + S.nameY - TYPE.fieldName.size / 3)}" r="${S.now.r}"/>`;
|
|
413
|
+
parts.push(`<g class="dg-fv-now dg-a" data-step="${c.step}" style="${cs(c.color)};${m.rise(c.at, until - MOTION.untilFade)}">${textOf(`${cls} dg-fv-c`)}${dot}</g>`);
|
|
414
|
+
if (next === undefined || next > until) parts.push(`<g class="dg-a" style="${m.vis(until - MOTION.untilFade, next === undefined ? m.last : next - MOTION.untilFade)}">${text}</g>`);
|
|
284
415
|
});
|
|
285
416
|
tags.forEach((c, k) => {
|
|
286
417
|
const next = tags[k + 1]?.at;
|
|
@@ -295,5 +426,21 @@ export function strip(s: SceneStrip, changes: Map<string, FieldChange[]>, ctx: C
|
|
|
295
426
|
);
|
|
296
427
|
});
|
|
297
428
|
});
|
|
429
|
+
if (s.trail) {
|
|
430
|
+
const t = s.trail;
|
|
431
|
+
const last = s.fields[s.fields.length - 1];
|
|
432
|
+
if (last && last.y === t.y) {
|
|
433
|
+
const sx = t.x - S.fieldGap / 2;
|
|
434
|
+
parts.push(`<line class="dg-sep" x1="${r1(sx)}" y1="${r1(t.y + S.sepInset)}" x2="${r1(sx)}" y2="${r1(t.y + S.h - S.sepInset)}"/>`);
|
|
435
|
+
}
|
|
436
|
+
parts.push(`<text class="dg-fl" x="${r1(t.x)}" y="${r1(t.y + S.nameY)}">${esc(t.title)}</text>`);
|
|
437
|
+
const dy = t.y + S.valueY - TYPE.fieldValue.size / 3;
|
|
438
|
+
t.states.forEach((state, k) => {
|
|
439
|
+
const cx = r1(t.x + S.trail.r + k * S.trail.step);
|
|
440
|
+
const at = arrivals.get(state.box);
|
|
441
|
+
const fill = at === undefined ? "" : `<circle class="dg-tr-fill dg-a" cx="${cx}" cy="${r1(dy)}" r="${S.trail.r}" style="${m.pop(at)}"/>`;
|
|
442
|
+
parts.push(`<g data-trail="${esc(state.box)}" style="${cs(state.color)}"><circle class="dg-tr-ring" cx="${cx}" cy="${r1(dy)}" r="${S.trail.r}"/>${fill}</g>`);
|
|
443
|
+
});
|
|
444
|
+
}
|
|
298
445
|
return parts.join("");
|
|
299
446
|
}
|
package/src/render/svg.ts
CHANGED
|
@@ -10,13 +10,35 @@ import { layoutArchitecture } from "../layout/architecture";
|
|
|
10
10
|
import { textWidth } from "../layout/measure";
|
|
11
11
|
import type { Layout, Rect } from "../layout/scene";
|
|
12
12
|
import { layoutSequence } from "../layout/sequence";
|
|
13
|
+
import { layoutState } from "../layout/state";
|
|
13
14
|
import { FONTS } from "../look/fonts";
|
|
14
15
|
import { type ColorKey, EASE, FONT_VARS, LOOK, MOTION, type Palette, paletteVars, resolvePalette, type TextStyle, type Theme, TYPE } from "../look/values";
|
|
15
16
|
import { readDiagram } from "../version";
|
|
16
17
|
import type { Diagram, ViewMode } from "../schema";
|
|
17
18
|
import { buildTimeline, type Span, type Timeline } from "../timeline";
|
|
18
19
|
import type { NotationLang } from "../words";
|
|
19
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
action,
|
|
22
|
+
badge,
|
|
23
|
+
band,
|
|
24
|
+
card,
|
|
25
|
+
carrier,
|
|
26
|
+
type Ctx,
|
|
27
|
+
cs,
|
|
28
|
+
doneMark,
|
|
29
|
+
edge,
|
|
30
|
+
endMark,
|
|
31
|
+
esc,
|
|
32
|
+
type FieldChange,
|
|
33
|
+
ghostCard,
|
|
34
|
+
headCard,
|
|
35
|
+
label,
|
|
36
|
+
lifeline,
|
|
37
|
+
path,
|
|
38
|
+
side,
|
|
39
|
+
strip,
|
|
40
|
+
type SubChange,
|
|
41
|
+
} from "./parts";
|
|
20
42
|
import { Motion } from "./motion";
|
|
21
43
|
|
|
22
44
|
export interface RenderOptions {
|
|
@@ -43,6 +65,8 @@ const font = (t: TextStyle): string => `font:${t.weight} ${t.size}px var(--${t.f
|
|
|
43
65
|
const mix = (pct: number, base = "var(--surf)"): string => `color-mix(in srgb,var(--c) ${pct}%,${base})`;
|
|
44
66
|
const S = LOOK.stroke;
|
|
45
67
|
const X = LOOK.mix;
|
|
68
|
+
/** 状態遷移図で段が終わった線と番号の灰 (#3171) */
|
|
69
|
+
const PAST = `color-mix(in srgb,var(--mute) ${X.past}%,var(--bg))`;
|
|
46
70
|
|
|
47
71
|
/** 部品の見た目。どの値も値の表から読む。 */
|
|
48
72
|
const PART_CSS = [
|
|
@@ -69,15 +93,21 @@ const PART_CSS = [
|
|
|
69
93
|
`.dg-e-on{fill:none;stroke:var(--c);stroke-width:${S.edgeOn};stroke-linecap:round;stroke-dasharray:1 2}`,
|
|
70
94
|
`.dg-eh-on{fill:none;stroke:var(--c);stroke-width:${S.edgeOn};stroke-linecap:round;stroke-linejoin:round}`,
|
|
71
95
|
`.dg-flow{fill:none;stroke:var(--surf);stroke-width:${S.flow};stroke-linecap:round;stroke-dasharray:1.5 ${LOOK.edge.flowDash}}`,
|
|
96
|
+
`.dg-e-past{fill:none;stroke:${PAST};stroke-width:${S.edge};stroke-linecap:round}`,
|
|
97
|
+
`.dg-eh-past{fill:none;stroke:${PAST};stroke-width:${S.arrow};stroke-linecap:round;stroke-linejoin:round}`,
|
|
72
98
|
".dg-bd{cursor:pointer}",
|
|
73
99
|
".dg-bdf{fill:var(--c)}",
|
|
74
100
|
`.dg-bdft{${font(TYPE.badge)};fill:var(--on-c)}`,
|
|
75
101
|
`.dg-bdr{fill:none;stroke:var(--c);stroke-width:${S.badgeRing}}`,
|
|
76
102
|
`.dg-bdp{fill:${mix(X.badgePast)};stroke:${mix(X.badgePastEdge, "transparent")};stroke-width:${S.hair}}`,
|
|
77
103
|
`.dg-bdpt{${font(TYPE.badge)};fill:var(--ct)}`,
|
|
104
|
+
`.dg-bdp-g{fill:var(--surf);stroke:${PAST};stroke-width:${S.hair}}`,
|
|
105
|
+
`.dg-bdpt-g{${font(TYPE.badge)};fill:var(--mute)}`,
|
|
78
106
|
`.dg-cap rect,.dg-act rect{fill:${mix(X.cap)};stroke:${mix(X.capEdge, "transparent")}}`,
|
|
79
107
|
`.dg-cap text{${font(TYPE.label)};fill:var(--ct)}`,
|
|
80
108
|
`.dg-capj text{${font(TYPE.labelJa)}}`,
|
|
109
|
+
`.dg-cap-past text{${font(TYPE.label)};fill:var(--mute);fill-opacity:${LOOK.state.pastLabel}}`,
|
|
110
|
+
`.dg-cap-pastj text{${font(TYPE.labelJa)}}`,
|
|
81
111
|
`.dg-act text{${font(TYPE.action)};fill:var(--ct)}`,
|
|
82
112
|
".dg-car{fill:var(--carry,var(--ink))}",
|
|
83
113
|
`.dg-car-t{${font(TYPE.carrier)};fill:var(--bg)}`,
|
|
@@ -92,8 +122,28 @@ const PART_CSS = [
|
|
|
92
122
|
`.dg-fl{${font(TYPE.fieldName)};fill:var(--mute)}`,
|
|
93
123
|
`.dg-fv{${font(TYPE.fieldValue)};fill:var(--ink)}`,
|
|
94
124
|
`.dg-fvj{${font(TYPE.fieldValueJa)};fill:var(--ink)}`,
|
|
125
|
+
".dg-fv-c{fill:var(--ct)}",
|
|
126
|
+
".dg-fl-now{fill:var(--c)}",
|
|
127
|
+
`.dg-tr-ring{fill:none;stroke:var(--line);stroke-width:${S.trail}}`,
|
|
128
|
+
".dg-tr-fill{fill:var(--c)}",
|
|
95
129
|
".dg-tg rect{fill:var(--c)}",
|
|
96
130
|
`.dg-tg text{${font(TYPE.tag)};fill:var(--on-c)}`,
|
|
131
|
+
`.dg-side{fill:color-mix(in srgb,var(--c) ${X.side}%,var(--bg));stroke:${mix(X.sideEdge, "transparent")};stroke-dasharray:4 4}`,
|
|
132
|
+
`.dg-side-t{${font(TYPE.regionName)};fill:var(--ct)}`,
|
|
133
|
+
`.dg-side-n{${font(TYPE.regionNote)};fill:var(--mute)}`,
|
|
134
|
+
`.dg-ghost{fill:var(--surf);stroke:${mix(X.ghostEdge, "transparent")};stroke-dasharray:4 3}`,
|
|
135
|
+
`.dg-path{fill:none;stroke:${mix(X.path, "transparent")};stroke-width:${S.path};stroke-linecap:round;stroke-dasharray:0 ${LOOK.edge.ghostDash}}`,
|
|
136
|
+
`.dg-path-h{fill:none;stroke:${mix(X.path, "transparent")};stroke-width:${S.arrow};stroke-linecap:round;stroke-linejoin:round}`,
|
|
137
|
+
`.dg-path-t{${font(TYPE.label)};fill:var(--ct)}`,
|
|
138
|
+
`.dg-path-tj{${font(TYPE.labelJa)}}`,
|
|
139
|
+
".dg-start{fill:var(--ink)}",
|
|
140
|
+
`.dg-end{fill:var(--bg);stroke:var(--ink);stroke-width:${S.end}}`,
|
|
141
|
+
".dg-end-in{fill:var(--ink)}",
|
|
142
|
+
`.dg-end-x{fill:var(--bg);stroke:${mix(X.path, "transparent")};stroke-width:${S.arrow};stroke-dasharray:2 2.5}`,
|
|
143
|
+
`.dg-end-xi{fill:${mix(X.path, "transparent")}}`,
|
|
144
|
+
`.dg-mark{${font(TYPE.regionNote)};fill:var(--mute)}`,
|
|
145
|
+
`.dg-done{fill:var(--ink);stroke:var(--surf);stroke-width:${S.done}}`,
|
|
146
|
+
`.dg-done-ck{fill:none;stroke:var(--surf);stroke-width:${S.glyph};stroke-linecap:round;stroke-linejoin:round}`,
|
|
97
147
|
".dg-vp-bg{fill:var(--bg)}",
|
|
98
148
|
".dg-vp-frame{fill:none;stroke:var(--hair)}",
|
|
99
149
|
".dg-hd-bg{fill:var(--bg)}",
|
|
@@ -181,7 +231,8 @@ function fieldChanges(diagram: Diagram, timeline: Timeline, layout: Layout): Map
|
|
|
181
231
|
if (event?.type !== "strip") continue;
|
|
182
232
|
const color: ColorKey = layout.badges[time.step]?.color ?? "slate";
|
|
183
233
|
const list = out.get(event.field) ?? [];
|
|
184
|
-
|
|
234
|
+
const until = (timeline.steps[time.step] as Span).end;
|
|
235
|
+
list.push({ at: time.at, color, step: time.step, until, ...(event.value !== undefined ? { value: event.value } : {}), ...(event.tag !== undefined ? { tag: event.tag } : {}) });
|
|
185
236
|
out.set(event.field, list);
|
|
186
237
|
}
|
|
187
238
|
return out;
|
|
@@ -203,21 +254,40 @@ export function drawSvg(diagram: Diagram, layout: Layout, timeline: Timeline, pa
|
|
|
203
254
|
const m = new Motion(`dg${id}`, timeline.total, timeline.last);
|
|
204
255
|
const ctx: Ctx = { m, t: timeline, sh: `filter="url(#dg-sh-${id})"`, end: (timeline.steps[timeline.steps.length - 1] as Span).end };
|
|
205
256
|
const on = involved(diagram);
|
|
206
|
-
|
|
257
|
+
// 時系列図の処理は生存線の上の札に出し、他の図の種類は箱の下の字に出す
|
|
258
|
+
const subs = subChanges(diagram, timeline, layout.kind !== "sequence");
|
|
207
259
|
|
|
208
260
|
const L = { band: [] as string[], base: [] as string[], lit: [] as string[], node: [] as string[], badge: [] as string[], top: [] as string[] };
|
|
209
261
|
for (const region of layout.regions) L.band.push(band(region));
|
|
210
|
-
|
|
262
|
+
const ghostColor = layout.boxes.find((b) => b.ghost)?.color ?? "slate";
|
|
263
|
+
if (layout.side) L.band.push(side(layout.side, ghostColor));
|
|
264
|
+
// 脇道へ入る道は脇道の状態の色、それ以外の道は送る側の色で描く
|
|
265
|
+
const boxColor = (id: string, ghost?: boolean): ColorKey | undefined => layout.boxes.find((b) => b.id === id && (ghost === undefined || b.ghost === ghost))?.color;
|
|
266
|
+
for (const p of layout.paths) L.base.push(path(p, boxColor(p.to, true) ?? boxColor(p.from) ?? ghostColor));
|
|
267
|
+
for (const e of layout.ends) L.node.push(endMark(e, ghostColor));
|
|
268
|
+
// 状態遷移図は、段で変わった値を今の状態の色で描き、帯の通った状態の点を着いた時刻に塗る (#3171)
|
|
269
|
+
const isState = layout.kind === "state";
|
|
270
|
+
const arrivals = new Map((timeline.states ?? []).flatMap((s) => (s.lit[0] ? [[s.box, s.lit[0].start] as [string, number]] : [])));
|
|
271
|
+
if (layout.strip) L.band.push(strip(layout.strip, fieldChanges(diagram, timeline, layout), ctx, isState, arrivals));
|
|
211
272
|
for (const line of layout.lifelines) L.base.push(lifeline(line.x, line.y1, line.y2));
|
|
212
273
|
for (const e of layout.edges) {
|
|
213
274
|
const time = timeline.edges.find((t) => t.step === e.step && t.event === e.event);
|
|
214
275
|
if (!time) continue;
|
|
215
|
-
const drawn = edge(e, time, ctx);
|
|
276
|
+
const drawn = edge(e, time, ctx, isState);
|
|
216
277
|
L.base.push(drawn.base);
|
|
217
278
|
L.lit.push(drawn.lit);
|
|
218
279
|
}
|
|
219
|
-
|
|
220
|
-
for (const
|
|
280
|
+
const states = new Map((timeline.states ?? []).map((s) => [s.box, s]));
|
|
281
|
+
for (const box of layout.boxes) {
|
|
282
|
+
if (box.ghost) {
|
|
283
|
+
L.node.push(ghostCard(box));
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
const state = states.get(box.id);
|
|
287
|
+
L.node.push(card(box, ctx, on.get(box.id) ?? [], subs.get(box.id) ?? [], isState ? (state?.lit ?? []) : undefined));
|
|
288
|
+
if (state?.done !== undefined) L.node.push(doneMark(box, state.done, ctx));
|
|
289
|
+
}
|
|
290
|
+
for (const b of layout.badges) L.badge.push(badge(b, ctx, isState));
|
|
221
291
|
for (const l of layout.labels) {
|
|
222
292
|
const time = timeline.labels.find((t) => t.step === l.step && t.event === l.event);
|
|
223
293
|
if (time) L.top.push(label(l, time, ctx));
|
|
@@ -421,7 +491,7 @@ export function renderDiagram(input: unknown, options: RenderOptions = {}): Rend
|
|
|
421
491
|
const diagram = read.diagram;
|
|
422
492
|
const palette = resolvePalette(options.palette);
|
|
423
493
|
const theme: Theme = options.theme === "dark" ? "dark" : "light";
|
|
424
|
-
const layout = diagram.kind === "sequence" ? layoutSequence(diagram) : layoutArchitecture(diagram);
|
|
494
|
+
const layout = diagram.kind === "sequence" ? layoutSequence(diagram) : diagram.kind === "state" ? layoutState(diagram, lang) : layoutArchitecture(diagram);
|
|
425
495
|
const timeline = buildTimeline(diagram);
|
|
426
496
|
return {
|
|
427
497
|
svg: drawSvg(diagram, layout, timeline, palette, theme),
|
package/src/schema.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import type { ErrorObject } from "ajv";
|
|
13
13
|
import { type Allowed, FIELD_MESSAGES, type FieldMessages, type Forbidden } from "./field-messages";
|
|
14
14
|
import validate from "./schemas/validate-diagram.js";
|
|
15
|
-
import type
|
|
15
|
+
import { type NotationLang, STATE_ENDS } from "./words";
|
|
16
16
|
|
|
17
17
|
export type Color = "blue" | "amber" | "green" | "violet" | "rose" | "slate" | "coin";
|
|
18
18
|
/** 色の役の一覧。記法の `color:` に書ける値と、誤りの文に並べる値 */
|
|
@@ -48,6 +48,13 @@ export type DiagramEvent =
|
|
|
48
48
|
| { type: "swap"; box: string; text: string }
|
|
49
49
|
| { type: "strip"; field: string; value?: string; tag?: string };
|
|
50
50
|
|
|
51
|
+
/** 状態遷移図の、最初の段より前に書いた移れる道。`from` に `start`、`to` に `finish` を書ける */
|
|
52
|
+
export interface StatePath {
|
|
53
|
+
from: string;
|
|
54
|
+
to: string;
|
|
55
|
+
label?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
51
58
|
export interface Step {
|
|
52
59
|
title: string;
|
|
53
60
|
body: string;
|
|
@@ -57,7 +64,7 @@ export interface Step {
|
|
|
57
64
|
|
|
58
65
|
export interface Diagram {
|
|
59
66
|
version: number;
|
|
60
|
-
kind: "sequence" | "architecture";
|
|
67
|
+
kind: "sequence" | "architecture" | "state";
|
|
61
68
|
title: string;
|
|
62
69
|
view?: ViewMode;
|
|
63
70
|
regions?: Region[];
|
|
@@ -65,6 +72,7 @@ export interface Diagram {
|
|
|
65
72
|
strip?: { title: string; fields: string[] };
|
|
66
73
|
steps: Step[];
|
|
67
74
|
sequence?: { lifelines: string[] };
|
|
75
|
+
state?: { paths: StatePath[] };
|
|
68
76
|
}
|
|
69
77
|
|
|
70
78
|
export interface FieldError {
|
|
@@ -84,14 +92,21 @@ export interface CheckOptions {
|
|
|
84
92
|
const EVENT_TYPES = ["edge", "action", "swap", "strip"] as const satisfies readonly DiagramEvent["type"][];
|
|
85
93
|
|
|
86
94
|
/**
|
|
87
|
-
* schema の書けない組み合わせ (`
|
|
88
|
-
*
|
|
95
|
+
* schema の書けない組み合わせ (`not: { required: [欄] }`) の欄。根の 3 か所は決まりの場所で引き、運ぶ物は誤りの場所の最後の欄で引く。
|
|
96
|
+
* 根の `sequence` と運ぶ物の `text` は ajv の誤りでは決まりの場所 (`#/else/not`) が同じになるので、誤りの場所でも分ける
|
|
89
97
|
*/
|
|
98
|
+
const ROOT_NOT_ALLOWED: Record<string, Allowed> = {
|
|
99
|
+
"#/else/not": { field: "sequence", kind: "sequence" },
|
|
100
|
+
"#/then/not": { field: "state", kind: "state" },
|
|
101
|
+
"#/else/else/not": { field: "state", kind: "state" },
|
|
102
|
+
};
|
|
90
103
|
const NOT_ALLOWED: Record<string, Allowed> = {
|
|
91
|
-
"": { field: "sequence", kind: "sequence" },
|
|
92
104
|
carry: { field: "text", kind: "tag" },
|
|
93
105
|
};
|
|
94
106
|
|
|
107
|
+
/** 状態遷移図の箱の名札に予約の名前を書いた時の決まりの場所 */
|
|
108
|
+
const RESERVED_ID = /\/boxes\/items\/properties\/id\/not$/u;
|
|
109
|
+
|
|
95
110
|
const escape = (key: string): string => key.replaceAll("~", "~0").replaceAll("/", "~1");
|
|
96
111
|
|
|
97
112
|
/** `^[^…]+$` の形の決まりから書けない字を読む。違う形の決まりでは `undefined` */
|
|
@@ -124,7 +139,8 @@ function toFieldError(error: ErrorObject, errors: readonly ErrorObject[], m: Fie
|
|
|
124
139
|
return { pointer: `${at}/${escape(key)}`, message: m.required(key) };
|
|
125
140
|
}
|
|
126
141
|
case "not": {
|
|
127
|
-
|
|
142
|
+
if (RESERVED_ID.test(error.schemaPath)) return { pointer: at, message: m.reserved(STATE_ENDS) };
|
|
143
|
+
const allowed = at === "" ? ROOT_NOT_ALLOWED[error.schemaPath] : NOT_ALLOWED[at.slice(at.lastIndexOf("/") + 1)];
|
|
128
144
|
return { pointer: allowed ? `${at}/${allowed.field}` : at, message: m.not(allowed) };
|
|
129
145
|
}
|
|
130
146
|
case "anyOf": {
|
|
@@ -6,18 +6,24 @@
|
|
|
6
6
|
"required": ["version", "kind", "title", "boxes", "steps"],
|
|
7
7
|
"properties": {
|
|
8
8
|
"version": { "description": "形式の版", "type": "integer", "minimum": 1 },
|
|
9
|
-
"kind": { "description": "図の種類", "enum": ["sequence", "architecture"] },
|
|
9
|
+
"kind": { "description": "図の種類", "enum": ["sequence", "architecture", "state"] },
|
|
10
10
|
"title": { "$ref": "#/definitions/text" },
|
|
11
11
|
"view": { "description": "見せ方。書かなければ字の大きさで選ぶ。fixed は全体を止めて中だけ動かし、follow は寄って動かし全体図で今どこかを示す", "enum": ["fixed", "follow"] },
|
|
12
12
|
"regions": { "type": "array", "items": { "$ref": "#/definitions/region" } },
|
|
13
13
|
"boxes": { "type": "array", "minItems": 1, "items": { "$ref": "#/definitions/box" } },
|
|
14
14
|
"strip": { "$ref": "#/definitions/strip" },
|
|
15
15
|
"steps": { "type": "array", "minItems": 1, "items": { "$ref": "#/definitions/step" } },
|
|
16
|
-
"sequence": { "$ref": "#/definitions/sequence" }
|
|
16
|
+
"sequence": { "$ref": "#/definitions/sequence" },
|
|
17
|
+
"state": { "$ref": "#/definitions/state" }
|
|
17
18
|
},
|
|
18
19
|
"if": { "required": ["kind"], "properties": { "kind": { "const": "sequence" } } },
|
|
19
|
-
"then": { "required": ["sequence"] },
|
|
20
|
-
"else": {
|
|
20
|
+
"then": { "required": ["sequence"], "not": { "required": ["state"] } },
|
|
21
|
+
"else": {
|
|
22
|
+
"not": { "required": ["sequence"] },
|
|
23
|
+
"if": { "required": ["kind"], "properties": { "kind": { "const": "state" } } },
|
|
24
|
+
"then": { "properties": { "boxes": { "items": { "properties": { "id": { "description": "始まりと終わりの印の名前は箱の名札に使えない", "not": { "enum": ["start", "finish"] } } } } } } },
|
|
25
|
+
"else": { "not": { "required": ["state"] } }
|
|
26
|
+
},
|
|
21
27
|
"definitions": {
|
|
22
28
|
"text": { "type": "string", "minLength": 1 },
|
|
23
29
|
"id": { "type": "string", "pattern": "^[^\\s\\[\\]「」=:]+$" },
|
|
@@ -145,6 +151,26 @@
|
|
|
145
151
|
"properties": {
|
|
146
152
|
"lifelines": { "type": "array", "minItems": 1, "items": { "$ref": "#/definitions/id" } }
|
|
147
153
|
}
|
|
154
|
+
},
|
|
155
|
+
"state": {
|
|
156
|
+
"description": "状態遷移図の種類の欄。最初の段より前に書いた、移れるが段で通るとは限らない線",
|
|
157
|
+
"type": "object",
|
|
158
|
+
"additionalProperties": false,
|
|
159
|
+
"required": ["paths"],
|
|
160
|
+
"properties": {
|
|
161
|
+
"paths": { "type": "array", "minItems": 1, "items": { "$ref": "#/definitions/path" } }
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
"path": {
|
|
165
|
+
"description": "移れる道。送る側に始まりの印 start、受ける側に終わりの印 finish を書ける。どの段にも出ない道は点線で描き、運ぶ物を走らせない",
|
|
166
|
+
"type": "object",
|
|
167
|
+
"additionalProperties": false,
|
|
168
|
+
"required": ["from", "to"],
|
|
169
|
+
"properties": {
|
|
170
|
+
"from": { "$ref": "#/definitions/id" },
|
|
171
|
+
"to": { "$ref": "#/definitions/id" },
|
|
172
|
+
"label": { "$ref": "#/definitions/text" }
|
|
173
|
+
}
|
|
148
174
|
}
|
|
149
175
|
}
|
|
150
176
|
}
|