@cardenelabs/dragon 0.11.0 → 0.13.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/src/compile.ts CHANGED
@@ -9,16 +9,41 @@
9
9
  * v0.2 ... 6 preset 全対応 (sequence / flow / swimlane / er / state / topology)
10
10
  */
11
11
 
12
- import type { DslActor, DslDocument, DslLane, DslPhase, DslStep, DslValue, PresetType } from "./types";
13
- import type { CdlDiagram, CdlEdge, ErRelationCardinality, FormulaAst, LaidDiagram } from "@cardenelabs/cdl";
12
+ import type {
13
+ DslActor,
14
+ DslDocument,
15
+ DslLane,
16
+ DslPhase,
17
+ DslStep,
18
+ DslValue,
19
+ PresetType,
20
+ } from "./types";
21
+ import type {
22
+ CdlDiagram,
23
+ CdlEdge,
24
+ ErRelationCardinality,
25
+ FormulaAst,
26
+ LaidDiagram,
27
+ } from "@cardenelabs/cdl";
14
28
  import {
15
- sequence, flow, swimlane, er, stateMachine, topology, diagram, layout,
16
- rendersRows, requiredRowsHeight, requiredRowsWidth, NODE_KINDS,
17
- applyDerivedValues, parseFormula,
29
+ sequence,
30
+ flow,
31
+ swimlane,
32
+ er,
33
+ stateMachine,
34
+ topology,
35
+ diagram,
36
+ layout,
37
+ rendersRows,
38
+ requiredRowsHeight,
39
+ requiredRowsWidth,
40
+ NODE_KINDS,
41
+ applyDerivedValues,
42
+ parseFormula,
18
43
  } from "@cardenelabs/cdl";
19
44
  import { parseFocusEntry } from "./focus";
20
45
  import { DRAW_TARGETS } from "./v05/parser";
