@cardenelabs/cdl 0.52.0 → 0.54.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.
@@ -105,7 +105,7 @@ type Side = "top" | "right" | "bottom" | "left";
105
105
  * - "dashed" ... 刻んだ線。 **光っていても刻みのまま**。
106
106
  *
107
107
  * `"dashed"` が要るのは、`"solid"` の刻みが「光っていない」 を表す合図に使われているため。
108
- * 光ると実線に戻るので、線の種類として刻みを持たせられない。 UML の「満たす」 と「使う」、
108
+ * 光ると実線に戻るので、線の種類として刻みを持たせられない。 UML の「実装」 と「依存」、
109
109
  * ER の「識別しない」 のように **刻みが意味を持つ関係** はこちらを使う。
110
110
  */
111
111
  type EdgeStyle = "solid" | "dotted-flow" | "dashed";
@@ -139,8 +139,8 @@ declare const EDGE_STYLES: readonly EdgeStyle[];
139
139
  * 4 通りを 2 つの欄に分けないのは、**端の印は 1 つの記号として読まれる**ため。 欄を分けると
140
140
  * 片方だけ書いた形 (個数はあるが任意かが無い) を許すことになり、描けない組み合わせが増える。
141
141
  *
142
- * 線の種類と分けて持つのは、両者が独立に組み合わさるため。 実線 × 三角 (継ぐ) と
143
- * 破線 × 三角 (満たす) は線種で分かれ、実線 × 三角 (継ぐ) と実線 × 菱 (持つ) は端で分かれる。
142
+ * 線の種類と分けて持つのは、両者が独立に組み合わさるため。 実線 × 三角 (継承) と
143
+ * 破線 × 三角 (実装) は線種で分かれ、実線 × 三角 (継承) と実線 × 菱 (集約) は端で分かれる。
144
144
  * 1 つの欄に畳むと 2 軸の積を名前で列挙することになり、組合せが増えるたびに語が増える。
145
145
  */
146
146
  type EdgeHead = "none" | "triangle" | "diamond" | "open" | "crow" | "one" | "zero-one" | "many" | "zero-many";
@@ -154,7 +154,8 @@ declare const EDGE_HEADS: readonly EdgeHead[];
154
154
  * - `"solid"` ... 墨で塗る。 **既定**。 書かなければ形ごとの既定 (`EDGE_HEAD_SHAPE.filled`) に従う
155
155
  * - `"hollow"` ... 紙の色で塗り、輪郭だけを線で描く
156
156
  *
157
- * UML は同じ形を塗りで分ける = 白抜きの菱が「持つ」、塗った菱が「抱える」 (命が同じ)。
157
+ * UML は同じ形を塗りで分ける = 白抜きの菱が「集約」、塗った菱が「コンポジション」
158
+ * (部分が全体と寿命を共にする)。
158
159
  * 形を増やして表そうとすると、塗りの軸を持つ全ての形について語が倍になる。
159
160
  *
160
161
  * 塗らない形 (`"open"` / `"crow"`) には効かない。 元から輪郭しか無いため。
