@cardenelabs/dragon 0.36.3 → 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.
@@ -81,6 +81,12 @@ type DiagramEvent = {
81
81
  value?: string;
82
82
  tag?: string;
83
83
  };
84
+ /** 状態遷移図の、最初の段より前に書いた移れる道。`from` に `start`、`to` に `finish` を書ける */
85
+ interface StatePath {
86
+ from: string;
87
+ to: string;
88
+ label?: string;
89
+ }
84
90
  interface Step {
85
91
  title: string;
86
92
  body: string;
@@ -89,7 +95,7 @@ interface Step {
89
95
  }
90
96
  interface Diagram {
91
97
  version: number;
92
- kind: "sequence" | "architecture";
98
+ kind: "sequence" | "architecture" | "state";
93
99
  title: string;
94
100
  view?: ViewMode;
95
101
  regions?: Region[];
@@ -102,6 +108,9 @@ interface Diagram {
102
108
  sequence?: {
103
109
  lifelines: string[];
104
110
  };
111
+ state?: {
112
+ paths: StatePath[];
113
+ };
105
114
  }
106
115
  interface FieldError {
107
116
  /** 誤った欄の場所 (JSON Pointer) */
@@ -121,4 +130,4 @@ interface CheckOptions {
121
130
  /** 原本の JSON を検査する。版が道具より新しいかは見ない (`readDiagram` が見る)。 */
122
131
  declare function validateDiagram(input: unknown, options?: CheckOptions): Checked;
123
132
 
124
- export { type Box as B, type CheckOptions as C, type Diagram as D, type FieldError as F, type LabelSide as L, type NotationLang as N, PALETTES as P, READABLE_PX as R, type Step as S, type Theme as T, type ViewMode as V, type ColorKey as a, type Carry as b, type Checked as c, type Color as d, DEFAULT_PALETTE as e, type DiagramEvent as f, type Palette as g, type Region as h, resolvePalette as r, validateDiagram as v };
133
+ export { type Box as B, type CheckOptions as C, type Diagram as D, type FieldError as F, type LabelSide as L, type NotationLang as N, PALETTES as P, READABLE_PX as R, type StatePath as S, type Theme as T, type ViewMode as V, type ColorKey as a, type Carry as b, type Checked as c, type Color as d, DEFAULT_PALETTE as e, type DiagramEvent as f, type Palette as g, type Region as h, type Step as i, resolvePalette as r, validateDiagram as v };
@@ -81,6 +81,12 @@ type DiagramEvent = {
81
81
  value?: string;
82
82
  tag?: string;
83
83
  };
84
+ /** 状態遷移図の、最初の段より前に書いた移れる道。`from` に `start`、`to` に `finish` を書ける */
85
+ interface StatePath {
86
+ from: string;
87
+ to: string;
88
+ label?: string;
89
+ }
84
90
  interface Step {
85
91
  title: string;
86
92
  body: string;
@@ -89,7 +95,7 @@ interface Step {
89
95
  }
90
96
  interface Diagram {
91
97
  version: number;
92
- kind: "sequence" | "architecture";
98
+ kind: "sequence" | "architecture" | "state";
93
99
  title: string;
94
100
  view?: ViewMode;
95
101
  regions?: Region[];
@@ -102,6 +108,9 @@ interface Diagram {
102
108
  sequence?: {
103
109
  lifelines: string[];
104
110
  };
111
+ state?: {
112
+ paths: StatePath[];
113
+ };
105
114
  }
106
115
  interface FieldError {
107
116
  /** 誤った欄の場所 (JSON Pointer) */
@@ -121,4 +130,4 @@ interface CheckOptions {
121
130
  /** 原本の JSON を検査する。版が道具より新しいかは見ない (`readDiagram` が見る)。 */
122
131
  declare function validateDiagram(input: unknown, options?: CheckOptions): Checked;
123
132
 
124
- export { type Box as B, type CheckOptions as C, type Diagram as D, type FieldError as F, type LabelSide as L, type NotationLang as N, PALETTES as P, READABLE_PX as R, type Step as S, type Theme as T, type ViewMode as V, type ColorKey as a, type Carry as b, type Checked as c, type Color as d, DEFAULT_PALETTE as e, type DiagramEvent as f, type Palette as g, type Region as h, resolvePalette as r, validateDiagram as v };
133
+ export { type Box as B, type CheckOptions as C, type Diagram as D, type FieldError as F, type LabelSide as L, type NotationLang as N, PALETTES as P, READABLE_PX as R, type StatePath as S, type Theme as T, type ViewMode as V, type ColorKey as a, type Carry as b, type Checked as c, type Color as d, DEFAULT_PALETTE as e, type DiagramEvent as f, type Palette as g, type Region as h, type Step as i, resolvePalette as r, validateDiagram as v };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cardenelabs/dragon",
3
- "version": "0.36.3",
3
+ "version": "0.37.0",
4
4
  "description": "Dragon — 動く図を、人は行ごとの記法で、AI は JSON で書く。時系列図と構成図を段ごとに動かし、mp4・GIF・HTML・PNG に書き出す。",
5
5
  "license": "MIT",
6
6
  "author": "cardene777",
@@ -32,6 +32,8 @@ export interface FieldMessages {
32
32
  const: (value: string) => string;
33
33
  /** どの欄の組み合わせか判らない場所では `undefined` */
34
34
  not: (allowed: Allowed | undefined) => string;
35
+ /** 状態遷移図の箱の名札に、始まりと終わりの印の名前 (`names`) を書いた */
36
+ reserved: (names: readonly string[]) => string;
35
37
  /** 選べる決まりがどれも欄が足りない時の、足りない欄。それ以外の時は空 */
36
38
  anyOf: (fields: readonly string[]) => string;
37
39
  oneOf: string;
@@ -74,6 +76,7 @@ const ja: FieldMessages = {
74
76
  minimum: (limit) => `${limit} 以上にする`,
75
77
  const: (value) => `${value} にする`,
76
78
  not: (allowed) => (allowed ? `${allowed.field} の欄は kind が ${allowed.kind} の時だけ書ける` : "この欄の組み合わせは書けない"),
79
+ reserved: (names) => `${names.join(" と ")} は状態遷移図の始まりと終わりの印の名前で、箱の名札に使えない`,
77
80
  anyOf: (fields) =>
78
81
  fields.length > 0 ? `${fields.join(" か ")} の${fields.length === 2 ? "どちらか" : "どれか"}の欄が要る` : "書ける形のどれにも合わない",
79
82
  oneOf: "書ける形のどれか 1 つだけに合わせる",
@@ -95,6 +98,7 @@ const en: FieldMessages = {
95
98
  minimum: (limit) => `Use ${limit} or greater`,
96
99
  const: (value) => `Use ${value}`,
97
100
  not: (allowed) => (allowed ? `The ${allowed.field} field is allowed only when kind is ${allowed.kind}` : "This combination of fields is not allowed"),
101
+ reserved: (names) => `${names.join(" and ")} name the start and end marks of a state diagram and cannot be box ids`,
98
102
  anyOf: (fields) => (fields.length > 0 ? `Write one of the ${fields.join(" or ")} fields` : "Does not match any of the allowed shapes"),
99
103
  oneOf: "Match exactly one of the allowed shapes",
100
104
  discriminator: (tag, values) => `${tag} is one of ${values.join(" / ")}`,
@@ -18,6 +18,15 @@ export interface FigureText {
18
18
  next: string;
19
19
  /** 段の一覧の読み上げの名前 */
20
20
  steps: (title: string) => string;
21
+ /** 状態遷移図の脇道の囲みの名前と添え書き (#3163)。添え書きの 2 行目は脇道へ入れる状態の数を言う (#3171) */
22
+ side: string;
23
+ sideNote: string;
24
+ sideFrom: (count: number) => string;
25
+ /** 状態遷移図の帯の最後の、通った状態の欄の名前 (#3171) */
26
+ trail: string;
27
+ /** 状態遷移図の始まりと終わりの印の字 */
28
+ start: string;
29
+ end: string;
21
30
  }
22
31
 
23
32
  export const FIGURE_TEXT: Record<NotationLang, FigureText> = {
@@ -29,6 +38,12 @@ export const FIGURE_TEXT: Record<NotationLang, FigureText> = {
29
38
  play: "動かす",
30
39
  next: "次の段",
31
40
  steps: (title) => `${title} の段`,
41
+ side: "脇道",
42
+ sideNote: "今回は通らない",
43
+ sideFrom: (count) => `${count} つの状態から入れる`,
44
+ trail: "通った状態",
45
+ start: "始まり",
46
+ end: "終わり",
32
47
  },
33
48
  en: {
34
49
  cannotDraw: (detail) => `Cannot draw the diagram: ${detail}`,
@@ -38,5 +53,11 @@ export const FIGURE_TEXT: Record<NotationLang, FigureText> = {
38
53
  play: "Play",
39
54
  next: "Next step",
40
55
  steps: (title) => `Steps of ${title}`,
56
+ side: "Side path",
57
+ sideNote: "Not taken this time",
58
+ sideFrom: (count) => `Enterable from ${count} ${count === 1 ? "state" : "states"}`,
59
+ trail: "States passed",
60
+ start: "Start",
61
+ end: "End",
41
62
  },
42
63
  };
package/src/index.ts CHANGED
@@ -20,6 +20,7 @@ export {
20
20
  type FieldError,
21
21
  type LabelSide,
22
22
  type Region,
23
+ type StatePath,
23
24
  type Step,
24
25
  validateDiagram,
25
26
  type ViewMode,
@@ -33,15 +34,21 @@ export type {
33
34
  SceneBadge,
34
35
  SceneBox,
35
36
  SceneEdge,
37
+ SceneEnd,
36
38
  SceneField,
37
39
  SceneLabel,
38
40
  SceneLifeline,
41
+ ScenePath,
39
42
  SceneRegion,
43
+ SceneSide,
40
44
  SceneStrip,
45
+ SceneTrail,
41
46
  } from "./layout/scene";
42
47
  export { type Family, FONTS } from "./look/fonts";
43
48
  export { type ColorKey, DEFAULT_PALETTE, type Palette, PALETTES, READABLE_PX, resolvePalette, type Theme } from "./look/values";
44
49
  export { mountDiagram, type Player, type PlayerOptions } from "./player";
50
+ export { ICONS } from "./render/icons";
51
+ export { iconSvg } from "./render/parts";
45
52
  export { type Rendered, type RenderOptions, renderDiagram } from "./render/svg";
46
- export type { CarrierTime, EdgeTime, EventTime, LabelTime, Span, Timeline } from "./timeline";
53
+ export type { CarrierTime, EdgeTime, EventTime, LabelTime, Span, StateTime, Timeline } from "./timeline";
47
54
  export { chooseView } from "./view";
@@ -334,7 +334,7 @@ function arrange(diagram: Diagram, groups: Group[], slots: Map<string, Slot>, ro
334
334
  );
335
335
  }
336
336
 
337
- const layout: Layout = { kind: "architecture", width: 0, height: 0, regions, boxes, lifelines: [], edges: sceneEdges, labels, actions: [], badges: [] };
337
+ const layout: Layout = { kind: "architecture", width: 0, height: 0, regions, boxes, lifelines: [], edges: sceneEdges, labels, actions: [], badges: [], paths: [], ends: [] };
338
338
  // 名前が左へ出たら、帯の中身を右へ寄せる。帯は図の幅いっぱいに引き直す
339
339
  const shift = labels.length > 0 ? Math.max(0, margin + LOOK.label.soft - Math.min(...labels.map((l) => l.x))) : 0;
340
340
  if (shift > 0) shiftContent(layout, shift, 0);
@@ -26,6 +26,8 @@ export interface SceneBox extends Rect {
26
26
  color: ColorKey;
27
27
  cx: number;
28
28
  cy: number;
29
+ /** 状態遷移図で、どの段も辿り着かない状態。点線の箱で脇道の囲みに描く */
30
+ ghost?: boolean;
29
31
  }
30
32
 
31
33
  export interface SceneRegion extends Rect {
@@ -60,6 +62,34 @@ export interface SceneLabel extends Rect {
60
62
  color: ColorKey;
61
63
  }
62
64
 
65
+ /** 状態遷移図の、最初の段より前に書いてどの段にも出ない線。点線で描き、運ぶ物を走らせない */
66
+ export interface ScenePath {
67
+ from: string;
68
+ to: string;
69
+ points: Point[];
70
+ label?: Rect & { text: string; family: Family };
71
+ }
72
+
73
+ /** 状態遷移図の始まりと終わりの印。`ghost` は脇道の中の終わりの印 */
74
+ export interface SceneEnd {
75
+ kind: "start" | "finish";
76
+ x: number;
77
+ y: number;
78
+ r: number;
79
+ ghost: boolean;
80
+ /** 印の右に添える字 */
81
+ text: string;
82
+ /** 終わりの印へ線を引く状態 */
83
+ box?: string;
84
+ }
85
+
86
+ /** 状態遷移図の脇道の囲み。`title` と `notes` は囲みの左上に置く字 */
87
+ export interface SceneSide extends Rect {
88
+ title: string;
89
+ /** 囲みの名前の下に 1 行ずつ置く添え書き */
90
+ notes: string[];
91
+ }
92
+
63
93
  /** 時系列図の箱の中の処理。生存線の上に札で出す。 */
64
94
  export interface SceneAction extends Rect {
65
95
  step: number;
@@ -90,9 +120,19 @@ export interface SceneField {
90
120
  w: number;
91
121
  }
92
122
 
123
+ /** 状態遷移図の帯の最後の欄。段が辿り着く順に状態を点で並べ、着いた状態を塗る (#3171) */
124
+ export interface SceneTrail {
125
+ title: string;
126
+ states: { box: string; color: ColorKey }[];
127
+ x: number;
128
+ y: number;
129
+ w: number;
130
+ }
131
+
93
132
  export interface SceneStrip extends Rect {
94
133
  title: string;
95
134
  fields: SceneField[];
135
+ trail?: SceneTrail;
96
136
  }
97
137
 
98
138
  export interface Layout {
@@ -107,6 +147,12 @@ export interface Layout {
107
147
  actions: SceneAction[];
108
148
  badges: SceneBadge[];
109
149
  strip?: SceneStrip;
150
+ /** 状態遷移図の通らない道。他の図の種類では空 */
151
+ paths: ScenePath[];
152
+ /** 状態遷移図の始まりと終わりの印。他の図の種類では空 */
153
+ ends: SceneEnd[];
154
+ /** 状態遷移図の脇道の囲み。通らない状態が無ければ無い */
155
+ side?: SceneSide;
110
156
  }
111
157
 
112
158
  export type EdgeEvent = Extract<DiagramEvent, { type: "edge" }>;
@@ -272,32 +318,43 @@ function fieldWidth(diagram: Diagram, name: string): number {
272
318
  return Math.max(textWidth(name, TYPE.fieldName), both);
273
319
  }
274
320
 
321
+ /** 帯の最後に並べる通った状態の欄の名前と点。状態遷移図だけが渡す */
322
+ export type TrailSource = Pick<SceneTrail, "title" | "states">;
323
+
324
+ const trailWidth = (trail: TrailSource): number => Math.max(textWidth(trail.title, TYPE.fieldName), trail.states.length * LOOK.strip.trail.step);
325
+
275
326
  /** 1 行に並べた時に帯が要る幅。 */
276
- export function stripNeed(diagram: Diagram): number {
327
+ export function stripNeed(diagram: Diagram, trail?: TrailSource): number {
277
328
  if (!diagram.strip) return 0;
278
329
  const { titleX, fieldGap, pad } = LOOK.strip;
279
330
  const start = titleX + textWidth(diagram.strip.title, TYPE.stripTitle) + fieldGap;
280
- return start + diagram.strip.fields.reduce((sum, name) => sum + fieldWidth(diagram, name) + fieldGap, 0) - fieldGap + pad;
331
+ const widths = [...diagram.strip.fields.map((name) => fieldWidth(diagram, name)), ...(trail ? [trailWidth(trail)] : [])];
332
+ return start + widths.reduce((sum, w) => sum + w + fieldGap, 0) - fieldGap + pad;
281
333
  }
282
334
 
283
- /** 中身の帯。欄は左から並べ、幅に収まらない欄は次の行へ送る。 */
284
- export function layoutStrip(diagram: Diagram, x: number, y: number, width: number): SceneStrip | undefined {
335
+ /** 中身の帯。欄は左から並べ、幅に収まらない欄は次の行へ送る。通った状態の欄を渡すと、最後の欄の後に同じ決まりで置く。 */
336
+ export function layoutStrip(diagram: Diagram, x: number, y: number, width: number, trail?: TrailSource): SceneStrip | undefined {
285
337
  if (!diagram.strip) return undefined;
286
338
  const { titleX, fieldGap, pad, line, h } = LOOK.strip;
287
339
  const start = titleX + textWidth(diagram.strip.title, TYPE.stripTitle) + fieldGap;
288
340
  const fields: SceneField[] = [];
289
341
  let cx = start;
290
342
  let row = 0;
291
- for (const name of diagram.strip.fields) {
292
- const w = fieldWidth(diagram, name);
343
+ const place = (w: number): { x: number; y: number } => {
293
344
  if (cx > start && cx + w > width - pad) {
294
345
  cx = start;
295
346
  row++;
296
347
  }
297
- fields.push({ name, x: x + cx, y: y + row * line, w });
348
+ const at = { x: x + cx, y: y + row * line };
298
349
  cx += w + fieldGap;
350
+ return at;
351
+ };
352
+ for (const name of diagram.strip.fields) {
353
+ const w = fieldWidth(diagram, name);
354
+ fields.push({ name, ...place(w), w });
299
355
  }
300
- return { x, y, w: width, h: h + row * line, title: diagram.strip.title, fields };
356
+ const laidTrail = trail ? { ...trail, ...place(trailWidth(trail)), w: trailWidth(trail) } : undefined;
357
+ return { x, y, w: width, h: h + row * line, title: diagram.strip.title, fields, ...(laidTrail ? { trail: laidTrail } : {}) };
301
358
  }
302
359
 
303
360
  /** 場面の全ての物を (dx, dy) だけ動かす。区画と帯は動かさず、横いっぱいの形を保つ。 */
@@ -325,6 +382,15 @@ export function shiftContent(layout: Layout, dx: number, dy: number): void {
325
382
  if (edge.settle) edge.settle = [edge.settle[0] + dx, edge.settle[1] + dy];
326
383
  }
327
384
  for (const label of layout.labels) move(label);
385
+ for (const path of layout.paths) {
386
+ path.points = path.points.map(([px, py]) => [px + dx, py + dy]);
387
+ if (path.label) move(path.label);
388
+ }
389
+ for (const end of layout.ends) {
390
+ end.x += dx;
391
+ end.y += dy;
392
+ }
393
+ if (layout.side) move(layout.side);
328
394
  for (const action of layout.actions) move(action);
329
395
  for (const badge of layout.badges) {
330
396
  badge.x += dx;
@@ -336,6 +402,10 @@ export function shiftContent(layout: Layout, dx: number, dy: number): void {
336
402
  field.x += dx;
337
403
  field.y += dy;
338
404
  }
405
+ if (layout.strip.trail) {
406
+ layout.strip.trail.x += dx;
407
+ layout.strip.trail.y += dy;
408
+ }
339
409
  }
340
410
  }
341
411
 
@@ -154,7 +154,7 @@ export function layoutSequence(diagram: Diagram): Layout {
154
154
  labels.push(placeLabel(request, { hard: boxes, soft }));
155
155
  }
156
156
 
157
- const layout: Layout = { kind: "sequence", width: 0, height: 0, regions: [], boxes, lifelines, edges, labels, actions, badges: [] };
157
+ const layout: Layout = { kind: "sequence", width: 0, height: 0, regions: [], boxes, lifelines, edges, labels, actions, badges: [], paths: [], ends: [] };
158
158
 
159
159
  // 図の幅。名前が左に出たら全体を右へ寄せ、右端は一番右の物に余白を足した所
160
160
  const lefts = [...boxes.map((b) => b.x), ...labels.map((l) => l.x), ...actions.map((a) => a.x)];