@cardenelabs/dragon 0.13.0 → 0.15.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cardenelabs/dragon",
3
- "version": "0.13.0",
3
+ "version": "0.15.0",
4
4
  "description": "Dragon — Mermaid 感覚で animated SVG を生成する Text DSL。 cdl engine を内部利用。",
5
5
  "license": "MIT",
6
6
  "author": "cardene777",
@@ -46,15 +46,6 @@
46
46
  "node": ">=18"
47
47
  },
48
48
  "sideEffects": false,
49
- "scripts": {
50
- "build": "tsup",
51
- "dev": "tsup --watch",
52
- "typecheck": "tsc --noEmit",
53
- "test": "cd ../.. && vitest run packages/dragon",
54
- "test:watch": "cd ../.. && vitest packages/dragon",
55
- "lint": "tsc --noEmit",
56
- "prepublishOnly": "pnpm run typecheck && pnpm run test && pnpm run build"
57
- },
58
49
  "dependencies": {
59
50
  "@cardenelabs/cdl": "^0.14.0"
60
51
  },
@@ -75,5 +66,14 @@
75
66
  "react": "^19.1.0",
76
67
  "tsup": "^8.5.1",
77
68
  "typescript": "^5.8.0"
69
+ },
70
+ "scripts": {
71
+ "build": "tsup",
72
+ "dev": "tsup --watch",
73
+ "typecheck": "tsc --noEmit",
74
+ "typecheck:test": "tsc --noEmit -p tsconfig.test.json",
75
+ "test": "cd ../.. && vitest run packages/dragon",
76
+ "test:watch": "cd ../.. && vitest packages/dragon",
77
+ "lint": "tsc --noEmit"
78
78
  }
79
- }
79
+ }
package/src/color.ts CHANGED
@@ -106,11 +106,13 @@ export type StrippedPaint = { path: string; value: string };
106
106
  /**
107
107
  * 図の中から、 色を塗る位置に入った外部参照を落とす。
108
108
  *
109
- * 対象は 2 種類ある。
109
+ * 対象は 3 種類ある。
110
110
  *
111
111
  * - 色を塗る key (`fill` / `stroke` / `bg` 等) の値
112
112
  * - 状態の値 (`states[].initial` と、 phase が状態へ入れる値)。 状態は `{名前}` の形で
113
113
  * `fill` に差し込まれるため、 色を塗る位置に届く
114
+ * - つまみの値 (`inputs[].defaultValue` / `defaultValues` / `options`)。 これらも状態を上書きし、
115
+ * 同じ形で paint に届く
114
116
  *
115
117
  * 説明文 (`title` / `subtitle` / `value` / `rows`) は対象外。 文字として出るだけで
116
118
  * 属性にはならないため、 URL を書く正当な用途を壊さない。
@@ -119,23 +121,40 @@ export type StrippedPaint = { path: string; value: string };
119
121
  */
120
122
  export function stripExternalPaint(diagram: unknown): StrippedPaint[] {
121
123
  const stripped: StrippedPaint[] = [];
122
- walk(diagram, "", false, stripped);
124
+ walk(diagram, "", false, stripped, false);
123
125
  return stripped;
124
126
  }
125
127
 
128
+ /** つまみから状態値になり得る文字列の欄。 */
129
+ const INPUT_VALUE_KEYS: ReadonlySet<string> = new Set(["defaultvalue", "defaultvalues", "options"]);
130
+
126
131
  /**
127
132
  * 図の中を辿って外部参照を落とす。
128
133
  *
129
134
  * `inStateValue` = 今見ている場所が状態の値かどうか。 状態は key の名前が `initial` や
130
135
  * 状態名そのもの (phase の `sets`) になるため、 key の名前だけでは色かどうか分からない。
131
136
  * 「状態を入れる箱の中にいる」 ことを引き継いで判断する。
137
+ * `inInputs` は `inputs` の中にいることを示し、値になる欄だけを `inStateValue` へ合流させる。
132
138
  */