@@ -664,8 +665,8 @@ type CdlEdge = {
664
665
  /**
665
666
  * 端の印の塗り方 (#578)。 書かなければ形ごとの既定。 塗らない形には効かない。
666
667
  *
667
- * 形と分けて持つのは、UML が同じ形を塗りで分けるため (白抜きの菱 = 持つ /
668
- * 塗った菱 = 抱える)。
668
+ * 形と分けて持つのは、UML が同じ形を塗りで分けるため (白抜きの菱 = 集約 /
669
+ * 塗った菱 = コンポジション)。
669
670
  */
670
671
  headFill?: EdgeHeadFill;
671
672
  /**
@@ -105,7 +105,7 @@ type Side = "top" | "right" | "bottom" | "left";
105
105
  * - "dashed" ... 刻んだ線。 **光っていても刻みのまま**。
106
106
  *
107
107
  * `"dashed"` が要るのは、`"solid"` の刻みが「光っていない」 を表す合図に使われているため。
108
- * 光ると実線に戻るので、線の種類として刻みを持たせられない。 UML の「満たす」 と「使う」、
108
+ * 光ると実線に戻るので、線の種類として刻みを持たせられない。 UML の「実装」 と「依存」、
109
109
  * ER の「識別しない」 のように **刻みが意味を持つ関係** はこちらを使う。
110
110
  */
111
111
  type EdgeStyle = "solid" | "dotted-flow" | "dashed";
@@ -139,8 +139,8 @@ declare const EDGE_STYLES: readonly EdgeStyle[];
139
139
  * 4 通りを 2 つの欄に分けないのは、**端の印は 1 つの記号として読まれる**ため。 欄を分けると
140
140
  * 片方だけ書いた形 (個数はあるが任意かが無い) を許すことになり、描けない組み合わせが増える。
141
141
  *
142
- * 線の種類と分けて持つのは、両者が独立に組み合わさるため。 実線 × 三角 (継ぐ) と
143
- * 破線 × 三角 (満たす) は線種で分かれ、実線 × 三角 (継ぐ) と実線 × 菱 (持つ) は端で分かれる。
142
+ * 線の種類と分けて持つのは、両者が独立に組み合わさるため。 実線 × 三角 (継承) と
143
+ * 破線 × 三角 (実装) は線種で分かれ、実線 × 三角 (継承) と実線 × 菱 (集約) は端で分かれる。
144
144
  * 1 つの欄に畳むと 2 軸の積を名前で列挙することになり、組合せが増えるたびに語が増える。
145
145
  */
146
146
  type EdgeHead = "none" | "triangle" | "diamond" | "open" | "crow" | "one" | "zero-one" | "many" | "zero-many";
@@ -154,7 +154,8 @@ declare const EDGE_HEADS: readonly EdgeHead[];
154
154
  * - `"solid"` ... 墨で塗る。 **既定**。 書かなければ形ごとの既定 (`EDGE_HEAD_SHAPE.filled`) に従う
155
155
  * - `"hollow"` ... 紙の色で塗り、輪郭だけを線で描く
156
156
  *
157
- * UML は同じ形を塗りで分ける = 白抜きの菱が「持つ」、塗った菱が「抱える」 (命が同じ)。
157
+ * UML は同じ形を塗りで分ける = 白抜きの菱が「集約」、塗った菱が「コンポジション」
158
+ * (部分が全体と寿命を共にする)。
158
159
  * 形を増やして表そうとすると、塗りの軸を持つ全ての形について語が倍になる。
159
160
  *
160
161
  * 塗らない形 (`"open"` / `"crow"`) には効かない。 元から輪郭しか無いため。
@@ -664,8 +665,8 @@ type CdlEdge = {
664
665
  /**
665
666
  * 端の印の塗り方 (#578)。 書かなければ形ごとの既定。 塗らない形には効かない。
666
667
  *
667
- * 形と分けて持つのは、UML が同じ形を塗りで分けるため (白抜きの菱 = 持つ /
668
- * 塗った菱 = 抱える)。
668
+ * 形と分けて持つのは、UML が同じ形を塗りで分けるため (白抜きの菱 = 集約 /
669
+ * 塗った菱 = コンポジション)。
669
670
  */
670
671
  headFill?: EdgeHeadFill;
671
672
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cardenelabs/cdl",
3
- "version": "0.52.0",
3
+ "version": "0.54.0",
4
4
  "description": "CDL (Chainome Diagram Language). Mermaid-like declarative DSL that compiles to animated SVG diagrams. Built for blockchain / Solidity flows, generic enough for sequence / flow / state / ER / topology diagrams.",
5
5
  "license": "MIT",
6
6
  "author": "cardene777",
@@ -7,7 +7,7 @@
7
7
  * 端での線の向きで決める。 端から `端の字を戻す距離` だけ線を戻った点を基準に、線の脇へ置く。
8
8
  * 字は端に近い側の縁を基準点に揃える = 字の長さが変わっても、端から字までの隙間は変わらない。
9
9
  *
10
- * **線の札の反対側に置く**。 札と同じ側に置くと、札の語と多重度が続けて読まれる (「持つ」 の
10
+ * **線の札の反対側に置く**。 札と同じ側に置くと、札の語と多重度が続けて読まれる (「集約」 の
11
11
  * 隣に `1..*`、 英字の頃は「HAS1」)。 実測で、札と同じ側 (横向きなら上、縦向きなら右) に
12
12
  * 置く形は見本帳のクラス図で 1 本が札と重なり、 もう 1 本が札と 2 world まで寄った。
13
13
  * 札を持たない線は、横向きなら下、縦向きなら左に置く (engine が札を置く既定の側の反対)。
@@ -25,7 +25,7 @@ import { measureTextWidth } from "./text-width";
25
25
  * 端から字までの、線に沿った距離 (world)。
26
26
  *
27
27
  * 端の印の長さの最大 (光っている時の 14、 `render/stage.tsx` の marker の大きさ) に隙間 8 を足す。
28
- * 印を持たない端 (菱が出どころ側にある「持つ」 「抱える」 の行き先) でも同じ距離にする = 端ごとに
28
+ * 印を持たない端 (菱が出どころ側にある「集約」 「コンポジション」 の行き先) でも同じ距離にする = 端ごとに
29
29
  * 距離が変わると、図の中で多重度の位置が揃わない。
30
30
  */
31
31
  export const 端の字を戻す距離 = 22;
@@ -42,6 +42,30 @@ const 箱の高さ = Math.round(端の字の大きさ * 1.2);
42
42
  /** 字の大きさに対する baseline の深さ。 等幅の字の ascent が字の大きさの 8 割ほど */
43
43
  const BASELINE_RATIO = 0.8;
44
44
 
45
+ /**
46
+ * 両端に字がある線で、2 つの字の間に空ける長さ (world)。
47
+ *
48
+ * 字と自分の端の間 (`端の字を戻す距離`) の 2 倍にする = 字が自分の端より相手の字の近くに
49
+ * 見えなくなる。 実測で、間が 10 だった短い縦の線 (長さ 100) は 2 つの字が 2 行の札のように
50
+ * 読まれ、どちらの端の数かが分からなかった (#829)。
51
+ */
52
+ export const 両端の字の間 = 端の字を戻す距離 * 2;
53
+
54
+ /**
55
+ * 端の字を線の同じ側に並べた時、線に沿って要る長さ (world)。 字が無い線は 0。
56
+ *
57
+ * 字は端から `端の字を戻す距離` 戻った所から、線に沿って `箱の高さ` だけ占める (縦向きの時)。
58
+ * 両端にあれば、その 2 つの間に `両端の字の間` を足す。 縦に隣り合う箱の間隔を決める
59
+ * `layout/nodes.ts` が使う = 間隔が線の長さそのものになる形だけが対象。
60
+ */
61
+ export function 端の字に要る線の長さ(edge: { headLabel?: string; tailLabel?: string }): number {
62
+ const 一つ分 = 端の字を戻す距離 + 箱の高さ;
63
+ const 数 = [edge.headLabel, edge.tailLabel].filter((t) => t?.trim()).length;
64
+ if (数 === 0) return 0;
65
+ if (数 === 1) return 一つ分;
66
+ return 一つ分 * 2 + 両端の字の間;
67
+ }
68
+
45
69
  /** どちらの端か。 `head` = 行き先 (`to`)、 `tail` = 出どころ (`from`) */
46
70
  export type 線の端 = "head" | "tail";
47
71
 
@@ -1,5 +1,6 @@
1
1
  import { requiredRowsWidth } from "../kinds/row-align";
2
2
  import type { CdlDiagram, LaidLane, LaidNode } from "../types";
3
+ import { 端の字に要る線の長さ } from "./head-label";
3
4
  import {
4
5
  hasRenderedLabel,
5
6
  labelPillH,
@@ -138,9 +139,9 @@ function requiredStackGapForEdgeLabels(diag: CdlDiagram, allStacks: readonly num
138
139
 
139
140
  // 同じ隙間 (lane + 上側 row の rank) を通る label の pill 高を集める。
140
141
  const pillHsByGap = new Map<string, number[]>();
142
+ // 端の字 (#829) は線の端から測るので、隙間ごとに集めず線 1 本ずつの長さで足りる
143
+ let 端の字の要求 = 0;
141
144
  for (const e of diag.edges) {
142
- // `label` が空でも `sub` があれば pill は描かれ、 その高さ (68) の分だけ隙間が要る (Issue #224)。
143
- if (!hasRenderedLabel(e)) continue;
144
145
  const from = byId.get(e.from);
145
146
  const to = byId.get(e.to);
146
147
  if (!from || !to) continue;
@@ -149,11 +150,14 @@ function requiredStackGapForEdgeLabels(diag: CdlDiagram, allStacks: readonly num
149
150
  const toRank = rankOf.get(to.stack);
150
151
  if (fromRank === undefined || toRank === undefined) continue;
151
152
  if (Math.abs(fromRank - toRank) !== 1) continue;
153
+ 端の字の要求 = Math.max(端の字の要求, 端の字に要る線の長さ(e));
154
+ // `label` が空でも `sub` があれば pill は描かれ、 その高さ (68) の分だけ隙間が要る (Issue #224)。
155
+ if (!hasRenderedLabel(e)) continue;
152
156
  const key = `${from.lane}::${Math.min(fromRank, toRank)}`;
153
157
  pillHsByGap.set(key, [...(pillHsByGap.get(key) ?? []), labelPillH(e.sub)]);
154
158
  }
155
159
 
156
- let required = 0;
160
+ let required = 端の字の要求;
157
161
  for (const pillHs of pillHsByGap.values()) {
158
162
  required = Math.max(required, requiredVerticalLabelGap(pillHs));
159
163
  }
package/src/presets.ts CHANGED
@@ -345,7 +345,7 @@ export function flow(preset: FlowPreset): FlowBuilder {
345
345
  build() {
346
346
  b.phase(
347
347
  "flow",
348
- { duration: 2400, title: preset.topic, body: "全 step 順次実行。" },
348
+ { duration: 2400, title: preset.topic, body: "すべての段を上から順に通った流れ。" },
349
349
  (p) => p.activate(...phaseActivate).badge("flow"),
350
350
  );
351
351
  return b.build();
@@ -366,7 +366,7 @@ export function flow(preset: FlowPreset): FlowBuilder {
366
366
  * | `return` | 破線 | 開いた矢 | 呼ばれた側から戻る。 新しい仕事ではないので線を切る |
367
367
  * | `fire` | 実線 | 開いた矢 | 返事を待たない。 実線だが矢を閉じないことで待たないと示す |
368
368
  *
369
- * 印の形はクラス図の「継ぐ」 と「結ぶ」 で使った形そのもの。 **意味は違う** (あちらは親を
369
+ * 印の形はクラス図の「継承」 と「関連」 で使った形そのもの。 **意味は違う** (あちらは親を
370
370
  * 指す / こちらは返事を待つか) が、形の並びは共有できる。
371
371
  */
372
372
  export type SequenceMessageKind = "call" | "return" | "fire";
@@ -517,7 +517,7 @@ export function sequence(preset: SequencePreset): SequenceBuilder {
517
517
  });
518
518
  b.phase(
519
519
  "sequence",
520
- { duration: 2400, title: preset.topic, body: "sequence の全 message を時系列展開。" },
520
+ { duration: 2400, title: preset.topic, body: "すべてのやり取りを時間の順に並べた姿。" },
521
521
  (p) => p.activate(preset.id).badge("sequence"),
522
522
  );
523
523
  return b.build();
@@ -653,7 +653,7 @@ export function topology(preset: TopologyPreset): TopologyBuilder {
653
653
  build() {
654
654
  b.phase(
655
655
  "topo",
656
- { duration: 2400, title: preset.topic, body: "topology の全 container + connection を visible 化。" },
656
+ { duration: 2400, title: preset.topic, body: "すべての構成要素と接続を並べた構成。" },
657
657
  (p) => p.activate(...phaseActivate).badge("topology"),
658
658
  );
659
659
  return b.build();
@@ -883,7 +883,7 @@ export function er(preset: ErPreset): ErBuilder {
883
883
  build() {
884
884
  b.phase(
885
885
  "er",
886
- { duration: 2400, title: preset.topic, body: "ER 図の全 entity + relation を visible 化。" },
886
+ { duration: 2400, title: preset.topic, body: "すべての表と関係を並べた構造。" },
887
887
  (p) => p.activate(...phaseActivate).badge("er"),
888
888
  );
889
889
  return b.build();
@@ -1158,7 +1158,7 @@ export function stateMachine(preset: StateMachinePreset): StateMachineBuilder {
1158
1158
 
1159
1159
  b.phase(
1160
1160
  "fsm",
1161
- { duration: 2400, title: preset.topic, body: "FSM の全 state + transition を visible 化。" },
1161
+ { duration: 2400, title: preset.topic, body: "すべての状態と遷移を並べた姿。" },
1162
1162
  (p) => p.activate(...phaseActivate).badge("fsm"),
1163
1163
  );
1164
1164
  return b.build();
@@ -1291,7 +1291,7 @@ export function infrastructure(preset: InfrastructurePreset): InfrastructureBuil
1291
1291
  build() {
1292
1292
  b.phase(
1293
1293
  "infra",
1294
- { duration: 2400, title: preset.topic, body: "infrastructure 全 node + connection を visible 化。" },
1294
+ { duration: 2400, title: preset.topic, body: "すべての構成要素と接続を並べた構成。" },
1295
1295
  (p) => p.activate(...phaseActivate).badge("infrastructure"),
1296
1296
  );
1297
1297
  return b.build();
@@ -1322,12 +1322,12 @@ export type ClassRelationType =
1322
1322
  *
1323
1323
  * | 種類 | 線 | 形 | 塗り | 側 | 意味 |
1324
1324
  * |---|---|---|---|---|---|
1325
- * | `extends` | 実線 | 三角 | 白抜き | 行き先 | 継ぐ |
1326
- * | `implements` | 破線 | 三角 | 白抜き | 行き先 | 満たす |
1327
- * | `aggregates` | 実線 | 菱 | 白抜き | 出どころ | 持つ |
1328
- * | `composes` | 実線 | 菱 | 塗る | 出どころ | 抱える |
1329
- * | `associates` | 実線 | 開いた矢 | — | 行き先 | 結ぶ |
1330
- * | `uses` | 破線 | 開いた矢 | — | 行き先 | 使う |
1325
+ * | `extends` | 実線 | 三角 | 白抜き | 行き先 | 継承 |
1326
+ * | `implements` | 破線 | 三角 | 白抜き | 行き先 | 実装 |
1327
+ * | `aggregates` | 実線 | 菱 | 白抜き | 出どころ | 集約 |
1328
+ * | `composes` | 実線 | 菱 | 塗る | 出どころ | コンポジション |
1329
+ * | `associates` | 実線 | 開いた矢 | — | 行き先 | 関連 |
1330
+ * | `uses` | 破線 | 開いた矢 | — | 行き先 | 依存 |
1331
1331
  *
1332
1332
  * **側が種で割れるのが要点**。 三角は「この先が親」 を指すので行き先に立ち、菱は
1333
1333
  * 「この箱が持っている」 を指すので出どころに立つ。 1 つの決まりで書こうとするとどちらかが
@@ -1594,7 +1594,7 @@ export function classDiagram(preset: ClassDiagramPreset): ClassDiagramBuilder {
1594
1594
 
1595
1595
  b.phase(
1596
1596
  "class",
1597
- { duration: 2400, title: preset.topic, body: "UML class 全 class + relation を visible 化。" },
1597
+ { duration: 2400, title: preset.topic, body: "すべてのクラスと関係を並べた構造。" },
1598
1598
  (p) => p.activate(...phaseActivate).badge("class"),
1599
1599
  );
1600
1600
  return b.build();
@@ -1673,7 +1673,7 @@ export function tree(preset: TreePreset): TreeBuilder {
1673
1673
  });
1674
1674
  b.phase(
1675
1675
  "tree",
1676
- { duration: 2400, title: preset.topic, body: "tree の全 node + parent-child edge を hierarchical layout で描画。" },
1676
+ { duration: 2400, title: preset.topic, body: "すべての項目を親子の階層で並べた木。" },
1677
1677
  (p) => p.activate(nodeId).badge("tree"),
1678
1678
  );
1679
1679
  return b.build();
@@ -1756,7 +1756,7 @@ export function userJourney(preset: UserJourneyPreset): UserJourneyBuilder {
1756
1756
  });
1757
1757
  b.phase(
1758
1758
  "journey",
1759
- { duration: 2400, title: preset.topic, body: "userJourney 全 step を感情曲線 + touchpoint で描画。" },
1759
+ { duration: 2400, title: preset.topic, body: "すべての段階を、気持ちの曲線と接点で並べた体験。" },
1760
1760
  (p) => p.activate(nodeId).badge("journey"),
1761
1761
  );
1762
1762
  return b.build();
@@ -1844,7 +1844,7 @@ export function mindMap(preset: MindMapPreset): MindMapBuilder {
1844
1844
  });
1845
1845
  b.phase(
1846
1846
  "mindmap",
1847
- { duration: 2400, title: preset.topic, body: "mindMap 全 branch を visible 化、 中心 + 放射で描画。" },
1847
+ { duration: 2400, title: preset.topic, body: "中心の主題から、すべての枝を放射状に広げた姿。" },
1848
1848
  (p) => p.activate(nodeId).badge("mindmap"),
1849
1849
  );
1850
1850
  return b.build();
@@ -1920,7 +1920,7 @@ export function funnel(preset: FunnelPreset): FunnelBuilder {
1920
1920
  });
1921
1921
  b.phase(
1922
1922
  "funnel",
1923
- { duration: 2400, title: preset.topic, body: "funnel 全 stage を逆三角形 + count で描画。" },
1923
+ { duration: 2400, title: preset.topic, body: "すべての段階を、人数とともに上から絞り込んだ姿。" },
1924
1924
  (p) => p.activate(nodeId).badge("funnel"),
1925
1925
  );
1926
1926
  return b.build();
@@ -2018,7 +2018,7 @@ export function quadrant(preset: QuadrantPreset): QuadrantBuilder {
2018
2018
  });
2019
2019
  b.phase(
2020
2020
  "quadrant",
2021
- { duration: 2400, title: preset.topic, body: "quadrant 十字軸 + 4 象限 + item を描画。" },
2021
+ { duration: 2400, title: preset.topic, body: "2 本の軸で分けた 4 つの枠に、すべての項目を置いた姿。" },
2022
2022
  (p) => p.activate(nodeId).badge("quadrant"),
2023
2023
  );
2024
2024
  return b.build();
@@ -2134,7 +2134,7 @@ export function chart(preset: ChartPreset): ChartBuilder {
2134
2134
  });
2135
2135
  b.phase(
2136
2136
  "chart",
2137
- { duration: 2400, title: preset.topic, body: `${preset.type} chart の全 datum を visible 化。` },
2137
+ { duration: 2400, title: preset.topic, body: "すべての値を描いたグラフ。" },
2138
2138
  (p) => p.activate(nodeId).badge(preset.type),
2139
2139
  );
2140
2140
  return b.build();
@@ -2254,7 +2254,7 @@ export function gantt(preset: GanttPreset): GanttBuilder {
2254
2254
  });
2255
2255
  b.phase(
2256
2256
  "gantt",
2257
- { duration: 2400, title: preset.topic, body: "gantt 全 task を visible 化、 帯状 timeline + 依存 arrow で描画。" },
2257
+ { duration: 2400, title: preset.topic, body: "すべての作業を、期間の帯と前後関係の矢印で並べた工程。" },
2258
2258
  (p) => p.activate(nodeId).badge("gantt"),
2259
2259
  );
2260
2260
  return b.build();
@@ -2307,12 +2307,16 @@ const SHAPE_KIND: Record<FlowchartNodeShape, NodeKind> = {
2307
2307
  loop: "card",
2308
2308
  };
2309
2309
 
2310
+ /**
2311
+ * 箱の上に出す形の名前 (#835)。 流れ図の記号の呼び方 (JIS X 0121 の 処理 / 判断 / 端子) に合わせる。
2312
+ * 端子は始まりと終わりで分けて書く = 同じ「端子」 では、どちらの端かが読めない。
2313
+ */
2310
2314
  const SHAPE_EYEBROW: Record<FlowchartNodeShape, string> = {
2311
- process: "process",
2312
- decision: "decision",
2313
- start: "start",
2314
- end: "end",
2315
- loop: "loop",
2315
+ process: "処理",
2316
+ decision: "判断",
2317
+ start: "開始",
2318
+ end: "終了",
2319
+ loop: "繰り返し",
2316
2320
  };
2317
2321
 
2318
2322
  /**
@@ -2381,7 +2385,7 @@ export function flowchart(preset: FlowchartPreset): FlowchartBuilder {
2381
2385
  build() {
2382
2386
  b.phase(
2383
2387
  "flowchart",
2384
- { duration: 2400, title: preset.topic, body: "flowchart 全 node + edge を visible 化、 swimlane + decision を表現。" },
2388
+ { duration: 2400, title: preset.topic, body: "すべての処理と分岐を、担当ごとのレーンに並べた流れ。" },
2385
2389
  (p) => p.activate(...phaseActivate).badge("flowchart"),
2386
2390
  );
2387
2391
  return b.build();
@@ -2434,6 +2438,15 @@ const NET_KIND_MAP: Record<NetworkDeviceKind, NodeKind> = {
2434
2438
  client: "actor",
2435
2439
  };
2436
2440
 
2441
+ /** 箱の上に出す機器の呼び名 (#835)。 種類の英語をそのまま出すと、日本語の図の中で浮く */
2442
+ const NET_KIND_LABEL: Record<NetworkDeviceKind, string> = {
2443
+ router: "ルーター",
2444
+ switch: "スイッチ",
2445
+ firewall: "ファイアウォール",
2446
+ server: "サーバー",
2447
+ client: "端末",
2448
+ };
2449
+
2437
2450
  /**
2438
2451
  * network preset ... NW topology (router / switch / firewall / server / client) を col × row 配置、
2439
2452
  * link で接続 (protocol / VLAN を label)。 On-prem / hybrid NW 構成図向け。
@@ -2464,7 +2477,7 @@ export function network(preset: NetworkPreset): NetworkBuilder {
2464
2477
  b.lane(laneId, { width: laneW });
2465
2478
  colLaneIds.set(d.col, laneId);
2466
2479
  }
2467
- const eyebrowParts: string[] = [d.kind];
2480
+ const eyebrowParts: string[] = [NET_KIND_LABEL[d.kind]];
2468
2481
  if (d.segment) eyebrowParts.push(d.segment);
2469
2482
  b.node(d.id, {
2470
2483
  lane: laneId,
@@ -2490,7 +2503,7 @@ export function network(preset: NetworkPreset): NetworkBuilder {
2490
2503
  build() {
2491
2504
  b.phase(
2492
2505
  "network",
2493
- { duration: 2400, title: preset.topic, body: "network 全 device + link を visible 化。" },
2506
+ { duration: 2400, title: preset.topic, body: "すべての機器と接続を並べた構成。" },
2494
2507
  (p) => p.activate(...phaseActivate).badge("network"),
2495
2508
  );
2496
2509
  return b.build();
@@ -2572,19 +2585,25 @@ export function stateMachine2(preset: StateMachine2Preset): StateMachine2Builder
2572
2585
  const stateOrder = new Map<string, number>();
2573
2586
  const phaseActivate: string[] = [];
2574
2587
  let transIdx = 0;
2588
+ // 状態の題と親 (#835)。 入れ子の札は親の題で書くので、親が後から宣言されても引けるよう
2589
+ // 札は `build()` で決める
2590
+ const 題 = new Map<string, string>();
2591
+ const 親 = new Map<string, string>();
2575
2592
 
2576
2593
  const api: StateMachine2Builder = {
2577
2594
  state(s) {
2578
2595
  const laneId = `lane-${s.id}`;
2579
2596
  b.lane(laneId, { width: laneW });
2597
+ 題.set(s.id, s.title);
2598
+ if (s.parent) 親.set(s.id, s.parent);
2580
2599
  const eyebrowParts: string[] = [];
2581
2600
  if (s.initial) eyebrowParts.push("初期");
2582
2601
  else if (s.final) eyebrowParts.push("最終");
2583
2602
  else eyebrowParts.push("状態");
2584
- if (s.parent) eyebrowParts.push(`nested in ${s.parent}`);
2603
+ // 入る時と出る時の呼び方は `types.ts` の行頭の印の説明と揃える
2585
2604
  const subtitleParts: string[] = [];
2586
- if (s.entry) subtitleParts.push(`entry: ${s.entry}`);
2587
- if (s.exit) subtitleParts.push(`exit: ${s.exit}`);
2605
+ if (s.entry) subtitleParts.push(`入る時: ${s.entry}`);
2606
+ if (s.exit) subtitleParts.push(`出る時: ${s.exit}`);
2588
2607
  b.node(s.id, {
2589
2608
  lane: laneId,
2590
2609
  stack: 0,
@@ -2620,10 +2639,24 @@ export function stateMachine2(preset: StateMachine2Preset): StateMachine2Builder
2620
2639
  build() {
2621
2640
  b.phase(
2622
2641
  "sm2",
2623
- { duration: 2400, title: preset.topic, body: "拡張 FSM (nested + action) を visible 化。" },
2642
+ {
2643
+ duration: 2400,
2644
+ title: preset.topic,
2645
+ body: "入れ子の状態と、入る時・出る時の処理を含めた、すべての状態と遷移。",
2646
+ },
2624
2647
  (p) => p.activate(...phaseActivate).badge("statemachine2"),
2625
2648
  );
2626
- return b.build();
2649
+ const d = b.build();
2650
+ // 入れ子の札に親の題を足す。 識別子 (`active`) を出すと、図のどこにも無い字になる
2651
+ return {
2652
+ ...d,
2653
+ nodes: d.nodes.map((n) => {
2654
+ const 親の識別子 = 親.get(n.id);
2655
+ if (親の識別子 === undefined) return n;
2656
+ const 親の題 = 題.get(親の識別子) ?? 親の識別子;
2657
+ return { ...n, eyebrow: `${n.eyebrow ?? "状態"} / ${親の題}の中` };
2658
+ }),
2659
+ };
2627
2660
  },
2628
2661
  };
2629
2662
 
@@ -6,7 +6,7 @@
6
6
  * 問いにその場で答えられる。
7
7
  *
8
8
  * 集合は 3 つで作る = 触れた箱 / その箱に繋がる線 / 線の相手の箱。
9
- * **線の向きは問わない** = クラス図の「継ぐ」 は子から親へ、 ER の「持つ」 は親から子へ向くが、
9
+ * **線の向きは問わない** = クラス図の「継承」 は子から親へ、 ER の「注文する」 は利用者から注文へ向くが、
10
10
  * 読み手にとってはどちらも「この箱の関係」 で、向きで見え方が変わる理由が無い。
11
11
  */
12
12
 
package/src/types.ts CHANGED
@@ -130,7 +130,7 @@ export type Side = "top" | "right" | "bottom" | "left";
130
130
  * - "dashed" ... 刻んだ線。 **光っていても刻みのまま**。
131
131
  *
132
132
  * `"dashed"` が要るのは、`"solid"` の刻みが「光っていない」 を表す合図に使われているため。
133
- * 光ると実線に戻るので、線の種類として刻みを持たせられない。 UML の「満たす」 と「使う」、
133
+ * 光ると実線に戻るので、線の種類として刻みを持たせられない。 UML の「実装」 と「依存」、
134
134
  * ER の「識別しない」 のように **刻みが意味を持つ関係** はこちらを使う。
135
135
  */
136
136
  export type EdgeStyle = "solid" | "dotted-flow" | "dashed";
@@ -167,8 +167,8 @@ export const EDGE_STYLES: readonly EdgeStyle[] = ["solid", "dotted-flow", "dashe
167
167
  * 4 通りを 2 つの欄に分けないのは、**端の印は 1 つの記号として読まれる**ため。 欄を分けると
168
168
  * 片方だけ書いた形 (個数はあるが任意かが無い) を許すことになり、描けない組み合わせが増える。
169
169
  *
170
- * 線の種類と分けて持つのは、両者が独立に組み合わさるため。 実線 × 三角 (継ぐ) と
171
- * 破線 × 三角 (満たす) は線種で分かれ、実線 × 三角 (継ぐ) と実線 × 菱 (持つ) は端で分かれる。
170
+ * 線の種類と分けて持つのは、両者が独立に組み合わさるため。 実線 × 三角 (継承) と
171
+ * 破線 × 三角 (実装) は線種で分かれ、実線 × 三角 (継承) と実線 × 菱 (集約) は端で分かれる。
172
172
  * 1 つの欄に畳むと 2 軸の積を名前で列挙することになり、組合せが増えるたびに語が増える。
173
173
  */
174
174
  export type EdgeHead =
@@ -204,7 +204,8 @@ export const EDGE_HEADS: readonly EdgeHead[] = [
204
204
  * - `"solid"` ... 墨で塗る。 **既定**。 書かなければ形ごとの既定 (`EDGE_HEAD_SHAPE.filled`) に従う
205
205
  * - `"hollow"` ... 紙の色で塗り、輪郭だけを線で描く
206
206
  *
207
- * UML は同じ形を塗りで分ける = 白抜きの菱が「持つ」、塗った菱が「抱える」 (命が同じ)。
207
+ * UML は同じ形を塗りで分ける = 白抜きの菱が「集約」、塗った菱が「コンポジション」
208
+ * (部分が全体と寿命を共にする)。
208
209
  * 形を増やして表そうとすると、塗りの軸を持つ全ての形について語が倍になる。
209
210
  *
210
211
  * 塗らない形 (`"open"` / `"crow"`) には効かない。 元から輪郭しか無いため。
@@ -736,8 +737,8 @@ export type CdlEdge = {
736
737
  /**
737
738
  * 端の印の塗り方 (#578)。 書かなければ形ごとの既定。 塗らない形には効かない。
738
739
  *
739
- * 形と分けて持つのは、UML が同じ形を塗りで分けるため (白抜きの菱 = 持つ /
740
- * 塗った菱 = 抱える)。
740
+ * 形と分けて持つのは、UML が同じ形を塗りで分けるため (白抜きの菱 = 集約 /
741
+ * 塗った菱 = コンポジション)。
741
742
  */
742
743
  headFill?: EdgeHeadFill;
743
744
  /**