@cardenelabs/dragon 0.30.0 → 0.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +314 -12
- package/dist/index.cjs +6966 -1438
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2150 -118
- package/dist/index.d.ts +2150 -118
- package/dist/index.js +6950 -1440
- package/dist/index.js.map +1 -1
- package/package.json +12 -13
- package/src/compile/actor-ids.ts +6 -15
- package/src/compile/actor-option-notice.ts +379 -0
- package/src/compile/actors.ts +32 -3
- package/src/compile/c4.ts +0 -3
- package/src/compile/chain.ts +43 -4
- package/src/compile/chart-fields.ts +16 -1
- package/src/compile/direction.ts +50 -11
- package/src/compile/edge-option-notice.ts +112 -0
- package/src/compile/flowchart.ts +344 -0
- package/src/compile/funnel.ts +14 -2
- package/src/compile/gantt.ts +141 -36
- package/src/compile/generic.ts +473 -61
- package/src/compile/journey.ts +14 -5
- package/src/compile/kinds.ts +5 -13
- package/src/compile/lanes.ts +95 -13
- package/src/compile/metro.ts +271 -0
- package/src/compile/mind.ts +55 -5
- package/src/compile/notice.ts +51 -4
- package/src/compile/order.ts +62 -0
- package/src/compile/parts.ts +107 -33
- package/src/compile/placement-apply.ts +27 -11
- package/src/compile/quadrant.ts +171 -16
- package/src/compile/record.ts +75 -0
- package/src/compile/row-marks.ts +128 -0
- package/src/compile/sequence.ts +105 -26
- package/src/compile/slug.ts +9 -1
- package/src/compile/stages.ts +247 -0
- package/src/compile/subtitle.ts +15 -2
- package/src/compile/swimlane-marks.ts +87 -0
- package/src/compile/swimlane.ts +13 -14
- package/src/compile/timeline.ts +213 -0
- package/src/compile/topology.ts +3 -9
- package/src/compile/tree.ts +13 -4
- package/src/compile/value-chart.ts +179 -25
- package/src/compile.ts +1550 -370
- package/src/focus.ts +2 -2
- package/src/index.ts +34 -2
- package/src/json-parser.ts +1407 -48
- package/src/keywords.ts +251 -17
- package/src/notation-lint.ts +41 -64
- package/src/schemas/diagram.json +643 -20
- package/src/types.ts +336 -35
- package/src/v05/parser.ts +2451 -271
- package/src/compile/class.ts +0 -100
- package/src/compile/er.ts +0 -44
- package/src/compile/solidity.ts +0 -39
- package/src/compile/state.ts +0 -71
package/src/compile/journey.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { diagram } from "@cardenelabs/cdl";
|
|
2
2
|
import type { CdlDiagram } from "@cardenelabs/cdl";
|
|
3
3
|
import type { DslActor, DslDocument } from "../types";
|
|
4
|
-
import { 図表の大きさ, 語の欄から参照できる名前 } from "./chart-fields";
|
|
4
|
+
import { 図表の大きさ, 図表の大きさを読む, 語の欄から参照できる名前 } from "./chart-fields";
|
|
5
5
|
import { 箱の題 } from "./node-title";
|
|
6
6
|
import type { CompileNotice } from "./notice";
|
|
7
7
|
import { slugify } from "./slug";
|
|
8
8
|
import { 図の小見出し } from "./subtitle";
|
|
9
9
|
import { 気持ち } from "./word-state";
|
|
10
10
|
/**
|
|
11
|
-
*
|
|
11
|
+
* ユーザージャーニー (`type: journey`) の組み立て (#1154)。 描画側に 1 つの箱で渡す。
|
|
12
12
|
*
|
|
13
13
|
* 登場人物ごとに **気持ち** を読む (`- 登録: "不満"`)。 使える語は `気持ち` が持つ。
|
|
14
14
|
*/
|
|
@@ -17,7 +17,7 @@ export function compileJourney(
|
|
|
17
17
|
onNotice?: (n: CompileNotice) => void,
|
|
18
18
|
): CdlDiagram {
|
|
19
19
|
const b = diagram(slugify(doc.title), { topic: doc.title, type: "journey" });
|
|
20
|
-
const { w: W, h: H } = 図表の大きさ.journey;
|
|
20
|
+
const { w: W, h: H } = 図表の大きさを読む(doc, 図表の大きさ.journey);
|
|
21
21
|
b.lane("chart", { width: W + 64 });
|
|
22
22
|
const data: NonNullable<CdlDiagram["nodes"][number]["journeyData"]> = [];
|
|
23
23
|
const 読めない: string[] = [];
|
|
@@ -66,19 +66,28 @@ export function compileJourney(
|
|
|
66
66
|
w: W,
|
|
67
67
|
h: H,
|
|
68
68
|
journeyData: data,
|
|
69
|
+
...(doc.journeyForm !== undefined ? { journeyForm: doc.journeyForm } : {}),
|
|
70
|
+
...(doc.journeyLineForm !== undefined ? { journeyLineForm: doc.journeyLineForm } : {}),
|
|
71
|
+
...(doc.journeyLabels !== undefined ? { journeyLabels: doc.journeyLabels } : {}),
|
|
72
|
+
...(doc.journeySize !== undefined ? { journeySize: doc.journeySize } : {}),
|
|
69
73
|
});
|
|
70
74
|
return b.build();
|
|
71
75
|
}
|
|
72
76
|
|
|
73
77
|
/**
|
|
74
|
-
*
|
|
78
|
+
* ユーザージャーニーの段に添える欄 (#1251)。
|
|
75
79
|
*
|
|
76
80
|
* 書かなければ項目ごと落とす = `undefined` を明示して渡すと、 組立て側が「空を書いた」 と
|
|
77
81
|
* 区別できなくなる (`図の小見出し` と同じ理由)。
|
|
78
82
|
*/
|
|
79
|
-
function 道筋の欄(a: DslActor): {
|
|
83
|
+
function 道筋の欄(a: DslActor): {
|
|
84
|
+
touchpoint?: string;
|
|
85
|
+
opportunity?: string;
|
|
86
|
+
opportunityPosition?: DslActor["opportunityPosition"];
|
|
87
|
+
} {
|
|
80
88
|
return {
|
|
81
89
|
...(a.touchpoint !== undefined ? { touchpoint: a.touchpoint } : {}),
|
|
82
90
|
...(a.opportunity !== undefined ? { opportunity: a.opportunity } : {}),
|
|
91
|
+
...(a.opportunityPosition !== undefined ? { opportunityPosition: a.opportunityPosition } : {}),
|
|
83
92
|
};
|
|
84
93
|
}
|
package/src/compile/kinds.ts
CHANGED
|
@@ -32,6 +32,7 @@ import type { PresetType } from "../types";
|
|
|
32
32
|
* | `contract` / `proxy` / `library` / `interface` | `card` | 契約は札で表す (`solidity` の置き換え先に合わせた) |
|
|
33
33
|
* | `eoa` | `person` | 人が持つ財布 |
|
|
34
34
|
* | `multisig` | `signer` | 複数人で署名する (`solidity` の置き換え先に合わせた) |
|
|
35
|
+
* | `loop` | `card` | 繰り返しは札で表す。 分かれ道の図では形に読み替わるのでここを通らない (#2523) |
|
|
35
36
|
*
|
|
36
37
|
* **表は `DSL_ONLY_KINDS` を鍵にして書く**。 種類を足した時に読み替え先が無いと
|
|
37
38
|
* 型検査が落ちるので、足し忘れが残らない。
|
|
@@ -46,6 +47,7 @@ export const 記法だけの種類の読み替え: Readonly<Record<(typeof DSL_O
|
|
|
46
47
|
interface: "card",
|
|
47
48
|
eoa: "person",
|
|
48
49
|
multisig: "signer",
|
|
50
|
+
loop: "card",
|
|
49
51
|
};
|
|
50
52
|
|
|
51
53
|
/** 描画側へ渡せる種類にする。 記法だけの種類はここで読み替わる (#1420) */
|
|
@@ -82,24 +84,14 @@ export const 図種の作り: Record<PresetType, "登場人物ごとに箱" | "
|
|
|
82
84
|
// 矢印の端と登場人物の名前が、 そのまま箱や枠の id になる
|
|
83
85
|
sequence: "登場人物ごとに箱",
|
|
84
86
|
flow: "登場人物ごとに箱",
|
|
87
|
+
flowchart: "登場人物ごとに箱",
|
|
85
88
|
swimlane: "登場人物ごとに箱",
|
|
86
|
-
|
|
87
|
-
state: "登場人物ごとに箱",
|
|
89
|
+
record: "登場人物ごとに箱",
|
|
88
90
|
topology: "登場人物ごとに箱",
|
|
89
|
-
solidity: "登場人物ごとに箱",
|
|
90
|
-
class: "登場人物ごとに箱",
|
|
91
91
|
c4: "登場人物ごとに箱",
|
|
92
92
|
// 中身は payload が持ち、 図そのものは 1 箱。 矢印は描かず本数を数えて知らせる
|
|
93
93
|
gantt: "図全体を 1 箱",
|
|
94
|
-
|
|
95
|
-
bar: "図全体を 1 箱",
|
|
96
|
-
line: "図全体を 1 箱",
|
|
97
|
-
gauge: "図全体を 1 箱",
|
|
98
|
-
radial: "図全体を 1 箱",
|
|
99
|
-
stat: "図全体を 1 箱",
|
|
100
|
-
waffle: "図全体を 1 箱",
|
|
101
|
-
stacked: "図全体を 1 箱",
|
|
102
|
-
slope: "図全体を 1 箱",
|
|
94
|
+
chart: "図全体を 1 箱",
|
|
103
95
|
funnel: "図全体を 1 箱",
|
|
104
96
|
tree: "図全体を 1 箱",
|
|
105
97
|
journey: "図全体を 1 箱",
|
package/src/compile/lanes.ts
CHANGED
|
@@ -17,27 +17,30 @@ import type { DslDocument, PresetType } from "../types";
|
|
|
17
17
|
* `er` は「1 縦列 1 箱」 が図の読み方そのもの (表が横に並ぶ) なので、2 つの箱を同じ縦列へ
|
|
18
18
|
* 入れられる形にはしない。
|
|
19
19
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
20
|
+
* 状態遷移図は外した (#1466)。設計が格子に置く形になり、
|
|
21
|
+
* 「箱の 1 つの辺には関係を 1 本まで」 を守るには 1 つの縦列に複数の箱が要る。
|
|
22
22
|
* 「1 縦列 1 箱」 が読み方だった前提はここで崩れている。
|
|
23
23
|
*/
|
|
24
24
|
export const 縦列を選べる図種: ReadonlySet<PresetType> = new Set<PresetType>([
|
|
25
25
|
"flow",
|
|
26
26
|
"topology",
|
|
27
27
|
"swimlane",
|
|
28
|
-
"class",
|
|
29
|
-
"state",
|
|
30
28
|
/*
|
|
31
|
-
*
|
|
29
|
+
* 分かれ道の図 (#2513)。
|
|
32
30
|
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
|
|
36
|
-
|
|
31
|
+
* この図種は **縦列が役割で、箱はその中に置く**。 1 人の役割の中に複数の箱が並ぶ形
|
|
32
|
+
* (申請を出す / 直して出し直す) が図の読み方そのものなので、書いた縦列をそのまま使う。
|
|
33
|
+
*/
|
|
34
|
+
"flowchart",
|
|
35
|
+
/*
|
|
36
|
+
* 行を持つ図 (#1571 / #2782)。
|
|
37
37
|
*
|
|
38
|
-
*
|
|
38
|
+
* 畳む前の表の図は、組み立て器が実体 1 つにつき帯を 1 本作り必ず段 0 に置いていた =
|
|
39
|
+
* 表が横 1 列にしか並ばない。 関係を 4 本持つ実体があると、どう並べ替えても 2 本は隣を
|
|
40
|
+
* 飛び越す (意匠帳 `docs/design/er/note.md` § 記法の制約 が 4 通りを実測しており、
|
|
41
|
+
* 1 列は縦横比 7.0 / 最長の線 4068、格子は 3.5 / 836)。
|
|
39
42
|
*/
|
|
40
|
-
"
|
|
43
|
+
"record",
|
|
41
44
|
]);
|
|
42
45
|
|
|
43
46
|
/**
|
|
@@ -47,10 +50,89 @@ export const 縦列を選べる図種: ReadonlySet<PresetType> = new Set<PresetT
|
|
|
47
50
|
* 行き先を決める規則が要る (既定の縦列に集める / 自分の縦列を作る) が、どちらも
|
|
48
51
|
* 書いた人の意図と一致する保証が無い。 混ざった形は `reportLaneMixed` が知らせる。
|
|
49
52
|
*
|
|
50
|
-
*
|
|
53
|
+
* 数える箱は `縦列の判定に数える箱` が決める (#2372)。
|
|
51
54
|
*/
|
|
52
55
|
export function 書いた縦列に置く(kind: PresetType, doc: DslDocument): boolean {
|
|
53
56
|
if (!縦列を選べる図種.has(kind)) return false;
|
|
54
|
-
const 対象 = doc
|
|
57
|
+
const 対象 = 縦列の判定に数える箱(doc);
|
|
55
58
|
return 対象.length > 0 && 対象.every((a) => a.lane !== undefined);
|
|
56
59
|
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 縦列の判定で数える箱 (#2372)。
|
|
63
|
+
*
|
|
64
|
+
* **見本 (`parts`) は縦列を書いた時だけ数える**。 見本の縦列は張替え先の指定で、
|
|
65
|
+
* 箱を並べる話ではない = 書いていない見本を数に入れると、普通の箱が全部書いた図まで
|
|
66
|
+
* 「一部だけ書いた」 に倒れる。
|
|
67
|
+
*
|
|
68
|
+
* 逆に見本を **常に** 外すと、箱が全部見本の図で対象が 0 件になり
|
|
69
|
+
* 「全ての箱が縦列を書いた」 が成立しない。 結果は 3 つ重なる =
|
|
70
|
+
* 書いた縦列に箱が入らない / 宣言した縦列が空のまま残る / 誤って「どの箱も入らない」 と知らせる
|
|
71
|
+
* (実測 = カタログの見本 19 枚で知らせが 38 件)。
|
|
72
|
+
*
|
|
73
|
+
* 見本が混ざった図では普通の箱が対象になって成立し、**見本も一緒に書いた縦列へ入っていた**
|
|
74
|
+
* = 見本を縦列へ入れられないわけではなく、数え方だけが割れていた。
|
|
75
|
+
*
|
|
76
|
+
* **置く側と知らせる側で同じ数え方を使う**。 別々に数えると、見本だけの図で
|
|
77
|
+
* 「一部だけ書いた」 形が知らせずに落ちる。
|
|
78
|
+
*/
|
|
79
|
+
export function 縦列の判定に数える箱(doc: DslDocument): DslDocument["actors"] {
|
|
80
|
+
const 見本が縦列を書いた = doc.actors.some((a) => a.partId !== undefined && a.lane !== undefined);
|
|
81
|
+
return 見本が縦列を書いた ? doc.actors : doc.actors.filter((a) => a.partId === undefined);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 縦列ごとに箱の段を決める (#1394 / #2396)。
|
|
86
|
+
*
|
|
87
|
+
* 段は **書いた番号をそのまま持つ**。 書き順で 0 から詰め直すと、`stack: 1` と書いた箱が
|
|
88
|
+
* 0 へ落ちる = 決定木のように「同じ高さに並ばない」 ことが図の意味そのものになる形では、
|
|
89
|
+
* 詰めた瞬間に別の図になる (実測 = 3 段の木の根が 1 段目へ上がった)。
|
|
90
|
+
*
|
|
91
|
+
* 書かなかった箱は、その縦列で **空いている一番小さい段** に置く。 単に数え上げると
|
|
92
|
+
* 書いた番号と重なり、2 つの箱が同じ段に載る。
|
|
93
|
+
*
|
|
94
|
+
* **箱を並べる図種すべてが同じ決め方を使う** (#2396)。かつてのクラス図だけこの決め方を持たず、
|
|
95
|
+
* 段を書かない箱を全て 0 に残していた。 同じ縦列に箱を 2 つ置くと配置の計算が
|
|
96
|
+
* 「同じ場所に 2 つある」 として投げ、図が 1 枚も描けない (実測 = 24 図種で落ちるのは
|
|
97
|
+
* class 専用経路だけ)。
|
|
98
|
+
*
|
|
99
|
+
* @param actors 記法から読んだ箱を書いた順に並べたもの
|
|
100
|
+
* @param 縦列を引く 箱を同じ段の集合へ分ける鍵。 省略時は `lane` を使う
|
|
101
|
+
* @returns 段を持つ箱だけの表。 縦列も段も書かない箱は入らない (呼出側が既定に任せる)
|
|
102
|
+
*/
|
|
103
|
+
export function 縦列ごとの段を決める(
|
|
104
|
+
actors: DslDocument["actors"],
|
|
105
|
+
縦列を引く: (actor: DslDocument["actors"][number]) => string | undefined = (actor) =>
|
|
106
|
+
actor.lane,
|
|
107
|
+
): ReadonlyMap<DslDocument["actors"][number], number> {
|
|
108
|
+
const 埋まった段 = new Map<string, Set<number>>();
|
|
109
|
+
const 埋める = (lid: string, stack: number): void => {
|
|
110
|
+
const 集合 = 埋まった段.get(lid) ?? new Set<number>();
|
|
111
|
+
集合.add(stack);
|
|
112
|
+
埋まった段.set(lid, 集合);
|
|
113
|
+
};
|
|
114
|
+
// 書いた番号を先に全て埋める = 書き順が後の箱の番号とも重ならない
|
|
115
|
+
for (const a of actors) {
|
|
116
|
+
const 縦列 = 縦列を引く(a);
|
|
117
|
+
if (縦列 !== undefined && a.stack !== undefined) 埋める(縦列, a.stack);
|
|
118
|
+
}
|
|
119
|
+
const 決めた = new Map<DslDocument["actors"][number], number>();
|
|
120
|
+
for (const a of actors) {
|
|
121
|
+
const 縦列 = 縦列を引く(a);
|
|
122
|
+
if (縦列 === undefined) {
|
|
123
|
+
// 縦列を書かない箱は縦列の中の段を持たない。 段だけ書いた箱はその番号を渡す
|
|
124
|
+
if (a.stack !== undefined) 決めた.set(a, a.stack);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (a.stack !== undefined) {
|
|
128
|
+
決めた.set(a, a.stack);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
let stack = 0;
|
|
132
|
+
const 集合 = 埋まった段.get(縦列);
|
|
133
|
+
while (集合?.has(stack)) stack += 1;
|
|
134
|
+
埋める(縦列, stack);
|
|
135
|
+
決めた.set(a, stack);
|
|
136
|
+
}
|
|
137
|
+
return 決めた;
|
|
138
|
+
}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import type { DslActor, DslDocument } from "../types";
|
|
2
|
+
import { 箱の題 } from "./node-title";
|
|
3
|
+
import { slugify } from "./slug";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 路線図の形 (`shape: metro`, #2799) の配置規則。
|
|
7
|
+
*
|
|
8
|
+
* 担当 (`lane`) ごとに横の帯を 1 本作り、箱を駅として書いた順に左から並べる。
|
|
9
|
+
* 担当を書かない箱は箱の名前を線路にする。省略したことは `compile.ts` の
|
|
10
|
+
* `reportTrackFromName` が知らせる。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 帯 1 本の高さ。見本の担当の線路の中心 (240 / 460 / 680) の差 220 に合わせる。
|
|
15
|
+
*/
|
|
16
|
+
const TRACK_HEIGHT = 220;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 最初の駅の中心。
|
|
20
|
+
* 見本の名札の左端 46 を描く側 0.125.0 の inset 24 に合わせると、lane の左端は 22 になる。
|
|
21
|
+
* 見本の始まりの中心 290 から lane の左端 22 を引いた 268 に合わせる。
|
|
22
|
+
* 名札 (24..224) と案内線の始まり (248) より右へ置く。
|
|
23
|
+
*/
|
|
24
|
+
const FIRST_STATION_X = 268;
|
|
25
|
+
|
|
26
|
+
/** 同じ線路の図形どうしに空ける 40。見本の駅と線を詰めすぎない値。 */
|
|
27
|
+
const NODE_SHAPE_GAP = 40;
|
|
28
|
+
|
|
29
|
+
/** 見本で始まりの中心 290 から最初の駅 380 まで離した 90。 */
|
|
30
|
+
const START_MARK_CENTER_GAP = 90;
|
|
31
|
+
|
|
32
|
+
/** 見本で「届けに行く」の中心 1410 から分かれ道「在宅?」の中心 1520 まで離した 110。 */
|
|
33
|
+
const STATION_DECISION_CENTER_GAP = 110;
|
|
34
|
+
|
|
35
|
+
/** 見本で最後の駅の中心 1690 から終わり 1752 まで離した 62。 */
|
|
36
|
+
const END_MARK_CENTER_GAP = 62;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 担当が替わる 2 駅の間に足す 140。見本の中心間 360 から線路間 220 を引いた値。
|
|
40
|
+
* 傾き ±1 の斜線の前後に横線と円弧を置く余裕になる。
|
|
41
|
+
*/
|
|
42
|
+
const TRANSFER_MARGIN = 140;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 同じ担当で隣り合う駅名どうしの最小の隙間と、名前の幅の見積りに使う字の大きさ。
|
|
46
|
+
* 25 は描く側 0.125.0 が駅名に使う字の大きさ。
|
|
47
|
+
* 描く側が字の大きさを変えたら、ここも合わせる。
|
|
48
|
+
*/
|
|
49
|
+
const TITLE_GAP = 27;
|
|
50
|
+
/** 駅名を下へ回した組だけは、見本の中心間 130 まで詰められる。 */
|
|
51
|
+
const BOTTOM_TITLE_GAP = 14.5;
|
|
52
|
+
const STATION_TITLE_FONT_SIZE = 25;
|
|
53
|
+
/** 太字の駅名が字面の外へ張り出す分を 3 と見積もる。 */
|
|
54
|
+
const STATION_TITLE_BOLD_OVERHANG = 3;
|
|
55
|
+
|
|
56
|
+
/** 見本の分かれ道の枝を、半幅 58 + 直線 30 + 半径 22 の曲がり 2 つ + 余白 22 で作る。 */
|
|
57
|
+
const DECISION_HALF_WIDTH = 58;
|
|
58
|
+
const BRANCH_STUB = 30;
|
|
59
|
+
const BRANCH_CORNER_RADIUS = 22;
|
|
60
|
+
const BRANCH_END_MARGIN = 22;
|
|
61
|
+
|
|
62
|
+
/** 見本で分かれ道の中心 y=680 から真下の駅 y=910 まで離した 230。 */
|
|
63
|
+
const DECISION_DOWN_GAP = 230;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 線路の右側に残す 26。
|
|
67
|
+
* 見本の案内線は、終わりの中心 1752 より 2 先の x2=1754 で終わる。
|
|
68
|
+
* 描く側 0.125.0 は `METRO_LANE_BADGE_INSET` の 24 を使い、案内線を lane の右端から 24 手前で止める。
|
|
69
|
+
* 2 と 24 の和を使う。
|
|
70
|
+
*/
|
|
71
|
+
const TRACK_END_PADDING = 26;
|
|
72
|
+
|
|
73
|
+
type MetroNodeKind = "station" | "decision" | "mark-start" | "mark-end";
|
|
74
|
+
|
|
75
|
+
/** 描く側 0.125.0 の路線図の図形の半幅。 */
|
|
76
|
+
function nodeHalfWidth(kind: MetroNodeKind): number {
|
|
77
|
+
if (kind === "decision") return DECISION_HALF_WIDTH;
|
|
78
|
+
if (kind === "mark-start") return 17;
|
|
79
|
+
if (kind === "mark-end") return 19;
|
|
80
|
+
return 16;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** 始まりと終わりの枠を、描く側が見せる丸の直径に合わせる。 */
|
|
84
|
+
export function metroMarkFrameSize(kind: DslActor["kind"]): number | undefined {
|
|
85
|
+
if (kind !== "mark-start" && kind !== "mark-end") return undefined;
|
|
86
|
+
return nodeHalfWidth(kind) * 2;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** 路線図では3種の印だけをその形で描き、それ以外の箱は駅として描く。 */
|
|
90
|
+
function metroNodeKind(actor: DslActor): MetroNodeKind {
|
|
91
|
+
if (actor.kind === "decision" || actor.kind === "mark-start" || actor.kind === "mark-end") {
|
|
92
|
+
return actor.kind;
|
|
93
|
+
}
|
|
94
|
+
return "station";
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export type MetroTrack = {
|
|
98
|
+
id: string;
|
|
99
|
+
label: string;
|
|
100
|
+
subtitle?: string;
|
|
101
|
+
posX: number;
|
|
102
|
+
posY: number;
|
|
103
|
+
posW: number;
|
|
104
|
+
posH: number;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export type MetroStation = {
|
|
108
|
+
actorName: string;
|
|
109
|
+
trackId: string;
|
|
110
|
+
posX: number;
|
|
111
|
+
posY: number;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export type MetroPlacement = {
|
|
115
|
+
width: number;
|
|
116
|
+
tracks: MetroTrack[];
|
|
117
|
+
stations: MetroStation[];
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/** 担当を書かない箱を独立した線路にするため、その箱の名前を線路の鍵として返す。 */
|
|
121
|
+
function 線路の鍵(actor: DslActor): string {
|
|
122
|
+
return actor.lane ?? actor.name;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** 駅名が重ならない間隔を取るため、描く側と同じ字の大きさで名前の幅を概算する。 */
|
|
126
|
+
function estimateStationTitleWidth(title: string): number {
|
|
127
|
+
if (title === "") return 0;
|
|
128
|
+
return (
|
|
129
|
+
[...title].reduce(
|
|
130
|
+
(width, char) =>
|
|
131
|
+
width +
|
|
132
|
+
(/^[\x20-\x7e]$/u.test(char) ? STATION_TITLE_FONT_SIZE * 0.6 : STATION_TITLE_FONT_SIZE),
|
|
133
|
+
0,
|
|
134
|
+
) + STATION_TITLE_BOLD_OVERHANG
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* 路線図の幅と、線路・駅の絶対座標を返す。
|
|
140
|
+
*
|
|
141
|
+
* 静止図と動く図はどちらも `compileGenericWithAnimate` からここを呼び、同じ配置を使う。
|
|
142
|
+
*/
|
|
143
|
+
export function placeMetro(doc: Pick<DslDocument, "actors" | "flow" | "lanes">): MetroPlacement {
|
|
144
|
+
const usedTracks: string[] = [];
|
|
145
|
+
for (const actor of doc.actors) {
|
|
146
|
+
const track = 線路の鍵(actor);
|
|
147
|
+
if (!usedTracks.includes(track)) usedTracks.push(track);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const declared = doc.lanes ? Object.keys(doc.lanes) : [];
|
|
151
|
+
const trackOrder = [
|
|
152
|
+
...declared.filter((track) => usedTracks.includes(track)),
|
|
153
|
+
...usedTracks.filter((track) => !declared.includes(track)),
|
|
154
|
+
];
|
|
155
|
+
|
|
156
|
+
const usedIds = new Set<string>();
|
|
157
|
+
const trackIdByKey = new Map<string, string>();
|
|
158
|
+
for (const [index, track] of trackOrder.entries()) {
|
|
159
|
+
const base = `track-${slugify(track) || index}`;
|
|
160
|
+
let id = base;
|
|
161
|
+
let suffix = 2;
|
|
162
|
+
while (usedIds.has(id)) id = `${base}-${suffix++}`;
|
|
163
|
+
usedIds.add(id);
|
|
164
|
+
trackIdByKey.set(track, id);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const actorByName = new Map(doc.actors.map((actor) => [actor.name, actor]));
|
|
168
|
+
const 下へ置く = new Map<string, { decision: string; order: number }>();
|
|
169
|
+
for (const decision of doc.actors.filter((actor) => metroNodeKind(actor) === "decision")) {
|
|
170
|
+
let order = 0;
|
|
171
|
+
const branches = doc.flow.filter((step) => step.from === decision.name);
|
|
172
|
+
for (const branch of branches.slice(1)) {
|
|
173
|
+
const target = actorByName.get(branch.to);
|
|
174
|
+
if (
|
|
175
|
+
target === undefined ||
|
|
176
|
+
線路の鍵(target) !== 線路の鍵(decision) ||
|
|
177
|
+
metroNodeKind(target) !== "station" ||
|
|
178
|
+
下へ置く.has(target.name)
|
|
179
|
+
) {
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
order += 1;
|
|
183
|
+
下へ置く.set(target.name, { decision: decision.name, order });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const stations: MetroStation[] = [];
|
|
188
|
+
const stationByActor = new Map<string, MetroStation>();
|
|
189
|
+
let previousOnTrack: { actor: DslActor; station: MetroStation } | undefined;
|
|
190
|
+
for (const [index, actor] of doc.actors.entries()) {
|
|
191
|
+
const track = 線路の鍵(actor);
|
|
192
|
+
const trackIndex = trackOrder.indexOf(track);
|
|
193
|
+
const posY = trackIndex * TRACK_HEIGHT + TRACK_HEIGHT / 2;
|
|
194
|
+
const below = 下へ置く.get(actor.name);
|
|
195
|
+
const decisionStation = below === undefined ? undefined : stationByActor.get(below.decision);
|
|
196
|
+
if (below !== undefined && decisionStation !== undefined) {
|
|
197
|
+
const station = {
|
|
198
|
+
actorName: actor.name,
|
|
199
|
+
trackId: trackIdByKey.get(track) ?? `track-${index}`,
|
|
200
|
+
posX: decisionStation.posX,
|
|
201
|
+
posY: decisionStation.posY + DECISION_DOWN_GAP * below.order,
|
|
202
|
+
};
|
|
203
|
+
stations.push(station);
|
|
204
|
+
stationByActor.set(actor.name, station);
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
let posX = FIRST_STATION_X;
|
|
209
|
+
if (previousOnTrack !== undefined) {
|
|
210
|
+
const previousActor = previousOnTrack.actor;
|
|
211
|
+
const previous = previousOnTrack.station;
|
|
212
|
+
const previousKind = metroNodeKind(previousActor);
|
|
213
|
+
const currentKind = metroNodeKind(actor);
|
|
214
|
+
if (線路の鍵(previousActor) !== track) {
|
|
215
|
+
const gap =
|
|
216
|
+
previousKind === "decision"
|
|
217
|
+
? DECISION_HALF_WIDTH +
|
|
218
|
+
BRANCH_STUB +
|
|
219
|
+
BRANCH_CORNER_RADIUS * 2 +
|
|
220
|
+
BRANCH_END_MARGIN +
|
|
221
|
+
nodeHalfWidth(currentKind)
|
|
222
|
+
: Math.abs(posY - previous.posY) + TRANSFER_MARGIN;
|
|
223
|
+
posX = previous.posX + gap;
|
|
224
|
+
} else {
|
|
225
|
+
const shapeGap = previousKind === "mark-start" && currentKind === "station"
|
|
226
|
+
? START_MARK_CENTER_GAP
|
|
227
|
+
: previousKind === "station" && currentKind === "decision"
|
|
228
|
+
? STATION_DECISION_CENTER_GAP
|
|
229
|
+
: previousKind === "station" && currentKind === "mark-end"
|
|
230
|
+
? END_MARK_CENTER_GAP
|
|
231
|
+
: nodeHalfWidth(previousKind) + nodeHalfWidth(currentKind) + NODE_SHAPE_GAP;
|
|
232
|
+
const titleGap =
|
|
233
|
+
previousKind === "station" && currentKind === "station"
|
|
234
|
+
? estimateStationTitleWidth(箱の題(previousActor)) / 2 +
|
|
235
|
+
estimateStationTitleWidth(箱の題(actor)) / 2 +
|
|
236
|
+
(actor.stationNamePosition === "bottom" ? BOTTOM_TITLE_GAP : TITLE_GAP)
|
|
237
|
+
: 0;
|
|
238
|
+
posX = previous.posX + Math.max(shapeGap, titleGap);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
const station = {
|
|
242
|
+
actorName: actor.name,
|
|
243
|
+
trackId: trackIdByKey.get(track) ?? `track-${index}`,
|
|
244
|
+
posX,
|
|
245
|
+
posY,
|
|
246
|
+
};
|
|
247
|
+
stations.push(station);
|
|
248
|
+
stationByActor.set(actor.name, station);
|
|
249
|
+
previousOnTrack = { actor, station };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const last = previousOnTrack;
|
|
253
|
+
const titleEndPadding =
|
|
254
|
+
last !== undefined && metroNodeKind(last.actor) === "station"
|
|
255
|
+
? estimateStationTitleWidth(箱の題(last.actor)) / 2 + 8
|
|
256
|
+
: 0;
|
|
257
|
+
const width = (last?.station.posX ?? 0) + Math.max(TRACK_END_PADDING, titleEndPadding);
|
|
258
|
+
const tracks = trackOrder.map((track, index): MetroTrack => ({
|
|
259
|
+
id: trackIdByKey.get(track) ?? `track-${index}`,
|
|
260
|
+
label: doc.lanes?.[track]?.label ?? track,
|
|
261
|
+
...(doc.lanes?.[track]?.subtitle !== undefined
|
|
262
|
+
? { subtitle: doc.lanes[track].subtitle }
|
|
263
|
+
: {}),
|
|
264
|
+
posX: 0,
|
|
265
|
+
posY: index * TRACK_HEIGHT,
|
|
266
|
+
posW: width,
|
|
267
|
+
posH: TRACK_HEIGHT,
|
|
268
|
+
}));
|
|
269
|
+
|
|
270
|
+
return { width, tracks, stations };
|
|
271
|
+
}
|
package/src/compile/mind.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { diagram } from "@cardenelabs/cdl";
|
|
2
2
|
import type { CdlDiagram } from "@cardenelabs/cdl";
|
|
3
3
|
import type { DslActor, DslDocument } from "../types";
|
|
4
|
-
import {
|
|
4
|
+
import { 図表の大きさ, 図表の大きさを読む } from "./chart-fields";
|
|
5
5
|
import { 矢印から親を決める, 放射に出す文字 } from "./hierarchy";
|
|
6
6
|
import type { CompileNotice } from "./notice";
|
|
7
7
|
import { slugify } from "./slug";
|
|
@@ -56,13 +56,27 @@ type 放射で描ける欄 =
|
|
|
56
56
|
/** 放射では描けない欄。 書かれていたら伝える */
|
|
57
57
|
type 放射で描けない欄 =
|
|
58
58
|
| "kind"
|
|
59
|
+
| "kindForm"
|
|
60
|
+
| "icon"
|
|
61
|
+
| "emptyRowsNote"
|
|
62
|
+
| "titleAlign"
|
|
63
|
+
| "rowRules"
|
|
64
|
+
| "rowDividerForm"
|
|
65
|
+
| "rowWell"
|
|
66
|
+
| "rowMarkForm"
|
|
67
|
+
| "nestIn"
|
|
68
|
+
| "figureCard"
|
|
59
69
|
| "eyebrow"
|
|
60
70
|
| "touchpoint"
|
|
61
71
|
| "opportunity"
|
|
72
|
+
| "opportunityPosition"
|
|
62
73
|
| "owner"
|
|
63
74
|
| "end"
|
|
75
|
+
| "at"
|
|
76
|
+
| "atPos"
|
|
64
77
|
| "rows"
|
|
65
78
|
| "marks"
|
|
79
|
+
| "stage"
|
|
66
80
|
| "lane"
|
|
67
81
|
| "stack"
|
|
68
82
|
| "initial"
|
|
@@ -90,7 +104,16 @@ type 放射で描けない欄 =
|
|
|
90
104
|
| "hBind"
|
|
91
105
|
| "opacity"
|
|
92
106
|
| "renderOffsetX"
|
|
93
|
-
| "renderOffsetY"
|
|
107
|
+
| "renderOffsetY"
|
|
108
|
+
| "stationNamePosition"
|
|
109
|
+
| "subtitlePlacement"
|
|
110
|
+
| "titleFontSize"
|
|
111
|
+
| "startLabel"
|
|
112
|
+
| "milestone"
|
|
113
|
+
| "emphasis"
|
|
114
|
+
| "markGap"
|
|
115
|
+
| "rowStripe"
|
|
116
|
+
| "rowLayout";
|
|
94
117
|
|
|
95
118
|
/** 引数が `never` でなければ型検査が落ちる */
|
|
96
119
|
type 空であること<T extends never> = T;
|
|
@@ -123,13 +146,32 @@ export type _放射の欄が重なっていない = 空であること<Extract<
|
|
|
123
146
|
*/
|
|
124
147
|
const 放射で描けない欄の名前: Record<放射で描けない欄, string> = {
|
|
125
148
|
kind: "種類",
|
|
149
|
+
kindForm: "部品箱の形",
|
|
150
|
+
icon: "部品箱の線画",
|
|
151
|
+
emptyRowsNote: "行のない表の注記",
|
|
152
|
+
titleAlign: "表の題の位置",
|
|
153
|
+
rowRules: "表の行の罫線",
|
|
154
|
+
rowStripe: "表の行の縞",
|
|
155
|
+
rowLayout: "表の題と行の位置",
|
|
156
|
+
rowDividerForm: "表の見出し下の罫線",
|
|
157
|
+
rowWell: "表の行の受け皿",
|
|
158
|
+
rowMarkForm: "表の印の形",
|
|
159
|
+
nestIn: "表の入れ子",
|
|
160
|
+
figureCard: "表・部品箱の足",
|
|
126
161
|
eyebrow: "上の小見出し",
|
|
127
|
-
touchpoint: "場所 (
|
|
128
|
-
opportunity: "改善の余地 (
|
|
162
|
+
touchpoint: "場所 (ユーザージャーニーの欄)",
|
|
163
|
+
opportunity: "改善の余地 (ユーザージャーニーの欄)",
|
|
164
|
+
opportunityPosition: "改善の余地の位置 (ユーザージャーニーの欄)",
|
|
129
165
|
owner: "担当 (工程の並びの欄)",
|
|
130
166
|
end: "終わる時期 (工程の並びの欄)",
|
|
167
|
+
startLabel: "始まりの札 (工程の並びの欄)",
|
|
168
|
+
milestone: "節目 (工程の並びの欄)",
|
|
169
|
+
emphasis: "主役 (工程の並びの欄)",
|
|
170
|
+
at: "点の位置 (四象限の欄)",
|
|
171
|
+
atPos: "点の位置 (四象限の欄)",
|
|
131
172
|
rows: "行",
|
|
132
173
|
marks: "印",
|
|
174
|
+
stage: "段階",
|
|
133
175
|
lane: "枠の指定",
|
|
134
176
|
stack: "積む順",
|
|
135
177
|
initial: "始まり / 終わり の印",
|
|
@@ -154,6 +196,10 @@ const 放射で描けない欄の名前: Record<放射で描けない欄, string
|
|
|
154
196
|
opacity: "濃さ",
|
|
155
197
|
renderOffsetX: "描く時のずらし",
|
|
156
198
|
renderOffsetY: "描く時のずらし",
|
|
199
|
+
stationNamePosition: "駅名の位置",
|
|
200
|
+
subtitlePlacement: "副題の位置",
|
|
201
|
+
titleFontSize: "題の字の大きさ",
|
|
202
|
+
markGap: "印までの間隔",
|
|
157
203
|
};
|
|
158
204
|
|
|
159
205
|
/**
|
|
@@ -190,7 +236,7 @@ export function compileMind(
|
|
|
190
236
|
): CdlDiagram {
|
|
191
237
|
// 図の型は描画側の組み立て関数 `mindMap()` と同じ綴り (cdl 0.63.0 で `mindmap` から `mind` に揃った)
|
|
192
238
|
const b = diagram(slugify(doc.title), { topic: doc.title, type: "mind" });
|
|
193
|
-
const { w: W, h: H } = 図表の大きさ.mind;
|
|
239
|
+
const { w: W, h: H } = 図表の大きさを読む(doc, 図表の大きさ.mind);
|
|
194
240
|
|
|
195
241
|
const 伝える = (kind: CompileNotice["kind"], 名: string, message: string, line = 0): void => {
|
|
196
242
|
onNotice?.({ kind, actor: 名, line, message });
|
|
@@ -338,6 +384,10 @@ export function compileMind(
|
|
|
338
384
|
branches,
|
|
339
385
|
};
|
|
340
386
|
})(),
|
|
387
|
+
...(doc.mindForm !== undefined ? { mindForm: doc.mindForm } : {}),
|
|
388
|
+
...(doc.mindSize !== undefined ? { mindSize: doc.mindSize } : {}),
|
|
389
|
+
...(doc.figureKindLabel !== undefined ? { figureKindLabel: doc.figureKindLabel } : {}),
|
|
390
|
+
...(doc.terminalDecoration !== undefined ? { terminalDecoration: doc.terminalDecoration } : {}),
|
|
341
391
|
});
|
|
342
392
|
return b.build();
|
|
343
393
|
}
|