133
- function walk(node: unknown, path: string, inStateValue: boolean, out: StrippedPaint[]): void {
139
+ function walk(
140
+ node: unknown,
141
+ path: string,
142
+ inStateValue: boolean,
143
+ out: StrippedPaint[],
144
+ inInputs: boolean,
145
+ ): void {
134
146
  if (node === null || typeof node !== "object") return;
135
147
 
136
148
  if (Array.isArray(node)) {
137
149
  for (let i = 0; i < node.length; i++) {
138
- walk(node[i], `${path}[${i}]`, inStateValue, out);
150
+ const here = `${path}[${i}]`;
151
+ const value = node[i];
152
+ if (inStateValue && typeof value === "string" && pointsOutside(value)) {
153
+ node[i] = SAFE_PAINT;
154
+ out.push({ path: here, value });
155
+ } else {
156
+ walk(value, here, inStateValue, out, inInputs);
157
+ }
139
158
  }
140
159
  return;
141
160
  }
@@ -145,7 +164,13 @@ function walk(node: unknown, path: string, inStateValue: boolean, out: StrippedP
145
164
  const here = path ? `${path}.${key}` : key;
146
165
  const lower = key.toLowerCase();
147
166
  // 状態を入れる箱に入ったら、 その中の値はすべて状態の値として扱う
148
- const nextInState = inStateValue || lower === "states" || lower === "sets" || lower === "tweens";
167
+ const nextInState =
168
+ inStateValue ||
169
+ lower === "states" ||
170
+ lower === "sets" ||
171
+ lower === "tweens" ||
172
+ (inInputs && INPUT_VALUE_KEYS.has(lower));
173
+ const nextInInputs = inInputs || lower === "inputs";
149
174
 
150
175
  if (typeof value === "string") {
151
176
  const isPaint = PAINT_KEYS.has(lower) || (nextInState && (lower === "initial" || lower === "to" || lower === "from" || !isReservedStateKey(lower)));
@@ -155,7 +180,7 @@ function walk(node: unknown, path: string, inStateValue: boolean, out: StrippedP
155
180
  }
156
181
  continue;
157
182
  }
158
- walk(value, here, nextInState, out);
183
+ walk(value, here, nextInState, out, nextInInputs);
159
184
  }
160
185
  }
161
186
 
package/src/compile.ts CHANGED
@@ -15,6 +15,7 @@ import type {
15
15
  DslLane,
16
16
  DslPhase,
17
17
  DslStep,
18
+ DslEventBinding,
18
19
  DslValue,
19
20
  PresetType,
20
21
  } from "./types";
@@ -24,6 +25,7 @@ import type {
24
25
  ErRelationCardinality,
25
26
  FormulaAst,
26
27
  LaidDiagram,
28
+ NodeKind,
27
29
  } from "@cardenelabs/cdl";
28
30
  import {
29
31
  sequence,
@@ -40,9 +42,11 @@ import {
40
42
  NODE_KINDS,
41
43
  applyDerivedValues,
42
44
  parseFormula,
45
+ extractIdentifiers,
46
+ inputDefaultValue,
43
47
  } from "@cardenelabs/cdl";
44
48
  import { parseFocusEntry } from "./focus";
45
- import { DRAW_TARGETS } from "./v05/parser";
49
+ import { DRAW_TARGETS, DSL_ONLY_KINDS } from "./v05/parser";
46
50
  import { isColorValue, pointsOutside, stripExternalPaint } from "./color";
47
51
  import {
48
52
  MAX_INPUT_ELEMENTS,
@@ -57,6 +61,56 @@ import {
57
61
  type RelativeDirection,
58
62
  } from "./relative-pos";
59
63
 
64
+ /**
65
+ * 記法だけが持つ種類を、描画できる種類へ読み替える (#1420)。
66
+ *
67
+ * 記法は `contract` / `eoa` のような **描画側に無い種類** を受け付ける
68
+ * (`v05/parser.ts` の `DSL_ONLY_KINDS`)。 図種ごとの役割分け (`solidity` の縦列の並べ替え
69
+ * など) に使うためで、記法としては正しい。
70
+ *
71
+ * **そのまま描画側へ渡すと図の組み立てが落ちる**。 描画側は知らない種類の大きさを引けず、
72
+ * `Cannot read properties of undefined (reading 'h')` で止まる (実測)。
73
+ *
74
+ * `solidity` と `er` は組み立ての中で別の種類に置き換えていたが、`flow` / `swimlane` /
75
+ * `state` / `topology` は `a.kind` をそのまま渡していた。 記法が受ける値で図が出ない状態
76
+ * だったので、渡す手前で必ず通す。
77
+ *
78
+ * ## 読み替え先
79
+ *
80
+ * | 種類 | 読み替え先 | なぜ |
81
+ * |---|---|---|
82
+ * | `entity` | `storage` | 表を持つ = ER 図の実体 |
83
+ * | `state` | `card` | 状態は札で表す |
84
+ * | `contract` / `proxy` / `library` / `interface` | `card` | 契約は札で表す (`solidity` の置き換え先に合わせた) |
85
+ * | `eoa` | `person` | 人が持つ財布 |
86
+ * | `multisig` | `signer` | 複数人で署名する (`solidity` の置き換え先に合わせた) |
87
+ *
88
+ * **表は `DSL_ONLY_KINDS` を鍵にして書く**。 種類を足した時に読み替え先が無いと
89
+ * 型検査が落ちるので、足し忘れが残らない。
90
+ */
91
+ const 記法だけの種類の読み替え: Readonly<
92
+ Record<(typeof DSL_ONLY_KINDS)[number], NodeKind>
93
+ > = {
94
+ entity: "storage",
95
+ state: "card",
96
+ contract: "card",
97
+ proxy: "card",
98
+ library: "card",
99
+ interface: "card",
100
+ eoa: "person",
101
+ multisig: "signer",
102
+ };
103
+
104
+ /** 描画側へ渡せる種類にする。 記法だけの種類はここで読み替わる (#1420) */
105
+ function 描ける種別(kind: string | undefined): NodeKind {
106
+ if (kind === undefined) return "actor";
107
+ const 読み替え先 = (
108
+ 記法だけの種類の読み替え as Record<string, NodeKind | undefined>
109
+ )[kind];
110
+ return 読み替え先 ?? (kind as NodeKind);
111
+ }
112
+
113
+
60
114
  export interface CompileToCdlOpts {
61
115
  /**
62
116
  * CAR-1657 = parts identifier lookup catalog、 caller (CdlEditor / test) が inject。
@@ -121,7 +175,11 @@ export type CompileNotice = {
121
175
  // 起点から描く動きを持たない図種で段に `draw:` を書いた (#1312)
122
176
  | "draw-not-honored"
123
177
  // `draw:` の語がその図種と食い違う (`type: bar` に `draw: pie`、 #1314)
124
- | "draw-target-mismatch";
178
+ | "draw-target-mismatch"
179
+ // 式が、どこにも書かれていない名前を読んだ (#1391)
180
+ | "formula-unresolved"
181
+ // 出来事が指す相手が図に無い (#1393)
182
+ | "event-target-missing";
125
183
  /** 対象の名前。 光らせる相手なら書かれた指定そのまま */
126
184
  actor: string;
127
185
  /** 書かれていた行 */
@@ -315,6 +373,132 @@ export function compileToCdl(doc: DslDocument, opts?: CompileToCdlOpts): CdlDiag
315
373
  );
316
374
  merged.readouts = [...(merged.readouts ?? []), ...ownReadouts];
317
375
  }
376
+ /*
377
+ * 読む人が動かすつまみを図に載せる (#1389)。
378
+ *
379
+ * 部品と同じく写して載せる = 出口の paint 検査が diagram を直接書き換えるため、doc の
380
+ * object を共有すると `compileToCdl` の入力まで書き換わって入力不変性が壊れる。
381
+ */
382
+ if (doc.inputs && doc.inputs.length > 0) {
383
+ const ownInputs = doc.inputs.map(
384
+ (input) => deepRewriteStrings(input, (value) => value) as typeof input,
385
+ );
386
+ merged.inputs = [...(merged.inputs ?? []), ...ownInputs];
387
+ }
388
+ /*
389
+ * つまみの値から決まる値を図に載せる (#1391)。
390
+ *
391
+ * 部品やつまみと同じく写して載せる。 式の文字列は色を塗る位置に届かないが、
392
+ * 入力を書き換えない形を 3 経路で揃える方が読み手に説明しやすい。
393
+ */
394
+ if (doc.formulas && doc.formulas.length > 0) {
395
+ const ownFormulas = doc.formulas.map((formula) => ({
396
+ id: formula.id,
397
+ expression: formula.expression,
398
+ line: formula.pos?.line ?? 0,
399
+ }));
400
+ /*
401
+ * 式が読む名前が、どこにも書かれていないことを知らせる (#1391)。
402
+ *
403
+ * engine が読める名前は、つまみと **先に宣言した式** だけ。綴り違い、状態、
404
+ * `values:`、後から宣言する式を渡すと、描画時に未定義参照として例外になる。
405
+ *
406
+ * **解けない式は図へ載せない**。 engine は未定義参照や input/formula の同名衝突を
407
+ * runtime error にするため、残すと図全体が描けない。式以外の図は出し、知らせを返す。
408
+ */
409
+ const つまみ = new Map((merged.inputs ?? []).map((input) => [input.id, input]));
410
+ const 先に書かれた式 = new Set((merged.formulas ?? []).map((formula) => formula.id));
411
+ const 載せる式 = [...(merged.formulas ?? [])];
412
+ for (const formula of ownFormulas) {
413
+ if (つまみ.has(formula.id) || 先に書かれた式.has(formula.id)) {
414
+ opts?.onNotice?.({
415
+ kind: "formula-unresolved",
416
+ actor: formula.id,
417
+ line: formula.line,
418
+ message: `式 "${formula.id}" の名前が、先に書かれたつまみまたは式と重なっています`,
419
+ hint: "`inputs:` と `formulas:` では重ならない名前を使う",
420
+ });
421
+ continue;
422
+ }
423
+ let 名前たち: Set<string>;
424
+ try {
425
+ 名前たち = extractIdentifiers(parseFormula(formula.expression));
426
+ } catch {
427
+ // 読めない式は記法の読み取りが既に知らせている。 ここで二重に出さない
428
+ continue;
429
+ }
430
+ let 解けない = false;
431
+ for (const 名 of 名前たち) {
432
+ if (先に書かれた式.has(名)) continue;
433
+ const input = つまみ.get(名);
434
+ if (input) {
435
+ const 初期値 = inputDefaultValue(input);
436
+ if (typeof 初期値 === "number" || typeof 初期値 === "boolean") continue;
437
+ 解けない = true;
438
+ opts?.onNotice?.({
439
+ kind: "formula-unresolved",
440
+ actor: formula.id,
441
+ line: formula.line,
442
+ message: `式 "${formula.id}" が、数でも真偽でもないつまみ "${名}" を読んでいます`,
443
+ hint: "式が読めるつまみは slider / number / stepper / timeline / toggle",
444
+ });
445
+ continue;
446
+ }
447
+ 解けない = true;
448
+ opts?.onNotice?.({
449
+ kind: "formula-unresolved",
450
+ actor: formula.id,
451
+ line: formula.line,
452
+ message: `式 "${formula.id}" が、つまみまたは先に書かれた式ではない名前 "${名}" を読んでいます`,
453
+ hint: "`inputs:` に書くか、参照される式をこの式より前に書く",
454
+ });
455
+ }
456
+ if (解けない) continue;
457
+ 載せる式.push({ id: formula.id, expression: formula.expression });
458
+ 先に書かれた式.add(formula.id);
459
+ }
460
+ if (載せる式.length > 0) merged.formulas = 載せる式;
461
+ else delete merged.formulas;
462
+ }
463
+ /*
464
+ * 押下などの出来事で動く仕掛けを図に載せる (#1393)。
465
+ *
466
+ * **相手の名前を識別子へ直す**。 記法は識別子を書けないため名前で指す。 指す先が
467
+ * 見つからない形は載せずに知らせる = 描画側は知らない識別子を黙って無視するため、
468
+ * 残すと「書いたのに押しても何も起きない」 が手掛かりなしで起きる。
469
+ *
470
+ * 識別子は書いた順に `evt-1` から振る (組み立て API と同じ)。
471
+ */
472
+ if (doc.events && doc.events.length > 0) {
473
+ const 載せる: NonNullable<CdlDiagram["eventBindings"]> = [...(merged.eventBindings ?? [])];
474
+ for (const e of doc.events) {
475
+ const 相手 = 出来事の相手を解く(merged, doc, e);
476
+ if (相手 === undefined) {
477
+ opts?.onNotice?.({
478
+ kind: "event-target-missing",
479
+ actor: e.handlerId,
480
+ line: e.pos.line,
481
+ message: `出来事 "${e.handlerId}" が指す相手が見つかりません`,
482
+ hint: "box は箱の名前、 lane は縦列の名前、 arrow は `A -> B` で書く",
483
+ });
484
+ continue;
485
+ }
486
+ 載せる.push({
487
+ id: `evt-${載せる.length + 1}`,
488
+ event: e.event,
489
+ target: 相手,
490
+ handlerId: e.handlerId,
491
+ });
492
+ }
493
+ if (載せる.length > 0) merged.eventBindings = 載せる;
494
+ }
495
+ // 巻き上げに応じて進む値を図に載せる (#1393)。 相手を持たないのでそのまま写す
496
+ if (doc.scrolls && doc.scrolls.length > 0) {
497
+ merged.scrollTriggers = [
498
+ ...(merged.scrollTriggers ?? []),
499
+ ...doc.scrolls.map((x) => ({ ...x })),
500
+ ];
501
+ }
318
502
  // 図の外を指す値を、 色を塗る位置から落とす (#1004)。
319
503
  //
320
504
  // 入口ごとに塞ぐ形は採らない。 状態の上書き / phase が入れる値 / 画面が直接書く背景色 /
@@ -997,6 +1181,26 @@ function disambiguateActorIds(
997
1181
  const to = 直す(s.to);
998
1182
  return from === s.from && to === s.to ? s : { ...s, from, to };
999
1183
  }),
1184
+ events: doc.events?.map((event) => {
1185
+ const target = event.target;
1186
+ if (target.kind === "node") {
1187
+ const name = 直す(target.name);
1188
+ return name === target.name ? event : { ...event, target: { ...target, name } };
1189
+ }
1190
+ if (target.kind === "edge") {
1191
+ const from = 直す(target.from);
1192
+ const to = 直す(target.to);
1193
+ return from === target.from && to === target.to
1194
+ ? event
1195
+ : { ...event, target: { ...target, from, to } };
1196
+ }
1197
+ // sequence 系の縦列は登場人物から作るため、同じ名前の読み替えが必要。
1198
+ if (target.kind === "lane" && (doc.type === "sequence" || doc.type === "solidity")) {
1199
+ const name = 直す(target.name);
1200
+ return name === target.name ? event : { ...event, target: { ...target, name } };
1201
+ }
1202
+ return event;
1203
+ }),
1000
1204
  };