21
- import { isColorValue, stripExternalPaint } from "./color";
46
+ import { isColorValue, pointsOutside, stripExternalPaint } from "./color";
22
47
  import {
23
48
  MAX_INPUT_ELEMENTS,
24
49
  countDiagramElements,
@@ -279,12 +304,24 @@ export function compileToCdl(doc: DslDocument, opts?: CompileToCdlOpts): CdlDiag
279
304
  // 扱いが式形と揃う。 時計の状態と段の補間もここで足す
280
305
  const 畳んだきっかけ = foldValueTriggers(merged, doc, opts?.onNotice);
281
306
  attachDerivedValues(merged, doc, opts?.onNotice, inheritedDerivedSourceLines, 畳んだきっかけ);
307
+ // 値を見せる部品を図に載せる (#1374)。 parts が持つ部品は merge 済みなので残したまま足す。
308
+ // **外部参照を落とす前に載せる**。 後から足すと readout の color / colors だけが出口の検査を
309
+ // 迂回し、 `url(https://...)` がそのまま SVG の paint 属性へ届く。
310
+ if (doc.readouts && doc.readouts.length > 0) {
311
+ // 出口の paint 検査は diagram を直接書き換える。 doc の object を共有すると、検査が
312
+ // compileToCdl の入力まで書き換えて入力不変性を壊すため、nested field も含めて写す。
313
+ const ownReadouts = doc.readouts.map(
314
+ (readout) => deepRewriteStrings(readout, (value) => value) as typeof readout,
315
+ );
316
+ merged.readouts = [...(merged.readouts ?? []), ...ownReadouts];
317
+ }
282
318
  // 図の外を指す値を、 色を塗る位置から落とす (#1004)。
283
319
  //
284
320
  // 入口ごとに塞ぐ形は採らない。 状態の上書き / phase が入れる値 / 画面が直接書く背景色 /
285
321
  // 埋め込んだ JSON / states / values と入口が複数あり、 1 つ見落とすと穴が残る。
286
322
  // **図への追加を全て終えた後**、 出口で 1 度だけ見る。 この後に状態を足すと検査を迂回する。
287
- for (const dropped of stripExternalPaint(merged)) {
323
+ const 外した部品の配色 = stripExternalReadoutPalettes(merged);
324
+ for (const dropped of [...stripExternalPaint(merged), ...外した部品の配色]) {
288
325
  opts?.onNotice?.({
289
326
  kind: "external-paint-dropped",
290
327
  actor: dropped.path,
@@ -299,6 +336,29 @@ export function compileToCdl(doc: DslDocument, opts?: CompileToCdlOpts): CdlDiag
299
336
  return merged;
300
337
  }
301
338
 
339
+ /**
340
+ * readout の配色配列から外部参照を落とす (#1374)。
341
+ *
342
+ * 共通の `stripExternalPaint` は `color` / `fill` のような key を見るが、 `colors` の中へ
343
+ * 入ると配列要素には key が無い。 `heat-cell` が公開した配色だけはここで要素ごとに閉じる。
344
+ */
345
+ function stripExternalReadoutPalettes(diagram: CdlDiagram): Array<{ path: string; value: string }> {
346
+ const stripped: Array<{ path: string; value: string }> = [];
347
+ for (let i = 0; i < (diagram.readouts?.length ?? 0); i += 1) {
348
+ const readout = diagram.readouts?.[i] as { colors?: readonly string[] } | undefined;
349
+ if (!readout?.colors) continue;
350
+ const colors = [...readout.colors];
351
+ for (let j = 0; j < colors.length; j += 1) {
352
+ const color = colors[j];
353
+ if (color === undefined || !pointsOutside(color)) continue;
354
+ colors[j] = "none";
355
+ stripped.push({ path: `readouts[${i}].colors[${j}]`, value: color });
356
+ }
357
+ readout.colors = colors;
358
+ }
359
+ return stripped;
360
+ }
361
+
302
362
  /**
303
363
  * 書いた状態 (`states:`) を図に載せる (#1162)。
304
364
  *
@@ -907,7 +967,9 @@ function disambiguateActorIds(
907
967
  }
908
968
  const 新しい名前 = new Map<string, string>();
909
969
  // 名前で並べてから配る = 書いた順に依らない
910
- for (const a of [...作り替える].sort((x, y) => (x.name < y.name ? -1 : x.name > y.name ? 1 : 0))) {
970
+ for (const a of [...作り替える].sort((x, y) =>
971
+ x.name < y.name ? -1 : x.name > y.name ? 1 : 0,
972
+ )) {
911
973
  const 尾 = 名前の尾(a.name);
912
974
  // **id の長さの上限のぶん、 元の名前を先に切る** (Round 1 の指摘)。 切らないと尾が
913
975
  // 64 字で落ちて、 同じ頭を持つ長い名前どうしが元のまま重なる
@@ -1453,10 +1515,7 @@ function applyCanvasPivotPositions(diagram: CdlDiagram, doc: DslDocument): void
1453
1515
  for (const [subKey, override] of Object.entries(actor.nodes)) {
1454
1516
  if (override.posX === undefined || override.posY === undefined) continue;
1455
1517
  for (const node of diagram.nodes) {
1456
- if (
1457
- node.id === `${aliasSlug}-${subKey}` ||
1458
- node.id === `${subKey}-${aliasSlug}`
1459
- ) {
1518
+ if (node.id === `${aliasSlug}-${subKey}` || node.id === `${subKey}-${aliasSlug}`) {
1460
1519
  node.posX = override.posX;
1461
1520
  node.posY = override.posY;
1462
1521
  if (override.posW !== undefined) node.posW = override.posW;
@@ -1684,8 +1743,6 @@ export function partTargetScale(
1684
1743
  return partScaleFactor(part, targetW, targetH, undefined);
1685
1744
  }
1686
1745
 
1687
-
1688
-
1689
1746
  /**
1690
1747
  * パーツ 1 個が図の上で占める外接矩形。
1691
1748
  *
@@ -2009,8 +2066,7 @@ export function partsGridCenters(
2009
2066
  if (items.length === 0) return out;
2010
2067
  // 公開している関数なので、 呼出側が渡す値を入口で閉じる。 数でない箱の数や桁溢れを
2011
2068
  // そのまま計算に入れると、 描けない座標を返すことになる
2012
- const safeCount =
2013
- Number.isSafeInteger(baseNodeCount) && baseNodeCount >= 0 ? baseNodeCount : 0;
2069
+ const safeCount = Number.isSafeInteger(baseNodeCount) && baseNodeCount >= 0 ? baseNodeCount : 0;
2014
2070
  const top = safeCount * STACK_PITCH + PARTS_GAP * 2;
2015
2071
  // 同じ名前が 2 度来たら先の方だけを見る。 後の分を残すと、 どちらを指したか決められない
2016
2072
  // まま列の送り幅にも影響する
@@ -2212,9 +2268,7 @@ function partBoxes(
2212
2268
  const t = partTargetSize(part, a.posW, a.posH, a.scale);
2213
2269
  const size = partExtent(part, t.w, t.h);
2214
2270
  const placed =
2215
- a.posX !== undefined && a.posY !== undefined
2216
- ? { cx: a.posX, cy: a.posY }
2217
- : grid.get(a.name);
2271
+ a.posX !== undefined && a.posY !== undefined ? { cx: a.posX, cy: a.posY } : grid.get(a.name);
2218
2272
  // 相対で書いた分はここでは決まらない (解決側が後で埋める)
2219
2273
  if (!placed) continue;
2220
2274
  // 渡す座標は段の中心。 矩形の中心はそこからずれる
@@ -2234,68 +2288,68 @@ function cleanupPlaceholderActor(
2234
2288
  doc: DslDocument,
2235
2289
  a: { name: string; lane?: string },
2236
2290
  ): void {
2237
- const aliasSlug = slugify(a.name);
2238
- const ownedLaneIds = new Set<string>();
2239
- if (doc.type === "sequence" || doc.type === "solidity") {
2240
- for (const l of target.lanes) {
2241
- // 明示 lane mapping (a.lane) 先は part の張替え先で actor 専用 lane ではないため除外
2242
- if (a.lane !== undefined && l.id === a.lane) continue;
2243
- if (l.label === a.name) ownedLaneIds.add(l.id);
2244
- }
2291
+ const aliasSlug = slugify(a.name);
2292
+ const ownedLaneIds = new Set<string>();
2293
+ if (doc.type === "sequence" || doc.type === "solidity") {
2294
+ for (const l of target.lanes) {
2295
+ // 明示 lane mapping (a.lane) 先は part の張替え先で actor 専用 lane ではないため除外
2296
+ if (a.lane !== undefined && l.id === a.lane) continue;
2297
+ if (l.label === a.name) ownedLaneIds.add(l.id);
2245
2298
  }
2246
- const ownedNodeIds = new Set<string>();
2247
- for (const n of target.nodes) {
2248
- if (ownedLaneIds.has(n.lane)) ownedNodeIds.add(n.id);
2249
- }
2250
- // actor 専用 lane を引き当てられない経路 (flow / topology 等の共有 lane preset) は従来どおり dragon
2251
- // slug prefix match fallback する。 これらは 1 actor = 1 node (id = slug) の生成規則。
2252
- const matchesAliasSlug = (id: string): boolean => {
2253
- if (id === aliasSlug) return true;
2254
- if (id.startsWith(`${aliasSlug}-`)) return true;
2255
- // sequence step anchor = `s{N}-{aliasSlug}` pattern
2256
- if (/^s\d+-/.test(id) && id.endsWith(`-${aliasSlug}`)) return true;
2257
- return false;
2258
- };
2259
- const relatedToActor = (id: string): boolean =>
2260
- ownedLaneIds.size > 0 ? ownedNodeIds.has(id) : matchesAliasSlug(id);
2261
- target.nodes = target.nodes.filter((n) => !relatedToActor(n.id));
2262
- // edge も同経路で削除 (parts actor に接続していた flow を除去、 parts merge 後の flow は user が
2263
- // 別途書く経路になる)。 削除した edge id phase.activate に残ると dangling 参照になるため回収する。
2264
- const removedEdgeIds = new Set<string>();
2265
- target.edges = target.edges.filter((e) => {
2266
- const drop = relatedToActor(e.from) || relatedToActor(e.to);
2267
- if (drop) removedEdgeIds.add(e.id);
2268
- return !drop;
2299
+ }
2300
+ const ownedNodeIds = new Set<string>();
2301
+ for (const n of target.nodes) {
2302
+ if (ownedLaneIds.has(n.lane)) ownedNodeIds.add(n.id);
2303
+ }
2304
+ // actor 専用 lane を引き当てられない経路 (flow / topology 等の共有 lane preset) は従来どおり dragon
2305
+ // slug の prefix match に fallback する。 これらは 1 actor = 1 node (id = slug) の生成規則。
2306
+ const matchesAliasSlug = (id: string): boolean => {
2307
+ if (id === aliasSlug) return true;
2308
+ if (id.startsWith(`${aliasSlug}-`)) return true;
2309
+ // sequence step anchor = `s{N}-{aliasSlug}` pattern
2310
+ if (/^s\d+-/.test(id) && id.endsWith(`-${aliasSlug}`)) return true;
2311
+ return false;
2312
+ };
2313
+ const relatedToActor = (id: string): boolean =>
2314
+ ownedLaneIds.size > 0 ? ownedNodeIds.has(id) : matchesAliasSlug(id);
2315
+ target.nodes = target.nodes.filter((n) => !relatedToActor(n.id));
2316
+ // edge も同経路で削除 (parts actor に接続していた flow を除去、 parts merge 後の flow は user が
2317
+ // 別途書く経路になる)。 削除した edge の id は phase.activate に残ると dangling 参照になるため回収する。
2318
+ const removedEdgeIds = new Set<string>();
2319
+ target.edges = target.edges.filter((e) => {
2320
+ const drop = relatedToActor(e.from) || relatedToActor(e.to);
2321
+ if (drop) removedEdgeIds.add(e.id);
2322
+ return !drop;
2323
+ });
2324
+ // lane も削除 = sequence preset は parts actor 用に lane (id = aliasSlug、 label = actor 名) を
2325
+ // 生成する。 node/edge だけ消して lane を残すと、 merge 後の part 側 lane (label = alias) と 2 本が
2326
+ // 同じ label を lane-label として描画し二重表示になる (actor ラベル二重表示 bug の root cause)。
2327
+ //
2328
+ // 削除は seq-like preset (sequence / solidity = compileSequence 経由) に限定する。 これらは
2329
+ // 1 actor = 1 lane (lane.label === a.name、 lane.id は actor 名の slug) の生成規則が成立し、
2330
+ // parts actor 用 lane を安全に削除できる。 他 preset (flow / topology / class / pie 等) は複数
2331
+ // actor が共有 lane (id = "main" 等) を参照するため、 一致 lane を消すと通常 actor の node が
2332
+ // 削除済 lane を参照する不正 diagram になる (cc-codex MAJOR 指摘)。
2333
+ //
2334
+ // leftover lane の特定は lane.label === a.name を第一に使う。 seq-like preset は非 animate 経路
2335
+ // (cdl preset の slugify) と animate 経路 (dragon の slugify) で lane.id の slug 規則が異なり
2336
+ // (`_`/全角の扱い等)、 aliasSlug (dragon slugify) と lane.id が不一致になる actor 名がある。 lane.label
2337
+ // は両経路とも a.name 生値なので slug 差の影響を受けず確実に一致する。 id === aliasSlug は
2338
+ // label 未設定 preset への fallback (exact match のみ、 prefix は false match risk のため付けない)。
2339
+ if (doc.type === "sequence" || doc.type === "solidity") {
2340
+ target.lanes = target.lanes.filter((l) => {
2341
+ // 明示 lane mapping (a.lane) 先は part の張替え先なので保持する。
2342
+ if (a.lane !== undefined && l.id === a.lane) return true;
2343
+ if (l.label === a.name) return false;
2344
+ if (l.id === aliasSlug) return false;
2345
+ return true;
2269
2346
  });
2270
- // lane も削除 = sequence preset は parts actor 用に lane (id = aliasSlug、 label = actor 名) を
2271
- // 生成する。 node/edge だけ消して lane を残すと、 merge 後の part 側 lane (label = alias) と 2 本が
2272
- // 同じ label lane-label として描画し二重表示になる (actor ラベル二重表示 bug の root cause)
2273
- //
2274
- // 削除は seq-like preset (sequence / solidity = compileSequence 経由) に限定する。 これらは
2275
- // 1 actor = 1 lane (lane.label === a.name、 lane.id は actor 名の slug) の生成規則が成立し、
2276
- // parts actor 用 lane を安全に削除できる。 他 preset (flow / topology / class / pie 等) は複数
2277
- // actor が共有 lane (id = "main" 等) を参照するため、 一致 lane を消すと通常 actor の node が
2278
- // 削除済 lane を参照する不正 diagram になる (cc-codex MAJOR 指摘)。
2279
- //
2280
- // leftover lane の特定は lane.label === a.name を第一に使う。 seq-like preset は非 animate 経路
2281
- // (cdl preset の slugify) と animate 経路 (dragon の slugify) で lane.id の slug 規則が異なり
2282
- // (`_`/全角の扱い等)、 aliasSlug (dragon slugify) と lane.id が不一致になる actor 名がある。 lane.label
2283
- // は両経路とも a.name 生値なので slug 差の影響を受けず確実に一致する。 id === aliasSlug は
2284
- // label 未設定 preset への fallback (exact match のみ、 prefix は false match risk のため付けない)。
2285
- if (doc.type === "sequence" || doc.type === "solidity") {
2286
- target.lanes = target.lanes.filter((l) => {
2287
- // 明示 lane mapping (a.lane) 先は part の張替え先なので保持する。
2288
- if (a.lane !== undefined && l.id === a.lane) return true;
2289
- if (l.label === a.name) return false;
2290
- if (l.id === aliasSlug) return false;
2291
- return true;
2292
- });
2293
- }
2294
- // 削除された node / edge を activate 参照している既存 phase の cleanup (node 削除と同じ判定経路
2295
- // = 取りこぼすと存在しない id が activate に残り dangling 参照になる、 #873)
2296
- for (const phase of target.phases) {
2297
- phase.activate = phase.activate.filter((id) => !relatedToActor(id) && !removedEdgeIds.has(id));
2298
- }
2347
+ }
2348
+ // 削除された node / edge activate 参照している既存 phase cleanup (node 削除と同じ判定経路
2349
+ // = 取りこぼすと存在しない id activate に残り dangling 参照になる、 #873)
2350
+ for (const phase of target.phases) {
2351
+ phase.activate = phase.activate.filter((id) => !relatedToActor(id) && !removedEdgeIds.has(id));
2352
+ }
2299
2353
  }
2300
2354
 
2301
2355
  /**
@@ -2362,7 +2416,9 @@ function mergePartsFromActors(
2362
2416
  const part = found;
2363
2417
  if (!part) {
2364
2418
  if (typeof console !== "undefined" && console.warn) {
2365
- console.warn(`[dragon] parts kind "${partId}" not found in partsCatalog (actor: ${actor.name})`);
2419
+ console.warn(
2420
+ `[dragon] parts kind "${partId}" not found in partsCatalog (actor: ${actor.name})`,
2421
+ );
2366
2422
  }
2367
2423
  continue;
2368
2424
  }
@@ -2450,7 +2506,8 @@ function resolveStateOverride(
2450
2506
  override: number | string | boolean | undefined,
2451
2507
  ): { initial: number | string; rejected: boolean } {
2452
2508
  if (override === undefined) return { initial: original, rejected: false };
2453
- if (isColorValue(original) && !isColorValue(override)) return { initial: original, rejected: true };
2509
+ if (isColorValue(original) && !isColorValue(override))
2510
+ return { initial: original, rejected: true };
2454
2511
  return { initial: override as number | string, rejected: false };
2455
2512
  }
2456
2513
 
@@ -2615,9 +2672,8 @@ function mergePartIntoDiagram(
2615
2672
  // directive で廃止 (2026-07-21)。 重なりは user の意図位置を優先し、 手動移動で回避する経路。
2616
2673
  // 未指定 (座標なし fallback) 時のみ existingMax + gap で右外配置 (通常経路は drop/click で座標を渡す)。
2617
2674
  const PARTS_LANE_GAP = 300;
2618
- const existingLaneMaxX = target.lanes.length > 0
2619
- ? Math.max(...target.lanes.map((l) => (l.x ?? 0) + l.width))
2620
- : 0;
2675
+ const existingLaneMaxX =
2676
+ target.lanes.length > 0 ? Math.max(...target.lanes.map((l) => (l.x ?? 0) + l.width)) : 0;
2621
2677
  // parts 全体 resize (I2 forensic): user が SE handle drag で targetW/H 指定 = actor.posW/H。
2622
2678
  // scale 基準は part 全体の bbox 幅 (全 lane の最左端〜最右端) にする。 lane[0] 幅だけを基準にすると
2623
2679
  // multi-lane part (複数 lane を横に並べた part) で全体幅を過小評価し、 非先頭 lane の node が自 lane
@@ -2649,9 +2705,10 @@ function mergePartIntoDiagram(
2649
2705
  // 変換し、 lane.x = mapLaneX(元 lane 左端) にすることで全 lane / 全 node が一貫して drop 座標を中心に
2650
2706
  // scale 配置される (cc-codex #879 の mapPartX と同じ発想を lane push まで前倒し、 #880 root fix)。
2651
2707
  const partOrigBboxCenterX = partMinLaneX + partsBboxW / 2;
2652
- const dropCenterX = offsetX !== undefined
2653
- ? offsetX
2654
- : existingLaneMaxX + PARTS_LANE_GAP + (partsBboxW * laneScaleX) / 2;
2708
+ const dropCenterX =
2709
+ offsetX !== undefined
2710
+ ? offsetX
2711
+ : existingLaneMaxX + PARTS_LANE_GAP + (partsBboxW * laneScaleX) / 2;
2655
2712
  const mapLaneX = (x: number): number => (x - partOrigBboxCenterX) * laneScaleX + dropCenterX;
2656
2713
 
2657
2714
  for (const laneOrig of part.lanes) {
@@ -2688,9 +2745,10 @@ function mergePartIntoDiagram(
2688
2745
  const shouldForcePos = offsetX !== undefined || offsetY !== undefined;
2689
2746
  // target 側の現在 max stack + isolation offset で parts node の stack を shift、
2690
2747
  // sequence の rowH 計算と完全分離 (D2 fix、 posX/posY 明示との 2 段防御)。
2691
- const targetMaxStack = shouldForcePos && target.nodes.length > 0
2692
- ? Math.max(...target.nodes.map((n) => n.stack ?? 0))
2693
- : 0;
2748
+ const targetMaxStack =
2749
+ shouldForcePos && target.nodes.length > 0
2750
+ ? Math.max(...target.nodes.map((n) => n.stack ?? 0))
2751
+ : 0;
2694
2752
  const stackShiftBase = shouldForcePos ? targetMaxStack + STACK_ISOLATION_OFFSET : 0;
2695
2753
  // parts 全体 resize scale (I2 forensic 対応): targetW / targetH 指定時、 parts の元 total size
2696
2754
  // に対する比率 = scale 係数、 全 sub-node の w / h + cx / cy 相対位置に scale 反映。
@@ -2758,8 +2816,10 @@ function mergePartIntoDiagram(
2758
2816
  // (cc-codex #879 Round 2/3 MAJOR + #880)。 mapLaneX は part bbox 中心 → drop 座標の scale 変換で、
2759
2817
  // lane / node / 明示 posX / auto-layout の全経路がこの 1 式を共有するため、 lane.x != 0 でも
2760
2818
  // multi-lane でも node 中心と自 lane 中心が一致する。
2761
- let nodePosX: number | undefined = nodeOrig.posX !== undefined ? mapLaneX(nodeOrig.posX) : undefined;
2762
- let nodePosY: number | undefined = nodeOrig.posY !== undefined ? nodeOrig.posY + (offsetY ?? 0) : undefined;
2819
+ let nodePosX: number | undefined =
2820
+ nodeOrig.posX !== undefined ? mapLaneX(nodeOrig.posX) : undefined;
2821
+ let nodePosY: number | undefined =
2822
+ nodeOrig.posY !== undefined ? nodeOrig.posY + (offsetY ?? 0) : undefined;
2763
2823
  if (shouldForcePos && nodePosX === undefined) {
2764
2824
  // posX を持たない node は所属 lane の中央 (auto layout の cx 相当) を同じ mapLaneX で変換する。
2765
2825
  const geom = partLaneGeom.get(nodeOrig.lane) ?? { x: 0, w: 320 };
@@ -2775,12 +2835,10 @@ function mergePartIntoDiagram(
2775
2835
  // 座標が非有限になって図が描けない (実測 = 箱の中心が NaN になった)
2776
2836
  const rawNodeW = nodeOrig.w !== undefined ? positiveOr(nodeOrig.w, 200) : undefined;
2777
2837
  const rawNodeH = nodeOrig.h !== undefined ? positiveOr(nodeOrig.h, 200) : undefined;
2778
- const nodeW = rawNodeW !== undefined && (scaleX !== 1 || scaleY !== 1)
2779
- ? rawNodeW * scaleX
2780
- : rawNodeW;
2781
- const nodeH = rawNodeH !== undefined && (scaleX !== 1 || scaleY !== 1)
2782
- ? rawNodeH * scaleY
2783
- : rawNodeH;
2838
+ const nodeW =
2839
+ rawNodeW !== undefined && (scaleX !== 1 || scaleY !== 1) ? rawNodeW * scaleX : rawNodeW;
2840
+ const nodeH =
2841
+ rawNodeH !== undefined && (scaleX !== 1 || scaleY !== 1) ? rawNodeH * scaleY : rawNodeH;
2784
2842
  target.nodes.push({
2785
2843
  ...nodeOrig,
2786
2844
  id: prefix(nodeOrig.id),
@@ -2800,7 +2858,10 @@ function mergePartIntoDiagram(
2800
2858
 
2801
2859
  // state merge = id prefix + initial override
2802
2860
  for (const stateOrig of part.states) {
2803
- const { initial, rejected } = resolveStateOverride(stateOrig.initial, stateOverride[stateOrig.id]);
2861
+ const { initial, rejected } = resolveStateOverride(
2862
+ stateOrig.initial,
2863
+ stateOverride[stateOrig.id],
2864
+ );
2804
2865
  if (rejected) {
2805
2866
  onNotice?.({
2806
2867
  kind: "state-override-rejected",
@@ -2844,7 +2905,10 @@ function mergePartIntoDiagram(
2844
2905
  if (part.readouts && part.readouts.length > 0) {
2845
2906
  if (!target.readouts) target.readouts = [];
2846
2907
  for (const readoutOrig of part.readouts) {
2847
- const rewritten = deepRewriteStrings(readoutOrig as unknown, rewriteTemplate) as CdlDiagram["readouts"] extends readonly (infer R)[] ? R : never;
2908
+ const rewritten = deepRewriteStrings(
2909
+ readoutOrig as unknown,
2910
+ rewriteTemplate,
2911
+ ) as CdlDiagram["readouts"] extends readonly (infer R)[] ? R : never;
2848
2912
  // id は shape 全 walk で rewrite されないので個別に prefix
2849
2913
  target.readouts.push({
2850
2914
  ...(rewritten as { id: string }),
@@ -2882,8 +2946,14 @@ function mergePartIntoDiagram(
2882
2946
  const partPhase = part.phases[i]!;
2883
2947
  targetPhase.duration = Math.max(targetPhase.duration, partPhase.duration);
2884
2948
  targetPhase.activate = [...targetPhase.activate, ...partPhase.activate.map(prefix)];
2885
- targetPhase.tweens = [...targetPhase.tweens, ...partPhase.tweens.map((t) => ({ ...t, stateId: valuePrefix(t.stateId) }))];
2886
- targetPhase.sets = [...targetPhase.sets, ...partPhase.sets.map((s) => ({ ...s, stateId: valuePrefix(s.stateId) }))];
2949
+ targetPhase.tweens = [
2950
+ ...targetPhase.tweens,
2951
+ ...partPhase.tweens.map((t) => ({ ...t, stateId: valuePrefix(t.stateId) })),
2952
+ ];
2953
+ targetPhase.sets = [
2954
+ ...targetPhase.sets,
2955
+ ...partPhase.sets.map((s) => ({ ...s, stateId: valuePrefix(s.stateId) })),
2956
+ ];
2887
2957
  }
2888
2958
  // parts phase 余剰は append (target より parts が長い場合)
2889
2959
  for (let i = commonLen; i < partsLen; i++) {
@@ -2993,11 +3063,11 @@ function 矢印へ書き写す(target: CdlEdge, s: DslStep, doc: DslDocument): v
2993
3063
  target.cardinality = s.cardinality;
2994
3064
  // ER preset の場合 label に "(1:N)" 形式で併記 (既に含まれていればスキップ)
2995
3065
  if (doc.type === "er" && !target.label.includes(s.cardinality)) {
2996
- target.label = target.label
2997
- ? `${target.label} (${s.cardinality})`
2998
- : `(${s.cardinality})`;
3066
+ target.label = target.label ? `${target.label} (${s.cardinality})` : `(${s.cardinality})`;
2999
3067
  }
3000
3068
  }
3069
+ // 矢印がどの辺から出るか (#1385)。 書かなければ描画側が自動で選ぶ
3070
+ if (s.side !== undefined) target.side = s.side;
3001
3071
  if (s.labelOffsetX !== undefined) target.labelOffsetX = s.labelOffsetX;
3002
3072
  if (s.labelOffsetY !== undefined) target.labelOffsetY = s.labelOffsetY;
3003
3073
  if (s.overlay !== undefined) target.overlay = s.overlay;
@@ -3090,7 +3160,8 @@ function 式に書く数(n: number): string {
3090
3160
  const decimalAt = whole.length + Number(exponentText);
3091
3161
  let expanded: string;
3092
3162
  if (decimalAt <= 0) expanded = `0.${"0".repeat(-decimalAt)}${digits}`;
3093
- else if (decimalAt >= digits.length) expanded = `${digits}${"0".repeat(decimalAt - digits.length)}`;
3163
+ else if (decimalAt >= digits.length)
3164
+ expanded = `${digits}${"0".repeat(decimalAt - digits.length)}`;
3094
3165
  else expanded = `${digits.slice(0, decimalAt)}.${digits.slice(decimalAt)}`;
3095
3166
  return negative ? `-${expanded}` : expanded;
3096
3167
  }
@@ -3109,13 +3180,20 @@ type 動く区間 = { 段: number; start: number; end: number; dur: number; from
3109
3180
  function 境目を通る時刻(区間: 動く区間, op: string, 境目: number): number | null {
3110
3181
  const 満たす = (x: number): boolean => {
3111
3182
  switch (op) {
3112
- case ">=": return x >= 境目;
3113
- case ">": return x > 境目;
3114
- case "<=": return x <= 境目;
3115
- case "<": return x < 境目;
3116
- case "==": return x === 境目;
3117
- case "!=": return x !== 境目;
3118
- default: return false;
3183
+ case ">=":
3184
+ return x >= 境目;
3185
+ case ">":
3186
+ return x > 境目;
3187
+ case "<=":
3188
+ return x <= 境目;
3189
+ case "<":
3190
+ return x < 境目;
3191
+ case "==":
3192
+ return x === 境目;
3193
+ case "!=":
3194
+ return x !== 境目;
3195
+ default:
3196
+ return false;
3119
3197
  }
3120
3198
  };
3121
3199
  if (満たす(区間.from)) return 区間.start;
@@ -3196,7 +3274,13 @@ function foldValueTriggers(
3196
3274
  const 解決中 = new Set<string>();
3197
3275
 
3198
3276
  const 知らせる = (v: DslValue, message: string, hint: string): void => {
3199
- onNotice?.({ kind: "value-trigger-unresolved", actor: v.name, line: v.pos?.line ?? 0, message, hint });
3277
+ onNotice?.({
3278
+ kind: "value-trigger-unresolved",
3279
+ actor: v.name,
3280
+ line: v.pos?.line ?? 0,
3281
+ message,
3282
+ hint,
3283
+ });
3200
3284
  };
3201
3285
 
3202
3286
  const 解く = (name: string): 動く区間 | null => {
@@ -3207,7 +3291,11 @@ function foldValueTriggers(
3207
3291
  if (!v || !v.trigger) return null;
3208
3292
  if (解決中.has(name)) {
3209
3293
  解けない.add(name);
3210
- 知らせる(v, `"${name}" のきっかけが一周しています`, "どれか 1 つを `trigger: step ...` に変える");
3294
+ 知らせる(
3295
+ v,
3296
+ `"${name}" のきっかけが一周しています`,
3297
+ "どれか 1 つを `trigger: step ...` に変える",
3298
+ );
3211
3299
  return null;
3212
3300
  }
3213
3301
  解決中.add(name);
@@ -3231,7 +3319,11 @@ function foldValueTriggers(
3231
3319
  if (trigger.kind === "step") {
3232
3320
  const idx = 段の番号.get(trigger.step);
3233
3321
  if (idx === undefined) {
3234
- 知らせる(v, `"${trigger.step}" という段がありません`, "`animation:` にその名前の段を書くか、 段の名前に合わせる");
3322
+ 知らせる(
3323
+ v,
3324
+ `"${trigger.step}" という段がありません`,
3325
+ "`animation:` にその名前の段を書くか、 段の名前に合わせる",
3326
+ );
3235
3327
  return null;
3236
3328
  }
3237
3329
  段 = idx;
@@ -3287,7 +3379,8 @@ function foldValueTriggers(
3287
3379
  段ごとの時計.set(段, 名前);
3288
3380
  diagram.states.push({ id: 名前, initial: 0 });
3289
3381
  const 段の中身 = diagram.phases[段];
3290
- if (段の中身) 段の中身.tweens = [...段の中身.tweens, { stateId: 名前, from: 0, to: 段の中身.duration }];
3382
+ if (段の中身)
3383
+ 段の中身.tweens = [...段の中身.tweens, { stateId: 名前, from: 0, to: 段の中身.duration }];
3291
3384
  return 名前;
3292
3385
  };
3293
3386
 
@@ -3555,9 +3648,15 @@ const VALUE_NOTICE_HINT: Readonly<Record<string, string>> = {
3555
3648
  * **今は記法からも到達する** (一覧へ戻す作業が要らなかったのはこのため)。
3556
3649
  */
3557
3650
  const SINGLE_BOX_KINDS: ReadonlySet<string> = new Set([
3558
- "chart-pie", "chart-line", "chart-bar",
3559
- "gantt-timeline", "mind-map",
3560
- "funnel-stages", "quadrant-matrix", "tree-hierarchy", "journey-map",
3651
+ "chart-pie",
3652
+ "chart-line",
3653
+ "chart-bar",
3654
+ "gantt-timeline",
3655
+ "mind-map",
3656
+ "funnel-stages",
3657
+ "quadrant-matrix",
3658
+ "tree-hierarchy",
3659
+ "journey-map",
3561
3660
  ]);
3562
3661
 
3563
3662
  /** 記法の種別を描画の種別に直す。 描けない種別のままなら `undefined`。 */
@@ -3666,14 +3765,36 @@ const LABEL_MIN_H: Readonly<Record<string, number>> = {
3666
3765
  */
3667
3766
  const LABEL_NEVER_FITS: ReadonlySet<string> = new Set([
3668
3767
  // 左右にはみ出す 24 種。 横幅は高さで変わらないため直らない
3669
- "shape-api-gateway", "shape-atm", "shape-auditor", "shape-bank", "shape-bitcoin-chain",
3670
- "shape-blockchain", "shape-blockchain-block", "shape-blockchain-node", "shape-brokerage",
3671
- "shape-code-block", "shape-customer-service", "shape-ethereum-chain", "shape-hexagon",
3672
- "shape-kanban-card", "shape-lawyer", "shape-network-node", "shape-nft", "shape-notary",
3673
- "shape-regulator", "shape-satellite", "shape-smart-contract", "shape-terminal",
3674
- "shape-trader", "shape-trust-bank",
3768
+ "shape-api-gateway",
3769
+ "shape-atm",
3770
+ "shape-auditor",
3771
+ "shape-bank",
3772
+ "shape-bitcoin-chain",
3773
+ "shape-blockchain",
3774
+ "shape-blockchain-block",
3775
+ "shape-blockchain-node",
3776
+ "shape-brokerage",
3777
+ "shape-code-block",
3778
+ "shape-customer-service",
3779
+ "shape-ethereum-chain",
3780
+ "shape-hexagon",
3781
+ "shape-kanban-card",
3782
+ "shape-lawyer",
3783
+ "shape-network-node",
3784
+ "shape-nft",
3785
+ "shape-notary",
3786
+ "shape-regulator",
3787
+ "shape-satellite",
3788
+ "shape-smart-contract",
3789
+ "shape-terminal",
3790
+ "shape-trader",
3791
+ "shape-trust-bank",
3675
3792
  // 下のはみ出しが高さに依らない 6 種
3676
- "shape-cylinder", "shape-diamond", "shape-file", "shape-folder", "shape-mobile-device",
3793
+ "shape-cylinder",
3794
+ "shape-diamond",
3795
+ "shape-file",
3796
+ "shape-folder",
3797
+ "shape-mobile-device",
3677
3798
  "shape-stack",
3678
3799
  // 上へ出る絵が名札の大きさでは読めない。 `#1067` では「上は何ともぶつからない」 として残したが、
3679
3800
  // 実際には絵が小さく潰れて名前と重なり、 横に並べた時も 1 本だけ頭が浮く (user 実機確認)
@@ -3979,6 +4100,7 @@ function compileGantt(doc: DslDocument): CdlDiagram {
3979
4100
  const 目盛りなし: string[] = [];
3980
4101
  const タスク: {
3981
4102
  name: string;
4103
+ title: string;
3982
4104
  label: string;
3983
4105
  tone?: DslDocument["actors"][number]["tone"];
3984
4106
  owner?: string;
@@ -3994,6 +4116,7 @@ function compileGantt(doc: DslDocument): CdlDiagram {
3994
4116
  // 色は帯にそのまま渡す。 箱が 1 つになっても、 書いた色が消えないようにする
3995
4117
  タスク.push({
3996
4118
  name: a.name,
4119
+ title: 箱の題(a),
3997
4120
  label,
3998
4121
  ...(a.tone !== undefined ? { tone: a.tone } : {}),
3999
4122
  ...(a.owner !== undefined ? { owner: a.owner } : {}),
@@ -4020,7 +4143,12 @@ function compileGantt(doc: DslDocument): CdlDiagram {
4020
4143
  }
4021
4144
  依存元.set(s.to, s.from);
4022
4145
  // 帯の依存は「どちらが先か」 だけを持つ。 矢印に書いた文字や色は描けないので伝える
4023
- if ((s.label ?? "") !== "" || (s.sub ?? "") !== "" || s.tone !== undefined || s.style !== undefined) {
4146
+ if (
4147
+ (s.label ?? "") !== "" ||
4148
+ (s.sub ?? "") !== "" ||
4149
+ s.tone !== undefined ||
4150
+ s.style !== undefined
4151
+ ) {
4024
4152
  装飾つき.push(`${s.from} -> ${s.to}`);
4025
4153
  }
4026
4154
  }
@@ -4059,7 +4187,7 @@ function compileGantt(doc: DslDocument): CdlDiagram {
4059
4187
  const 終わり = 終わる位置(t.end, idx, 目盛り, t.name, 逆向きを伝える, doc);
4060
4188
  return {
4061
4189
  id: slugify(t.name) || t.name,
4062
- title: t.name,
4190
+ title: t.title,
4063
4191
  startIdx: idx,
4064
4192
  endIdx: 終わり.idx,
4065
4193
  startLabel: t.label,
@@ -4110,7 +4238,7 @@ function compileClass(doc: DslDocument): CdlDiagram {
4110
4238
  lane: laneId,
4111
4239
  stack: 0,
4112
4240
  kind: "storage",
4113
- title: a.name,
4241
+ title: 箱の題(a),
4114
4242
  w: CLASS_W,
4115
4243
  });
4116
4244
  });
@@ -4121,6 +4249,7 @@ function compileClass(doc: DslDocument): CdlDiagram {
4121
4249
  b.edge(fromId, toId, {
4122
4250
  label: s.label,
4123
4251
  ...(s.sub ? { sub: s.sub } : {}),
4252
+ ...(s.side ? { side: s.side } : {}),
4124
4253
  ...(s.tone ? { tone: s.tone } : {}),
4125
4254
  ...(s.style ? { style: s.style } : {}),
4126
4255
  });
@@ -4185,7 +4314,6 @@ function parseChartValue(raw: string | undefined): number | string | null {
4185
4314
  return parseBoundValue(raw);
4186
4315
  }
4187
4316
 
4188
-
4189
4317
  /** 状態を読む欄が指している名前 (`{v.sum}` なら `v`)。 欄でなければ null */
4190
4318
  function 参照する名前(value: number | string | null): string | null {
4191
4319
  if (typeof value !== "string") return null;
@@ -4251,7 +4379,7 @@ function compileValueChart(
4251
4379
  continue;
4252
4380
  }
4253
4381
  // 色はそのまま渡す。 箱が 1 つになっても、 書いた色が消えないようにする
4254
- data.push({ label: a.name, value, ...(a.tone !== undefined ? { tone: a.tone } : {}) });
4382
+ data.push({ label: 箱の題(a), value, ...(a.tone !== undefined ? { tone: a.tone } : {}) });
4255
4383
  }
4256
4384
  // 案内の言葉は型ごとに変える。 共通化した時に `pie` の「割合 / 円 / 45%」 が「値 / 図 / 45」 に
4257
4385
  // 薄まり、 既存の案内が後退した (review 指摘)。 何を書けばよいかは型ごとに違う
@@ -4313,7 +4441,6 @@ function compileValueChart(
4313
4441
  return b.build();
4314
4442
  }
4315
4443
 
4316
-
4317
4444
  /**
4318
4445
  * 図表 4 種の組立て (#1154 段 2 / 段 3)。
4319
4446
  *
@@ -4415,7 +4542,11 @@ const 区画 = new Map<string, "topLeft" | "topRight" | "bottomLeft" | "bottomRi
4415
4542
  */
4416
4543
  function 図表の欄から参照できる名前(
4417
4544
  doc: DslDocument,
4418
- 欄: { 読めるか: (v: number | string) => boolean; 補間で壊れるか: boolean; 自動の値を許すか: boolean },
4545
+ 欄: {
4546
+ 読めるか: (v: number | string) => boolean;
4547
+ 補間で壊れるか: boolean;
4548
+ 自動の値を許すか: boolean;
4549
+ },
4419
4550
  ): Set<string> {
4420
4551
  // 同じ名前を 2 回宣言した時は後ろが効く。 描画側が後の宣言を有効値として扱うため、
4421
4552
  // 前の宣言で判定すると「読めると判定したのに読めない値が入る」 状態になる (実測)
@@ -4536,7 +4667,7 @@ function compileFunnel(doc: DslDocument, onNotice?: (n: CompileNotice) => void):
4536
4667
  未宣言.push(a.name);
4537
4668
  continue;
4538
4669
  }
4539
- data.push({ id: slugify(a.name), title: a.name, count: v });
4670
+ data.push({ id: slugify(a.name), title: 箱の題(a), count: v });
4540
4671
  }
4541
4672
  if (未宣言.length > 0) {
4542
4673
  const m3 = `type: funnel で数にならない値を参照した項目があります (段に載せません): ${未宣言.join(", ")}。 \`states:\` にその名前を数で書いてください`;
@@ -4551,11 +4682,23 @@ function compileFunnel(doc: DslDocument, onNotice?: (n: CompileNotice) => void):
4551
4682
  // 矢印は描けない。 書かれていたら伝える (黙って捨てると「書いたのに効かない」 が残る)
4552
4683
  if (doc.flow.length > 0) {
4553
4684
  const m2 = `type: funnel では矢印を描けません (${doc.flow.length} 本を無視しました)。 関係を描くなら type: flow を使ってください`;
4554
- onNotice?.({ kind: "chart-edge-dropped", actor: doc.flow[0]?.from ?? "", line: doc.flow[0]?.pos?.line ?? 0, message: m2 });
4685
+ onNotice?.({
4686
+ kind: "chart-edge-dropped",
4687
+ actor: doc.flow[0]?.from ?? "",
4688
+ line: doc.flow[0]?.pos?.line ?? 0,
4689
+ message: m2,
4690
+ });
4555
4691
  if (typeof console !== "undefined" && console.warn) console.warn(`[dragon] ${m2}`);
4556
4692
  }
4557
4693
  b.node(`${slugify(doc.title) || "funnel"}-chart`, {
4558
- lane: "chart", stack: 0, kind: "funnel-stages", title: doc.title, ...図の小見出し(doc), w: W, h: H, funnelData: data,
4694
+ lane: "chart",
4695
+ stack: 0,
4696
+ kind: "funnel-stages",
4697
+ title: doc.title,
4698
+ ...図の小見出し(doc),
4699
+ w: W,
4700
+ h: H,
4701
+ funnelData: data,
4559
4702
  });
4560
4703
  return b.build();
4561
4704
  }
@@ -4582,12 +4725,20 @@ function 矢印から親を決める(
4582
4725
  const 子 = slugify(f.to);
4583
4726
  const 親名 = slugify(f.from);
4584
4727
  if (!名前.has(親名)) {
4585
- 伝える(f.from, `type: ${図種} で書いていない名前を親にしています: ${f.from} -> ${f.to}`, f.pos?.line ?? 0);
4728
+ 伝える(
4729
+ f.from,
4730
+ `type: ${図種} で書いていない名前を親にしています: ${f.from} -> ${f.to}`,
4731
+ f.pos?.line ?? 0,
4732
+ );
4586
4733
  continue;
4587
4734
  }
4588
4735
  // 子の側も見る。 書いていない名前への矢印は、 黙って捨てると図から関係が消える
4589
4736
  if (!名前.has(子)) {
4590
- 伝える(f.to, `type: ${図種} で書いていない名前を子にしています: ${f.from} -> ${f.to}`, f.pos?.line ?? 0);
4737
+ 伝える(
4738
+ f.to,
4739
+ `type: ${図種} で書いていない名前を子にしています: ${f.from} -> ${f.to}`,
4740
+ f.pos?.line ?? 0,
4741
+ );
4591
4742
  continue;
4592
4743
  }
4593
4744
  if (子 === 親名) {
@@ -4596,7 +4747,11 @@ function 矢印から親を決める(
4596
4747
  }
4597
4748
  const 既存 = 親.get(子);
4598
4749
  if (既存 !== undefined && 既存 !== 親名) {
4599
- 伝える(f.to, `type: ${図種} で ${f.to} に親が 2 つあります (後の ${f.from} は使いません)`, f.pos?.line ?? 0);
4750
+ 伝える(
4751
+ f.to,
4752
+ `type: ${図種} で ${f.to} に親が 2 つあります (後の ${f.from} は使いません)`,
4753
+ f.pos?.line ?? 0,
4754
+ );
4600
4755
  continue;
4601
4756
  }
4602
4757
  親.set(子, 親名);
@@ -4638,7 +4793,10 @@ function compileTree(doc: DslDocument, onNotice?: (n: CompileNotice) => void): C
4638
4793
  };
4639
4794
  for (const [k, 群] of slug別) {
4640
4795
  if (群.length > 1) {
4641
- 伝える(群[0]!, `type: tree で ${群.join(" / ")} が同じ id (${k}) になります。 名前を変えてください`);
4796
+ 伝える(
4797
+ 群[0]!,
4798
+ `type: tree で ${群.join(" / ")} が同じ id (${k}) になります。 名前を変えてください`,
4799
+ );
4642
4800
  }
4643
4801
  }
4644
4802
  const 親 = 矢印から親を決める(doc, "tree", 名前, 伝える);
@@ -4650,7 +4808,14 @@ function compileTree(doc: DslDocument, onNotice?: (n: CompileNotice) => void): C
4650
4808
  return { id, ...放射に出す文字(a), ...(p3 !== undefined ? { parent: p3 } : {}) };
4651
4809
  });
4652
4810
  b.node(`${slugify(doc.title) || "tree"}-chart`, {
4653
- lane: "chart", stack: 0, kind: "tree-hierarchy", title: doc.title, ...図の小見出し(doc), w: W, h: H, treeData: data,
4811
+ lane: "chart",
4812
+ stack: 0,
4813
+ kind: "tree-hierarchy",
4814
+ title: doc.title,
4815
+ ...図の小見出し(doc),
4816
+ w: W,
4817
+ h: H,
4818
+ treeData: data,
4654
4819
  });
4655
4820
  return b.build();
4656
4821
  }
@@ -4677,19 +4842,25 @@ function 道筋の欄(a: DslActor): { touchpoint?: string; opportunity?: string
4677
4842
  * `type: mind` では伝えない = `compileMind` が描けない欄をまとめて 1 件で伝えており、
4678
4843
  * そこに 2 つとも入っている (`放射で描けない欄`)。 二重に伝えない (#1246 と同じ扱い)。
4679
4844
  */
4680
- function reportChartFieldsNotHonored(doc: DslDocument, onNotice?: (n: CompileNotice) => void): void {
4845
+ function reportChartFieldsNotHonored(
4846
+ doc: DslDocument,
4847
+ onNotice?: (n: CompileNotice) => void,
4848
+ ): void {
4681
4849
  if (!onNotice) return;
4682
4850
  if (doc.type === "mind") return;
4683
4851
  for (const a of doc.actors) {
4684
4852
  if (a.partId !== undefined) continue;
4685
- const 道筋 = doc.type === "journey" ? [] : [
4686
- ...(a.touchpoint !== undefined ? ["touchpoint"] : []),
4687
- ...(a.opportunity !== undefined ? ["opportunity"] : []),
4688
- ];
4689
- const 工程 = doc.type === "gantt" ? [] : [
4690
- ...(a.owner !== undefined ? ["owner"] : []),
4691
- ...(a.end !== undefined ? ["end"] : []),
4692
- ];
4853
+ const 道筋 =
4854
+ doc.type === "journey"
4855
+ ? []
4856
+ : [
4857
+ ...(a.touchpoint !== undefined ? ["touchpoint"] : []),
4858
+ ...(a.opportunity !== undefined ? ["opportunity"] : []),
4859
+ ];
4860
+ const 工程 =
4861
+ doc.type === "gantt"
4862
+ ? []
4863
+ : [...(a.owner !== undefined ? ["owner"] : []), ...(a.end !== undefined ? ["end"] : [])];
4693
4864
  if (道筋.length > 0) {
4694
4865
  onNotice({
4695
4866
  kind: "chart-value-unreadable",
@@ -4727,7 +4898,7 @@ function compileJourney(doc: DslDocument, onNotice?: (n: CompileNotice) => void)
4727
4898
  読めない.push(a.name);
4728
4899
  continue;
4729
4900
  }
4730
- data.push({ id: slugify(a.name), title: a.name, emotion: 語 as never, ...道筋の欄(a) });
4901
+ data.push({ id: slugify(a.name), title: 箱の題(a), emotion: 語 as never, ...道筋の欄(a) });
4731
4902
  continue;
4732
4903
  }
4733
4904
  const e = 気持ち.get(語);
@@ -4735,7 +4906,7 @@ function compileJourney(doc: DslDocument, onNotice?: (n: CompileNotice) => void)
4735
4906
  読めない.push(a.name);
4736
4907
  continue;
4737
4908
  }
4738
- data.push({ id: slugify(a.name), title: a.name, emotion: e, ...道筋の欄(a) });
4909
+ data.push({ id: slugify(a.name), title: 箱の題(a), emotion: e, ...道筋の欄(a) });
4739
4910
  }
4740
4911
  if (読めない.length > 0) {
4741
4912
  const m = `type: journey で気持ちを読めない項目があります (道筋に載せません): ${読めない.join(", ")}。 \`- 登録: "不満"\` の形で、 ${[...気持ち.keys()].join(" / ")} のどれかを書いてください`;
@@ -4745,11 +4916,23 @@ function compileJourney(doc: DslDocument, onNotice?: (n: CompileNotice) => void)
4745
4916
  // 矢印は描けない。 書かれていたら伝える (黙って捨てると「書いたのに効かない」 が残る)
4746
4917
  if (doc.flow.length > 0) {
4747
4918
  const m2 = `type: journey では矢印を描けません (${doc.flow.length} 本を無視しました)。 関係を描くなら type: flow を使ってください`;
4748
- onNotice?.({ kind: "chart-edge-dropped", actor: doc.flow[0]?.from ?? "", line: doc.flow[0]?.pos?.line ?? 0, message: m2 });
4919
+ onNotice?.({
4920
+ kind: "chart-edge-dropped",
4921
+ actor: doc.flow[0]?.from ?? "",
4922
+ line: doc.flow[0]?.pos?.line ?? 0,
4923
+ message: m2,
4924
+ });
4749
4925
  if (typeof console !== "undefined" && console.warn) console.warn(`[dragon] ${m2}`);
4750
4926
  }
4751
4927
  b.node(`${slugify(doc.title) || "journey"}-chart`, {
4752
- lane: "chart", stack: 0, kind: "journey-map", title: doc.title, ...図の小見出し(doc), w: W, h: H, journeyData: data,
4928
+ lane: "chart",
4929
+ stack: 0,
4930
+ kind: "journey-map",
4931
+ title: doc.title,
4932
+ ...図の小見出し(doc),
4933
+ w: W,
4934
+ h: H,
4935
+ journeyData: data,
4753
4936
  });
4754
4937
  return b.build();
4755
4938
  }
@@ -4828,7 +5011,7 @@ function compileQuadrant(doc: DslDocument, onNotice?: (n: CompileNotice) => void
4828
5011
  読めない.push(a.name);
4829
5012
  continue;
4830
5013
  }
4831
- items.push({ id: slugify(a.name), title: a.name, quadrant: 語 as never });
5014
+ items.push({ id: slugify(a.name), title: 箱の題(a), quadrant: 語 as never });
4832
5015
  continue;
4833
5016
  }
4834
5017
  const q = 区画.get(語);
@@ -4836,7 +5019,7 @@ function compileQuadrant(doc: DslDocument, onNotice?: (n: CompileNotice) => void
4836
5019
  読めない.push(a.name);
4837
5020
  continue;
4838
5021
  }
4839
- items.push({ id: slugify(a.name), title: a.name, quadrant: q });
5022
+ items.push({ id: slugify(a.name), title: 箱の題(a), quadrant: q });
4840
5023
  }
4841
5024
  if (読めない.length > 0) {
4842
5025
  const m = `type: quadrant で区画を読めない項目があります (図に載せません): ${読めない.join(", ")}。 \`- 重複削除: "左上"\` の形で、 ${[...区画.keys()].join(" / ")} のどれかを書いてください`;
@@ -4846,11 +5029,22 @@ function compileQuadrant(doc: DslDocument, onNotice?: (n: CompileNotice) => void
4846
5029
  // 矢印は描けない。 書かれていたら伝える (黙って捨てると「書いたのに効かない」 が残る)
4847
5030
  if (doc.flow.length > 0) {
4848
5031
  const m2 = `type: quadrant では矢印を描けません (${doc.flow.length} 本を無視しました)。 関係を描くなら type: flow を使ってください`;
4849
- onNotice?.({ kind: "chart-edge-dropped", actor: doc.flow[0]?.from ?? "", line: doc.flow[0]?.pos?.line ?? 0, message: m2 });
5032
+ onNotice?.({
5033
+ kind: "chart-edge-dropped",
5034
+ actor: doc.flow[0]?.from ?? "",
5035
+ line: doc.flow[0]?.pos?.line ?? 0,
5036
+ message: m2,
5037
+ });
4850
5038
  if (typeof console !== "undefined" && console.warn) console.warn(`[dragon] ${m2}`);
4851
5039
  }
4852
5040
  b.node(`${slugify(doc.title) || "quadrant"}-chart`, {
4853
- lane: "chart", stack: 0, kind: "quadrant-matrix", title: doc.title, ...図の小見出し(doc), w: W, h: H,
5041
+ lane: "chart",
5042
+ stack: 0,
5043
+ kind: "quadrant-matrix",
5044
+ title: doc.title,
5045
+ ...図の小見出し(doc),
5046
+ w: W,
5047
+ h: H,
4854
5048
  quadrantData: { ...軸と区画の名前(doc), items },
4855
5049
  });
4856
5050
  return b.build();
@@ -4879,7 +5073,10 @@ function 段を読み取る(subtitle: string | undefined): { 段: number; 説明
4879
5073
  const m = 元.match(/^L([123])(?![0-9A-Za-z])/i);
4880
5074
  if (m === null) return { 段: 1, 説明: subtitle };
4881
5075
  // 目印と、 その直後の区切り (`:` / 全角コロン / 空白) を落とす
4882
- const 残り = 元.slice(m[0].length).replace(/^[::\s]+/, "").trim();
5076
+ const 残り =
5077
+ .slice(m[0].length)
5078
+ .replace(/^[::\s]+/, "")
5079
+ .trim();
4883
5080
  return { 段: Number(m[1]), 説明: 残り === "" ? undefined : 残り };
4884
5081
  }
4885
5082
 
@@ -4943,6 +5140,7 @@ function compileC4(doc: DslDocument): CdlDiagram {
4943
5140
  b.edge(fromId, toId, {
4944
5141
  label: s.label,
4945
5142
  ...(s.sub ? { sub: s.sub } : {}),
5143
+ ...(s.side ? { side: s.side } : {}),
4946
5144
  ...(s.tone ? { tone: s.tone } : {}),
4947
5145
  ...(s.style ? { style: s.style } : {}),
4948
5146
  });
@@ -4998,6 +5196,8 @@ function compileC4(doc: DslDocument): CdlDiagram {
4998
5196
  type 放射で描ける欄 =
4999
5197
  /** 中心の名前 / 枝の名前になる */
5000
5198
  | "name"
5199
+ /** 名前と分けて中心 / 枝に出す題。 書かなければ名前を出す */
5200
+ | "title"
5001
5201
  /**
5002
5202
  * 名前とは分けて補足に出す 2 欄。
5003
5203
  *
@@ -5018,6 +5218,16 @@ type 放射で描ける欄 =
5018
5218
  /** 本文の行番号。 知らせに載せるために使う */
5019
5219
  | "pos";
5020
5220
 
5221
+ /**
5222
+ * 箱に出す題 (#1381)。 書いていなければ名前をそのまま使う。
5223
+ *
5224
+ * 名前は図の中で 1 つに決まる必要がある (`focus:` と `flow:` が名前で指す) 一方、題は
5225
+ * 重なってよい。 同じ題の箱を並べる図と、題を持たない箱は、名前と切り離さないと書けない。
5226
+ */
5227
+ function 箱の題(a: DslActor): string {
5228
+ return a.title ?? a.name;
5229
+ }
5230
+
5021
5231
  /** 放射では描けない欄。 書かれていたら伝える */
5022
5232
  type 放射で描けない欄 =
5023
5233
  | "kind"
@@ -5041,7 +5251,11 @@ type 放射で描けない欄 =
5041
5251
  | "scaleKeys"
5042
5252
  | "posRel"
5043
5253
  | "nodes"
5044
- | "layoutPos";
5254
+ | "layoutPos"
5255
+ // 箱の中に描く図形 (#1374)。 放射の枝は箱の中に図形を持たない
5256
+ | "shape"
5257
+ // 出す条件 (#1381)。 放射の枝は個別に出し分けられない
5258
+ | "visibleIf";
5045
5259
 
5046
5260
  /** 引数が `never` でなければ型検査が落ちる */
5047
5261
  type 空であること<T extends never> = T;
@@ -5096,6 +5310,8 @@ const 放射で描けない欄の名前: Record<放射で描けない欄, string
5096
5310
  posRel: "位置 (相対)",
5097
5311
  nodes: "中の箱ごとの指定",
5098
5312
  layoutPos: "配置のずらし",
5313
+ shape: "箱の中の図形",
5314
+ visibleIf: "出す条件",
5099
5315
  };
5100
5316
 
5101
5317
  /**
@@ -5126,7 +5342,7 @@ function 放射で描けない欄を書いたか(a: DslActor, 欄: 放射で描
5126
5342
  */
5127
5343
  function 放射に出す文字(a: DslActor): { title: string; subtitle?: string } {
5128
5344
  const 続き = [a.subtitle, a.value].map((x) => x?.trim()).filter((x): x is string => !!x);
5129
- return { title: a.name, ...(続き.length > 0 ? { subtitle: 続き.join(" ") } : {}) };
5345
+ return { title: 箱の題(a), ...(続き.length > 0 ? { subtitle: 続き.join(" ") } : {}) };
5130
5346
  }
5131
5347
 
5132
5348
  function compileMind(doc: DslDocument, onNotice?: (n: CompileNotice) => void): CdlDiagram {
@@ -5226,7 +5442,8 @@ function compileMind(doc: DslDocument, onNotice?: (n: CompileNotice) => void): C
5226
5442
  const 使った = new Set<string>([rootId]);
5227
5443
  記録する(root);
5228
5444
  // 中心は色の欄を持たない (`MindBranchPayload` に `tone` が無い)
5229
- if (root.tone) 消えた欄.set(root.name, [...(消えた欄.get(root.name) ?? []), "色 (中心は持てない)"]);
5445
+ if (root.tone)
5446
+ 消えた欄.set(root.name, [...(消えた欄.get(root.name) ?? []), "色 (中心は持てない)"]);
5230
5447
 
5231
5448
  見本でない.slice(1).forEach((a, i) => {
5232
5449
  const id = slugify(a.name) || `leaf-${i}`;
@@ -5304,10 +5521,7 @@ function compileMind(doc: DslDocument, onNotice?: (n: CompileNotice) => void): C
5304
5521
  * 後から来た見本の縦列に書いた値を移し、先に作った空の方を外す。 書いた人から見れば
5305
5522
  * 「id を書けば効く」 が成り立つ。
5306
5523
  */
5307
- function mergeDuplicateDeclaredLanes(
5308
- diagram: CdlDiagram,
5309
- 追加した縦列: readonly DslLane[],
5310
- ): void {
5524
+ function mergeDuplicateDeclaredLanes(diagram: CdlDiagram, 追加した縦列: readonly DslLane[]): void {
5311
5525
  for (const 宣言 of 追加した縦列) {
5312
5526
  const { id } = 宣言;
5313
5527
  const 同一idの縦列 = diagram.lanes.filter((l) => l.id === id);
@@ -5399,14 +5613,31 @@ function applyV05Extensions(
5399
5613
  // `s{idx}-{laneId}` が actor 名末尾 "Header" (slug `...-header`) で誤マッチし、 option が invisible
5400
5614
  // な step anchor にも copy される (cc-codex #883 MAJOR)。 lane id との構造 exact 一致で header だけを
5401
5615
  // 引くことで step box / spacer / footer を排除する。
5402
- primaryNodes = ownedLaneIds.size > 0
5403
- ? diagram.nodes.filter((n) => ownedLaneIds.has(n.lane) && n.id === `${n.lane}-header`)
5404
- : diagram.nodes.filter((n) => n.id === `${dragonSlug}-header`);
5616
+ primaryNodes =
5617
+ ownedLaneIds.size > 0
5618
+ ? diagram.nodes.filter((n) => ownedLaneIds.has(n.lane) && n.id === `${n.lane}-header`)
5619
+ : diagram.nodes.filter((n) => n.id === `${dragonSlug}-header`);
5405
5620
  } else {
5406
5621
  // 非 seq preset は 1 actor = 1 node (id = dragon slug) で node id と dragon slug が一致する。
5407
5622
  primaryNodes = diagram.nodes.filter((n) => n.id === dragonSlug);
5408
5623
  }
5409
5624
  for (const node of primaryNodes) {
5625
+ // 識別に使う名前と、箱に出す題を分ける (#1381)。 preset が actor 名で
5626
+ // node を作る経路 (sequence / solidity / swimlane / c4) もここで書き換える。
5627
+ if (a.title !== undefined) {
5628
+ node.title = a.title;
5629
+ if (isSeqLike) {
5630
+ const footer = diagram.nodes.find((n) => n.id === `${node.lane}-footer`);
5631
+ if (footer) footer.title = a.title;
5632
+
5633
+ // 名前で決めた preset の幅を題に合わせる。 明示の大きさは後段が優先する。
5634
+ if (a.posW === undefined) {
5635
+ const titleW = Math.max(140, a.title.length * 22 + 52);
5636
+ node.w = titleW;
5637
+ if (footer) footer.w = titleW;
5638
+ }
5639
+ }
5640
+ }
5410
5641
  // `type: c4` では説明の先頭に段の目印 (`L1` / `L2` / `L3`) を書く。 目印は組み立てに
5411
5642
  // 段を伝えるためのもので読む人に意味を持たず、 段の名前は枠のラベルが既に出している。
5412
5643
  // ここで落とさないと、 組み立てが読み取った目印がそのまま箱の説明として出る (#1098)
@@ -5482,6 +5713,30 @@ function applyV05Extensions(
5482
5713
  const footer = diagram.nodes.find((n) => n.id === `${node.lane}-footer`);
5483
5714
  if (footer && a.posW !== undefined) footer.w = a.posW;
5484
5715
  }
5716
+ // 箱の中に描く図形 (#1374)。 renderer が shape を描くのは dyn-* kind だけなので、
5717
+ // shape 自身を SSOT にして対応する kind へ揃える。 card 等のまま shape だけ渡すと、指定を
5718
+ // 保持しているのに画面には何も出ない。 paint 検査が入力を mutate しないよう object も写す。
5719
+ if (a.shape !== undefined) {
5720
+ const dynamicKind = `dyn-${a.shape.kind}` as typeof node.kind;
5721
+ node.kind = dynamicKind;
5722
+ node.shape = { ...a.shape };
5723
+ if (isSeqLike) {
5724
+ const footer = diagram.nodes.find((n) => n.id === `${node.lane}-footer`);
5725
+ if (footer) {
5726
+ footer.kind = dynamicKind;
5727
+ footer.shape = { ...a.shape };
5728
+ }
5729
+ }
5730
+ }
5731
+ // その箱を出すかどうかの条件 (#1381)。 名札と足にも同じ条件を渡す = 片方だけ隠すと
5732
+ // 順序図で縦線の頭と足が食い違う
5733
+ if (a.visibleIf !== undefined) {
5734
+ node.visibleIf = a.visibleIf;
5735
+ if (isSeqLike) {
5736
+ const footer = diagram.nodes.find((n) => n.id === `${node.lane}-footer`);
5737
+ if (footer) footer.visibleIf = a.visibleIf;
5738
+ }
5739
+ }
5485
5740
  }
5486
5741
  }
5487
5742
  // 名札の高さを揃える。 kind ごとに高さが変わると縦線の始まる位置がばらけ、 順序図の
@@ -5599,7 +5854,12 @@ function injectPhasesFallback(diagram: CdlDiagram, doc: DslDocument): void {
5599
5854
  }
5600
5855
  // 線を持たない種類 (帯の依存等) では矢印が edge にならない。 両端が実在するなら
5601
5856
  // その箱を光らせる = 矢印を指した段で何も光らないより意図に近い (#1077)
5602
- if (!見つかった && singleBoxNode !== undefined && knownNames.has(entry.from) && knownNames.has(entry.to)) {
5857
+ if (
5858
+ !見つかった &&
5859
+ singleBoxNode !== undefined &&
5860
+ knownNames.has(entry.from) &&
5861
+ knownNames.has(entry.to)
5862
+ ) {
5603
5863
  out.push(singleBoxNode.id);
5604
5864
  }
5605
5865
  continue;
@@ -5629,9 +5889,7 @@ function injectPhasesFallback(diagram: CdlDiagram, doc: DslDocument): void {
5629
5889
  // **`activate` と兼ねない**。 描画側は焦点と別集合で持つ (`cdl#512`) = 焦点が当たり
5630
5890
  // 続ける図で毎段引き直しになるため。 書いた段だけが欄を持つ
5631
5891
  const drawIds =
5632
- p.draw !== undefined &&
5633
- DRAW_TARGETS.get(p.draw) === doc.type &&
5634
- singleBoxNode !== undefined
5892
+ p.draw !== undefined && DRAW_TARGETS.get(p.draw) === doc.type && singleBoxNode !== undefined
5635
5893
  ? [singleBoxNode.id]
5636
5894
  : [];
5637
5895
  diagram.phases.push({
@@ -5666,6 +5924,7 @@ function compileSequence(doc: DslDocument): CdlDiagram {
5666
5924
  to: s.to,
5667
5925
  label: s.label,
5668
5926
  ...(s.sub ? { sub: s.sub } : {}),
5927
+ ...(s.side ? { side: s.side } : {}),
5669
5928
  ...(s.tone ? { tone: s.tone } : {}),
5670
5929
  ...(s.style ? { style: s.style } : {}),
5671
5930
  });
@@ -5705,8 +5964,8 @@ function compileSequenceWithAnimate(doc: DslDocument): CdlDiagram {
5705
5964
  const headerId = `${id}-header`;
5706
5965
  // header/footer 幅を title 長に応じて auto-size (text-readability warning 解消)。
5707
5966
  // formula = 22px/char + 52px padding (visualValidate text-readability と完全一致)、 min 140 で従来 sample 互換維持。
5708
- const actorW = Math.max(140, a.name.length * 22 + 52);
5709
- b.node(headerId, { lane: id, stack: 0, kind: "card", title: a.name, w: actorW, h: 72 });
5967
+ const actorW = Math.max(140, 箱の題(a).length * 22 + 52);
5968
+ b.node(headerId, { lane: id, stack: 0, kind: "card", title: 箱の題(a), w: actorW, h: 72 });
5710
5969
  headerNodeIds.push(headerId);
5711
5970
  const spacerId = `${id}-spacer`;
5712
5971
  b.node(spacerId, { lane: id, stack: 1, kind: "card", title: "", w: 2, h: 40 });
@@ -5733,6 +5992,7 @@ function compileSequenceWithAnimate(doc: DslDocument): CdlDiagram {
5733
5992
  id: edgeId,
5734
5993
  label: s.label,
5735
5994
  ...(s.sub ? { sub: s.sub } : {}),
5995
+ ...(s.side ? { side: s.side } : {}),
5736
5996
  ...(s.tone ? { tone: s.tone } : {}),
5737
5997
  ...(s.style ? { style: s.style } : {}),
5738
5998
  });
@@ -5744,7 +6004,7 @@ function compileSequenceWithAnimate(doc: DslDocument): CdlDiagram {
5744
6004
  doc.actors.forEach((a) => {
5745
6005
  const laneId = actorIds.get(a.name) ?? slugify(a.name);
5746
6006
  const footerId = `${laneId}-footer`;
5747
- const actorW = Math.max(140, a.name.length * 22 + 52);
6007
+ const actorW = Math.max(140, 箱の題(a).length * 22 + 52);
5748
6008
  // `role` を付ける (#1273)。 付けないと生命線の終わりが footer より 100 下まで伸びる
5749
6009
  // (実測 = 組立て API は `y2=848`、記法は `y2=948`)。 枠の大きさは同じなので
5750
6010
  // 描いた図の大きさの比較では捕まらない
@@ -5752,7 +6012,7 @@ function compileSequenceWithAnimate(doc: DslDocument): CdlDiagram {
5752
6012
  lane: laneId,
5753
6013
  stack: footerStack,
5754
6014
  kind: "card",
5755
- title: a.name,
6015
+ title: 箱の題(a),
5756
6016
  w: actorW,
5757
6017
  h: 72,
5758
6018
  role: "lifeline-footer",
@@ -5901,7 +6161,7 @@ function compileFlow(doc: DslDocument): CdlDiagram {
5901
6161
  {
5902
6162
  id: slugify(a.name) || `n${i}`,
5903
6163
  kind: a.kind,
5904
- title: a.name,
6164
+ title: 箱の題(a),
5905
6165
  },
5906
6166
  edgeLabel,
5907
6167
  );
@@ -5951,6 +6211,7 @@ function compileSwimlane(doc: DslDocument): CdlDiagram {
5951
6211
  id: `e${edgeIdx++}-${fromId}-${toId}`,
5952
6212
  label: s.label,
5953
6213
  ...(s.sub ? { sub: s.sub } : {}),
6214
+ ...(s.side ? { side: s.side } : {}),
5954
6215
  ...(s.tone ? { tone: s.tone } : {}),
5955
6216
  ...(s.style ? { style: s.style } : {}),
5956
6217
  });
@@ -5970,7 +6231,7 @@ function compileSwimlane(doc: DslDocument): CdlDiagram {
5970
6231
  lane: swim.laneId(a.name),
5971
6232
  stack: 0,
5972
6233
  kind: a.kind ?? "actor",
5973
- title: a.name,
6234
+ title: 箱の題(a),
5974
6235
  });
5975
6236
  });
5976
6237
  }
@@ -5995,7 +6256,7 @@ function compileEr(doc: DslDocument): CdlDiagram {
5995
6256
  // v0.3 で「列定義」 ブロックを追加検討
5996
6257
  erBuilder.entity({
5997
6258
  id: slugify(a.name) || a.name,
5998
- title: a.name,
6259
+ title: 箱の題(a),
5999
6260
  rows: [], // v0.2 では rows なし
6000
6261
  });
6001
6262
  }
@@ -6052,7 +6313,7 @@ function compileState(doc: DslDocument): CdlDiagram {
6052
6313
  const final = 決め方.終わり(a, i);
6053
6314
  fsm.state({
6054
6315
  id: slugify(a.name) || `s${i}`,
6055
- title: a.name,
6316
+ title: 箱の題(a),
6056
6317
  ...(initial ? { initial: true } : {}),
6057
6318
  ...(final ? { final: true } : {}),
6058
6319
  });
@@ -6093,13 +6354,14 @@ function compileTopology(doc: DslDocument): CdlDiagram {
6093
6354
  groupBuilder.add({
6094
6355
  id: slugify(a.name) || a.name,
6095
6356
  kind: a.kind,
6096
- title: a.name,
6357
+ title: 箱の題(a),
6097
6358
  });
6098
6359
  }
6099
6360
  for (const s of doc.flow) {
6100
6361
  topo.connect(slugify(s.from), slugify(s.to), {
6101
6362
  label: s.label,
6102
6363
  ...(s.sub ? { sub: s.sub } : {}),
6364
+ ...(s.side ? { side: s.side } : {}),
6103
6365
  ...(s.tone ? { tone: s.tone } : {}),
6104
6366
  ...(s.style ? { style: s.style } : {}),
6105
6367
  });
@@ -6196,16 +6458,20 @@ function compileGenericWithAnimate(doc: DslDocument, opts: GenericOpts): CdlDiag
6196
6458
  const lid = a.lane!;
6197
6459
  const stack = 積んだ数.get(lid) ?? 0;
6198
6460
  積んだ数.set(lid, stack + 1);
6199
- b.node(id, { lane: lid, stack, kind: a.kind, title: a.name });
6461
+ b.node(id, { lane: lid, stack, kind: a.kind, title: 箱の題(a) });
6200
6462
  });
6201
6463
  } else if (kind === "flow" || kind === "topology") {
6202
6464
  // 1 lane に全 actor を縦 stack
6203
6465
  const lid = opts.laneId ?? "main";
6204
- b.lane(lid, { width: laneWidth, label: doc.title, ...(kind === "topology" ? { contain: true } : {}) });
6466
+ b.lane(lid, {
6467
+ width: laneWidth,
6468
+ label: doc.title,
6469
+ ...(kind === "topology" ? { contain: true } : {}),
6470
+ });
6205
6471
  doc.actors.forEach((a, idx) => {
6206
6472
  const id = slugify(a.name) || `n${idx}`;
6207
6473
  actorToNodeId.set(a.name, id);
6208
- b.node(id, { lane: lid, stack: idx, kind: a.kind, title: a.name });
6474
+ b.node(id, { lane: lid, stack: idx, kind: a.kind, title: 箱の題(a) });
6209
6475
  });
6210
6476
  } else {
6211
6477
  // swimlane / er / state ... actor ごとに 1 lane (横並び)
@@ -6231,7 +6497,7 @@ function compileGenericWithAnimate(doc: DslDocument, opts: GenericOpts): CdlDiag
6231
6497
  lane: lid,
6232
6498
  stack: 0,
6233
6499
  kind: a.kind,
6234
- title: a.name,
6500
+ title: 箱の題(a),
6235
6501
  ...(isInitial ? { eyebrow: "初期" } : {}),
6236
6502
  ...(isFinal ? { eyebrow: "最終" } : {}),
6237
6503
  });
@@ -6264,8 +6530,11 @@ function compileGenericWithAnimate(doc: DslDocument, opts: GenericOpts): CdlDiag
6264
6530
  b.edge(fromId, toId, {
6265
6531
  id: edgeId,
6266
6532
  label: labelWithCard,
6267
- ...後ろへ戻る矢印か(kind, fromId, toId, 箱の並び) ? { routing: "back-detour" as const } : {},
6533
+ ...(後ろへ戻る矢印か(kind, fromId, toId, 箱の並び)
6534
+ ? { routing: "back-detour" as const }
6535
+ : {}),
6268
6536
  ...(s.sub ? { sub: s.sub } : {}),
6537
+ ...(s.side ? { side: s.side } : {}),
6269
6538
  ...(s.tone ? { tone: s.tone } : {}),
6270
6539
  ...(s.style ? { style: s.style } : {}),
6271
6540
  });