1001
1205
  // 光らせる指定と位置の基準も名前で書くので、 同じ表で直す
1002
1206
  if (次.animate) {
@@ -3048,6 +3252,49 @@ function applyEdgeInlineOptions(
3048
3252
  });
3049
3253
  }
3050
3254
 
3255
+ /**
3256
+ * 出来事が指す相手を、図の識別子へ直す (#1393)。 見つからなければ `undefined`。
3257
+ *
3258
+ * 箱と縦列は **名前から作った識別子** と **描かれる題** の両方で探す。 記法は名前で書き、
3259
+ * 図の識別子はそこから作られるが、見本を重ねた図など識別子が名前と揃わない形もある。
3260
+ * 矢印は両端の名前から識別子を作って突き合わせる。
3261
+ */
3262
+ function 出来事の相手を解く(
3263
+ diagram: CdlDiagram,
3264
+ doc: DslDocument,
3265
+ e: DslEventBinding,
3266
+ ): NonNullable<CdlDiagram["eventBindings"]>[number]["target"] | undefined {
3267
+ if (e.target.kind === "diagram") return { kind: "diagram" };
3268
+ if (e.target.kind === "edge") {
3269
+ const from = slugify(e.target.from);
3270
+ const to = slugify(e.target.to);
3271
+ let 矢印: CdlEdge | undefined;
3272
+ if (doc.type === "sequence" || doc.type === "solidity") {
3273
+ const stepIdx = doc.flow.findIndex(
3274
+ (step) => slugify(step.from) === from && slugify(step.to) === to,
3275
+ );
3276
+ if (stepIdx >= 0) {
3277
+ 矢印 = diagram.edges.find(
3278
+ (x) =>
3279
+ (x.from === `s${stepIdx}-${from}` || x.from === from) &&
3280
+ (x.to === `s${stepIdx}-${to}` || x.from === x.to),
3281
+ );
3282
+ }
3283
+ } else {
3284
+ 矢印 = diagram.edges.find((x) => x.from === from && x.to === to);
3285
+ }
3286
+ return 矢印 ? { kind: "edge", id: 矢印.id } : undefined;
3287
+ }
3288
+ const 名 = e.target.name;
3289
+ const slug = slugify(名);
3290
+ if (e.target.kind === "node") {
3291
+ const 箱 = diagram.nodes.find((n) => n.id === slug || n.id === 名 || n.title === 名);
3292
+ return 箱 ? { kind: "node", id: 箱.id } : undefined;
3293
+ }
3294
+ const 列 = (diagram.lanes ?? []).find((l) => l.id === slug || l.id === 名 || l.label === 名);
3295
+ return 列 ? { kind: "lane", id: 列.id } : undefined;
3296
+ }
3297
+
3051
3298
  /** 本文に書いた矢印の指定を、対応が取れた矢印へ書き写す。 対応の取り方は呼出側が決める。 */
3052
3299
  function 矢印へ書き写す(target: CdlEdge, s: DslStep, doc: DslDocument): void {
3053
3300
  // **書いた補足が勝つ** (#1275)。 ここで写さないと 2 つ落ちる。 静止した `type: flow` は
@@ -3071,6 +3318,11 @@ function 矢印へ書き写す(target: CdlEdge, s: DslStep, doc: DslDocument): v
3071
3318
  if (s.labelOffsetX !== undefined) target.labelOffsetX = s.labelOffsetX;
3072
3319
  if (s.labelOffsetY !== undefined) target.labelOffsetY = s.labelOffsetY;
3073
3320
  if (s.overlay !== undefined) target.overlay = s.overlay;
3321
+ // 値に追随する 3 欄 (#1396)。 太さ / 色 / 破線の位置が値に合わせて動く。
3322
+ // 通り道そのものは動かないので、配置計算と重なり解消には影響しない
3323
+ if (s.widthBind !== undefined) target.widthBind = s.widthBind;
3324
+ if (s.strokeBind !== undefined) target.strokeBind = s.strokeBind;
3325
+ if (s.dashOffsetBind !== undefined) target.dashOffsetBind = s.dashOffsetBind;
3074
3326
  }
3075
3327
 
3076
3328
  /**
@@ -5129,7 +5381,7 @@ function compileC4(doc: DslDocument): CdlDiagram {
5129
5381
  b.node(x.id, {
5130
5382
  lane: lid,
5131
5383
  stack,
5132
- kind: x.actor.kind,
5384
+ kind: 描ける種別(x.actor.kind),
5133
5385
  title: x.actor.name,
5134
5386
  });
5135
5387
  }
@@ -5255,7 +5507,13 @@ type 放射で描けない欄 =
5255
5507
  // 箱の中に描く図形 (#1374)。 放射の枝は箱の中に図形を持たない
5256
5508
  | "shape"
5257
5509
  // 出す条件 (#1381)。 放射の枝は個別に出し分けられない
5258
- | "visibleIf";
5510
+ | "visibleIf"
5511
+ // 値に追随する 5 欄 (#1392)。 放射の枝は大きさも位置も中心からの配置で決まる
5512
+ | "wBind"
5513
+ | "hBind"
5514
+ | "opacity"
5515
+ | "renderOffsetX"
5516
+ | "renderOffsetY";
5259
5517
 
5260
5518
  /** 引数が `never` でなければ型検査が落ちる */
5261
5519
  type 空であること<T extends never> = T;
@@ -5312,6 +5570,11 @@ const 放射で描けない欄の名前: Record<放射で描けない欄, string
5312
5570
  layoutPos: "配置のずらし",
5313
5571
  shape: "箱の中の図形",
5314
5572
  visibleIf: "出す条件",
5573
+ wBind: "値に追随する大きさ",
5574
+ hBind: "値に追随する大きさ",
5575
+ opacity: "濃さ",
5576
+ renderOffsetX: "描く時のずらし",
5577
+ renderOffsetY: "描く時のずらし",
5315
5578
  };
5316
5579
 
5317
5580
  /**
@@ -5737,6 +6000,18 @@ function applyV05Extensions(
5737
6000
  if (footer) footer.visibleIf = a.visibleIf;
5738
6001
  }
5739
6002
  }
6003
+ /*
6004
+ * 値に追随する 5 欄 (#1392)。
6005
+ *
6006
+ * **名札と足へは渡さない**。 `visibleIf` は片方だけ隠すと順序図の縦線の頭と足が
6007
+ * 食い違うため揃えるが、こちらは見た目の大きさ / 濃さ / ずらしで、名札まで同じだけ
6008
+ * 動かすと縦線の頭が本体から離れる。 書いた箱にだけ効かせる。
6009
+ */
6010
+ if (a.wBind !== undefined) node.wBind = a.wBind;
6011
+ if (a.hBind !== undefined) node.hBind = a.hBind;
6012
+ if (a.opacity !== undefined) node.opacity = a.opacity;
6013
+ if (a.renderOffsetX !== undefined) node.renderOffsetX = a.renderOffsetX;
6014
+ if (a.renderOffsetY !== undefined) node.renderOffsetY = a.renderOffsetY;
5740
6015
  }
5741
6016
  }
5742
6017
  // 名札の高さを揃える。 kind ごとに高さが変わると縦線の始まる位置がばらけ、 順序図の
@@ -6160,7 +6435,7 @@ function compileFlow(doc: DslDocument): CdlDiagram {
6160
6435
  flowBuilder.step(
6161
6436
  {
6162
6437
  id: slugify(a.name) || `n${i}`,
6163
- kind: a.kind,
6438
+ kind: 描ける種別(a.kind),
6164
6439
  title: 箱の題(a),
6165
6440
  },
6166
6441
  edgeLabel,
@@ -6199,7 +6474,7 @@ function compileSwimlane(doc: DslDocument): CdlDiagram {
6199
6474
  swim.node(nodeId, {
6200
6475
  lane: laneId,
6201
6476
  stack,
6202
- kind: actor?.kind ?? "actor",
6477
+ kind: 描ける種別(actor?.kind),
6203
6478
  title: actorName,
6204
6479
  });
6205
6480
  laneStackCount.set(laneId, stack + 1);
@@ -6230,7 +6505,7 @@ function compileSwimlane(doc: DslDocument): CdlDiagram {
6230
6505
  swim.node(slugify(a.name) || `n${i}`, {
6231
6506
  lane: swim.laneId(a.name),
6232
6507
  stack: 0,
6233
- kind: a.kind ?? "actor",
6508
+ kind: 描ける種別(a.kind),
6234
6509
  title: 箱の題(a),
6235
6510
  });
6236
6511
  });
@@ -6353,7 +6628,7 @@ function compileTopology(doc: DslDocument): CdlDiagram {
6353
6628
  for (const a of doc.actors) {
6354
6629
  groupBuilder.add({
6355
6630
  id: slugify(a.name) || a.name,
6356
- kind: a.kind,
6631
+ kind: 描ける種別(a.kind),
6357
6632
  title: 箱の題(a),
6358
6633
  });
6359
6634
  }
@@ -6441,24 +6716,64 @@ function compileGenericWithAnimate(doc: DslDocument, opts: GenericOpts): CdlDiag
6441
6716
  // lane / node 配置 ... preset kind に応じて切替
6442
6717
  const actorToNodeId = new Map<string, string>();
6443
6718
  if (書いた縦列に置く(kind, doc)) {
6444
- // **書いた縦列に置く** (#1263)。 縦列を並べるための入れ物として使う図種でだけ効く。
6445
- // 縦列は書かれた順に作り、同じ縦列の箱は書かれた順に積む
6446
- const 並び: string[] = [];
6719
+ /*
6720
+ * **書いた縦列に置く** (#1263)。 縦列を並べるための入れ物として使う図種でだけ効く。
6721
+ *
6722
+ * 縦列の並びは **`lanes:` に書いた順** を優先する (#1394)。 箱が最初に使った順で
6723
+ * 並べていた間、`lanes:` で左から順に宣言しても箱の書き順で入れ替わっていた
6724
+ * (実測 = 中心を先に書いた放射の図で、左端の縦列が中心の右へ回った)。
6725
+ *
6726
+ * `lanes:` に無い縦列は、これまでどおり箱が使った順で後ろに続ける。
6727
+ */
6728
+ const 使った: string[] = [];
6447
6729
  for (const a of doc.actors) {
6448
6730
  const lid = a.lane;
6449
- if (lid !== undefined && !並び.includes(lid)) 並び.push(lid);
6731
+ if (lid !== undefined && !使った.includes(lid)) 使った.push(lid);
6450
6732
  }
6733
+ const 書いた順 = doc.lanes ? Object.keys(doc.lanes) : [];
6734
+ const 並び = [
6735
+ ...書いた順.filter((lid) => 使った.includes(lid)),
6736
+ ...使った.filter((lid) => !書いた順.includes(lid)),
6737
+ ];
6451
6738
  for (const lid of 並び) {
6452
6739
  b.lane(lid, { width: laneWidth, ...(kind === "topology" ? { contain: true } : {}) });
6453
6740
  }
6454
- const 積んだ数 = new Map<string, number>();
6741
+ /*
6742
+ * 段は **書いた番号をそのまま持つ** (#1394)。
6743
+ *
6744
+ * 書き順で 0 から詰め直していた間、`stack: 1` と書いた箱が 0 へ落ちていた。
6745
+ * 決定木のように「同じ高さに並ばない」 ことが図の意味そのものになる形では、
6746
+ * 詰めた瞬間に別の図になる (実測 = 3 段の木の根が 1 段目へ上がった)。
6747
+ *
6748
+ * 書かなかった箱は、その縦列で **空いている一番小さい段** に置く。 単に数え上げると
6749
+ * 書いた番号と重なり、2 つの箱が同じ段に載る。
6750
+ */
6751
+ const 埋まった段 = new Map<string, Set<number>>();
6752
+ const 埋める = (lid: string, stack: number): void => {
6753
+ const 集合 = 埋まった段.get(lid) ?? new Set<number>();
6754
+ 集合.add(stack);
6755
+ 埋まった段.set(lid, 集合);
6756
+ };
6757
+ for (const a of doc.actors) {
6758
+ if (a.lane !== undefined && a.stack !== undefined) 埋める(a.lane, a.stack);
6759
+ }
6455
6760
  doc.actors.forEach((a, idx) => {
6456
6761
  const id = slugify(a.name) || `n${idx}`;
6457
6762
  actorToNodeId.set(a.name, id);
6458
6763
  const lid = a.lane!;
6459
- const stack = 積んだ数.get(lid) ?? 0;
6460
- 積んだ数.set(lid, stack + 1);
6461
- b.node(id, { lane: lid, stack, kind: a.kind, title: 箱の題(a) });
6764
+ let stack = a.stack;
6765
+ if (stack === undefined) {
6766
+ stack = 0;
6767
+ const 集合 = 埋まった段.get(lid);
6768
+ while (集合?.has(stack)) stack += 1;
6769
+ 埋める(lid, stack);
6770
+ }
6771
+ b.node(id, {
6772
+ lane: lid,
6773
+ stack,
6774
+ kind: 描ける種別(a.kind),
6775
+ title: 箱の題(a),
6776
+ });
6462
6777
  });
6463
6778
  } else if (kind === "flow" || kind === "topology") {
6464
6779
  // 1 lane に全 actor を縦 stack
@@ -6471,7 +6786,12 @@ function compileGenericWithAnimate(doc: DslDocument, opts: GenericOpts): CdlDiag
6471
6786
  doc.actors.forEach((a, idx) => {
6472
6787
  const id = slugify(a.name) || `n${idx}`;
6473
6788
  actorToNodeId.set(a.name, id);
6474
- b.node(id, { lane: lid, stack: idx, kind: a.kind, title: 箱の題(a) });
6789
+ b.node(id, {
6790
+ lane: lid,
6791
+ stack: idx,
6792
+ kind: 描ける種別(a.kind),
6793
+ title: 箱の題(a),
6794
+ });
6475
6795
  });
6476
6796
  } else {
6477
6797
  // swimlane / er / state ... actor ごとに 1 lane (横並び)
@@ -6496,7 +6816,7 @@ function compileGenericWithAnimate(doc: DslDocument, opts: GenericOpts): CdlDiag
6496
6816
  b.node(id, {
6497
6817
  lane: lid,
6498
6818
  stack: 0,
6499
- kind: a.kind,
6819
+ kind: 描ける種別(a.kind),
6500
6820
  title: 箱の題(a),
6501
6821
  ...(isInitial ? { eyebrow: "初期" } : {}),
6502
6822
  ...(isFinal ? { eyebrow: "最終" } : {}),
package/src/index.ts CHANGED
@@ -11,6 +11,17 @@
11
11
  export { parseTextDsl } from "./parser";
12
12
  export { compileToCdl } from "./compile";
13
13
  export type { CompileNotice } from "./compile";
14
+ /**
15
+ * 組み立てた図の型 (#1415)。
16
+ *
17
+ * `textDslToDiagram` が返す型そのもので、実体は `@cardenelabs/cdl` が持つ。 使う側が
18
+ * 戻り値に型を付けるには必ず要るので、ここから引けるようにする。
19
+ *
20
+ * 再 export が無い間、使う側は `@cardenelabs/cdl` を直接の依存に足す必要があった。
21
+ * 足さずに書くと **型が暗黙の `any` に落ち、その先の `.map((n) => ...)` まで検査が効かなく
22
+ * なる** (実測 = この 1 件が 39 件の暗黙 `any` を生んでいた)。
23
+ */
24
+ export type { CdlDiagram } from "@cardenelabs/cdl";
14
25
  export { parseTextDslV05 } from "./v05";
15
26
  // 記法一覧が「実際に受け付ける値」 を実装から引くための公開。 手書きすると説明と実装がずれる。
16
27
  export { DRAW_TARGETS, DRAW_WORDS, PHASE_ITEM_WORDS, PRESET_TYPES, TOP_LEVEL_KEYS } from "./v05/parser";
package/src/input-size.ts CHANGED
@@ -69,6 +69,18 @@ export function countDocElements(doc: DslDocument): number {
69
69
  // 値を見せる部品も描画時に 1 widget ずつ展開される (#1374)。 数えないと、 actors が
70
70
  // 少ないまま readouts だけを大量に並べた入力が組み立て前の上限をすり抜ける。
71
71
  (doc.readouts?.length ?? 0) +
72
+ // 読む人が動かすつまみも 1 widget ずつ展開される (#1389)。 readouts と同じ上限で守る。
73
+ (doc.inputs?.length ?? 0) +
74
+ /*
75
+ * 式 (#1391) と、押下 / 巻き上げ (#1393) も数える。
76
+ *
77
+ * **図の側 (`countDiagramElements`) は既に数えている**。 記法側だけ数えないと、
78
+ * 箱が少ないままこれらを大量に並べた入力が組み立て前の上限をすり抜け、
79
+ * 組み立て終わってから (= 時間をかけてから) 図の側で弾かれる。
80
+ */
81
+ (doc.formulas?.length ?? 0) +
82
+ (doc.events?.length ?? 0) +
83
+ (doc.scrolls?.length ?? 0) +
72
84
  (doc.groups ? Object.keys(doc.groups).length : 0) +
73
85
  (doc.lanes ? Object.keys(doc.lanes).length : 0)
74
86
  );