@shibayama/pdgkit 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -124,79 +124,109 @@ pdgkit の主な使い方は、AI に日本語で頼んで図を作らせるこ
124
124
 
125
125
  ### AI に自然文で頼む
126
126
 
127
- いちばん手軽な使い方です。**コードを実行できる AI** に、最初に一度だけ「セットアップ依頼」を貼り付ければ、あとは日本語で「〜の図を描いて」と頼むだけで、図のファイルまで作ってくれます。
127
+ pdgkit の主な使い方は、AI に日本語で頼んで図を作らせることです。**コードを実行できる AI**(ChatGPT のコードインタープリタ、Claude Code / Codex などのエージェント)なら、導入から図のファイル生成まで会話内で完結します。確実なのは次の 2 通りです。
128
128
 
129
- 使える AI(どれか 1 つ):
129
+ - **明示手順プロンプト(下記・推奨)** 1 つのプロンプトに「pdgkit を使う手順」と「発明」をまとめて渡す。AI が自前で図を描いてしまうのを防げます。
130
+ - **MCP 登録** — [MCP サーバとして](#mcp-サーバとして) を一度登録すれば、以降どの会話でも「〜を描いて」と頼むだけで pdgkit が呼ばれます(対応クライアント限定)。
130
131
 
131
- - **ChatGPT**(コードインタープリタが使えるもの)
132
- - **Claude Code / Codex** などのコーディングエージェント
133
- - pdgkit を登録した **Claude(MCP)** … この場合はセットアップ不要。[MCP サーバとして](#mcp-サーバとして) を参照
132
+ > **注意**: 「図を描いて」とだけ頼むと、AI は pdgkit を使わず**自前で図を生成してしまう**ことがあります(特に ChatGPT のコードインタープリタ)。「pdgkit を使う」ことをプロンプトに含めると確実です。コードを実行できない素のチャットでの使い方は「[C. コードを実行できない素のチャット](#c-コードを実行できない素のチャット)」を参照してください。
134
133
 
135
- > ふつうのチャット(コードを実行できない ChatGPT / Claude)でも使えますが、その場合 AI は `.pdg` テキストを書くところまでで、図にするのは手元で行います。後述の「C. コードを実行できない素のチャット」を参照してください。
134
+ #### 明示手順プロンプト(推奨)
136
135
 
137
- #### ステップ 1: 最初に一度だけ、セットアップを頼む
136
+ 次のテンプレの「発明」だけを、図にしたい内容に差し替えて貼り付けます。
138
137
 
139
- 新しいチャットを開き、次の文をそのまま貼り付けます。pdgkit の導入(`npm install @shibayama/pdgkit`)と記法の読み込みまで AI が行います。
138
+ ```text
139
+ pdgkit を使って特許図面を描いてください。自分で図を描かず、必ず下のコマンドを実行して、pdgkit が出力したファイルを返してください。
140
+
141
+ 1. npm install @shibayama/pdgkit
142
+ 2. npx @shibayama/pdgkit guide を実行し、表示された記法ガイドに従う
143
+ 3. 下の発明について .pdg を書いて fig.pdg に保存する(図種は内容から最適なものを選ぶ。私が指定したらそれに従う)
144
+ 4. npx @shibayama/pdgkit validate fig.pdg がエラーなしになるまで直す
145
+ 5. npx @shibayama/pdgkit render fig.pdg -o fig.svg(PNG なら --to png、PDF なら --to pdf)で図を作り、ダウンロードできるようにする
146
+
147
+ 発明:
148
+ 撮像装置に関する。本装置は、被写体を撮像して画像信号を出力する撮像部と、
149
+ 撮像部からの画像信号にノイズ低減とエッジ強調を行う画像処理部と、
150
+ 画像処理部が処理した画像データを格納する記憶部と、
151
+ 画像データを外部サーバへ送信する通信部と、これらを制御する制御部と、を備える。
152
+ 画像は撮像部から画像処理部、画像処理部から記憶部の順に処理され、
153
+ 記憶部の画像は制御部の指示で通信部を介して外部サーバへ送信される。
154
+ ```
155
+
156
+ 「発明」は、構成・方法・状態・やりとり・クレーム文など図にしたい内容に置き換えます(書き方は下の「頼み方の例」を参照)。図種を指定したいときは「フローチャートにして」などと一言添え、出力形式は手順 5 を `--to png` / `--to pdf` / `--to pptx`(編集可能版は `--to pptx --editable`)に変えます。
157
+
158
+ #### 一度セットアップして、続けて頼む(手軽版)
159
+
160
+ 同じ会話で何枚も作るときは、最初に一度だけセットアップを頼み、以降は短い依頼を続けられます。ただし会話が長くなると AI が pdgkit を使うのを忘れて自前描画に戻ることがあります。そうなったら、上の明示手順プロンプトに切り替えてください。
161
+
162
+ 最初に一度だけ、次を貼り付けます。
140
163
 
141
164
  ```text
142
- これから特許図面を作ります。次の準備をしてください。
165
+ これから特許図面を作ります。次の準備をして、以降のルールに従ってください。
143
166
  1. npm install @shibayama/pdgkit を実行する。
144
167
  2. npx @shibayama/pdgkit guide を実行し、表示された PatentDSL(.pdg)の記法に厳密に従う。
145
- 3. 以降、私が日本語で図の内容を伝えます。あなたは .pdg を書いて fig.pdg に保存し、
146
- npx @shibayama/pdgkit validate fig.pdg でエラーが無いことを確認してから、
147
- npx @shibayama/pdgkit render fig.pdg(既定は SVG。指定があれば --to png / jpeg / pdf / pptx
148
- で図のファイルを作り、私がダウンロードできるようにしてください。
149
- 図種(ブロック図・フローチャートなど)や出力形式は、私が指定しなければ
150
- 内容から最適なものを選んで構いません。準備ができたら「準備完了」とだけ返してください。
168
+ 3. 以降この会話では、私が図を頼んだら自分で図を描かず、必ず pdgkit を使うこと。
169
+ .pdg を書いて fig.pdg に保存し、npx @shibayama/pdgkit validate fig.pdg でエラーが無いことを確認してから、
170
+ npx @shibayama/pdgkit render fig.pdg(既定は SVG。指定があれば --to png / jpeg / pdf / pptx)で図のファイルを作り、
171
+ 実行したコマンドを短く示して、私がダウンロードできるようにすること。
172
+ 図種や出力形式は、私が指定しなければ内容から最適なものを選んで構いません。準備ができたら「準備完了」とだけ返してください。
151
173
  ```
152
174
 
153
- `fig.pdg` はファイルの名前です(中身は AI が書きます)。**ファイルの作成・保存・実行はすべて AI が行うので、あなたはファイル名を気にする必要はありません。** 上の文をそのまま貼り付ければ大丈夫です。
175
+ 「準備完了」と返ってきたら、作りたい図を伝えます。**各依頼の頭に「pdgkit で」と付ける**と、pdgkit 経由が確実になります(コードボックス 1 つが 1 つの依頼。コピーして送ってください)。
154
176
 
155
- #### ステップ 2: あとは日本語で頼むだけ
177
+ **構成・装置(ブロック図になりやすい)** 何が何を含み、何がどこへ流れるかを書く:
156
178
 
157
- 「準備完了」と返ってきたら、作りたい図を言葉で伝えるだけです。図種も出力形式も、指定しなければ AI が内容から選びます。
179
+ ```text
180
+ pdgkit で、制御装置の構成図を描いて。制御装置は CPU、メモリ、I/O インターフェースを内蔵し、I/O インターフェースが外部機器へ信号を送る。
181
+ ```
158
182
 
159
- 構成・装置(ブロック図になりやすい):
183
+ ```text
184
+ pdgkit で、センサ端末の構成図を描いて。検出部(温度センサと加速度センサ)、処理部(取得部と判定部)、通信部を備え、検出部から処理部、処理部から通信部の順にデータが流れ、通信部からゲートウェイへ無線送信する。日英併記で。
185
+ ```
160
186
 
161
187
  ```text
162
- 制御装置が CPU、メモリ、I/O インターフェースを内蔵し、I/O インターフェースが外部機器へ信号を送る図を描いて
163
- センサ端末の構成図を描いて。検出部(温度センサと加速度センサ)、処理部(取得部と判定部)、通信部を備え、検出部→処理部→通信部の順にデータが流れ、通信部からゲートウェイへ無線送信する。日英併記で
164
- 制御部・駆動部・センサ部・対象装置から成る制御ループの図。指令で駆動し、センサが状態を検出して制御部へフィードバック
188
+ pdgkit で、制御ループの図を描いて。制御システムは制御部・駆動部・センサ部・対象装置から成り、制御部が指令で駆動部を動かし、駆動部が対象装置を駆動し、センサ部が状態を検出して制御部へフィードバックする。
165
189
  ```
166
190
 
167
- 処理・方法(フローチャートになりやすい):
191
+ **処理・方法(フローチャートになりやすい)** — 手順と分岐・ループを書く:
168
192
 
169
193
  ```text
170
- 画像取得→前処理→特徴抽出→欠陥あり?(Yes/No)→警告出力/正常記録→終了 のフローチャートにして
171
- ログイン処理のフローを描いて。入力→認証成功?で分岐、失敗ならリトライ、成功ならホームへ
194
+ pdgkit で、検査方法のフローチャートを描いて。画像取得→前処理→特徴抽出→欠陥あり? と進み、欠陥ありなら警告出力、無しなら正常記録、いずれも終了へ。
172
195
  ```
173
196
 
174
- 状態・やりとり:
175
-
176
197
  ```text
177
- 待機・動作中・エラーの状態遷移図。起動で動作中、停止で待機、異常でエラー、リセットで待機に戻る
178
- クライアントとサーバの、認証からリソース取得までのシーケンス図
198
+ pdgkit で、ログイン処理のフローチャートを描いて。入力→認証成功? で分岐し、成功ならホーム画面へ、失敗なら入力へ戻って再試行する。
179
199
  ```
180
200
 
181
- クレーム文からの変換:
201
+ **状態・やりとり:**
202
+
203
+ ```text
204
+ pdgkit で、装置の状態遷移図を描いて。待機・動作中・エラーの3状態があり、起動で動作中、停止で待機、異常でエラー、リセットで待機に戻る。
205
+ ```
182
206
 
183
207
  ```text
184
- 次の装置クレームを構成図にして: 撮像部と、前記撮像部が取得した画像を処理する画像処理部と、処理結果を表示する表示部と、を備える撮像装置。
185
- この方法クレームをフローチャートにして: 画像を取得するステップと、特徴量を抽出するステップと、欠陥の有無を判定するステップと、…を含む検査方法。
208
+ pdgkit で、クライアントとサーバの、認証からリソース取得までのシーケンス図を描いて。認証要求→トークン応答→リソース要求→リソース応答の順。
186
209
  ```
187
210
 
188
- 出力形式の指定(続けて頼める):
211
+ **クレーム文からの変換** — クレーム本文をそのまま貼って図種を指定する:
189
212
 
190
213
  ```text
191
- さっきの図を PDF にして(出願用)
192
- 編集できる PPTX にして
193
- PowerPoint のスライドにして
194
- PNG で出して
195
- 日英併記で出して
196
- 符号表(符号の説明)も Markdown で出して
214
+ pdgkit で、次の装置クレームを構成図にして。「撮像部と、前記撮像部が取得した画像を処理する画像処理部と、処理結果を表示する表示部と、を備える撮像装置。」
197
215
  ```
198
216
 
199
- > うまくいかないときは、AI に「`npx @shibayama/pdgkit guide` をもう一度読んで、`npx @shibayama/pdgkit validate` のエラーが無くなるまで直して」と伝えてください。PNG / PDF / PPTX は環境により失敗することがありますが、SVG は確実に出ます。
217
+ ```text
218
+ pdgkit で、次の方法クレームをフローチャートにして。「画像を取得するステップと、前記画像から特徴量を抽出するステップと、欠陥の有無を判定するステップと、欠陥が有る場合に警告を出力するステップと、を含む検査方法。」
219
+ ```
220
+
221
+ **出力形式を指定したいとき** — 続けて一言頼むだけです(指定しなければ SVG):
222
+
223
+ - 出願用の PDF にする → 「さっきの図を PDF にして」
224
+ - 編集できる PowerPoint にする → 「編集できる PPTX にして」
225
+ - 画像(PNG)にする → 「PNG で出して」
226
+ - 日英併記にする → 「日英併記で出して」
227
+ - 符号表(符号の説明)も出す → 「符号表も Markdown で出して」
228
+
229
+ > **AI が自分で図を描いてしまう**ときは、上の「明示手順プロンプト」に切り替えてください。図がうまく描けないときは、AI に「`npx @shibayama/pdgkit guide` をもう一度読んで、`npx @shibayama/pdgkit validate` のエラーが無くなるまで直して」と伝えます。PNG / PDF / PPTX は環境により失敗することがありますが、SVG は確実に出ます。
200
230
 
201
231
  ### ライブラリとして(Node / TypeScript)
202
232
 
@@ -254,26 +284,9 @@ pdgkit samples
254
284
 
255
285
  #### A. コードを実行できるチャット(推奨)
256
286
 
257
- ChatGPT のコードインタープリタは、2026 年初頭以降 Node.js と `npm install` に対応しています(一般的なネット接続はオフですが、パッケージのインストールは可能です)。このようなチャットでは、pdgkit を導入して図の生成・ダウンロードまでチャット内で完結できます。**記法ガイドはパッケージに同梱されているので、プロンプトに貼り付ける必要はありません**(`npx @shibayama/pdgkit guide` で読めます)。**図種(ブロック図など)も指定不要で、AI が発明内容から選びます**。次のように依頼するだけです。
258
-
259
- ```text
260
- pdgkit を使って特許図面を描いてください。
287
+ ChatGPT のコードインタープリタは、2026 年初頭以降 Node.js と `npm install` に対応しています(一般的なネット接続はオフですが、パッケージのインストールは可能です)。このようなチャットでは、pdgkit を導入して図の生成・ダウンロードまでチャット内で完結できます。**記法ガイドはパッケージに同梱されているので、プロンプトに貼り付ける必要はありません**(`npx @shibayama/pdgkit guide` で読めます)。**図種(ブロック図など)も指定不要で、AI が発明内容から選びます**。
261
288
 
262
- 1. npm install @shibayama/pdgkit
263
- 2. npx @shibayama/pdgkit guide を実行し、表示された記法ガイドに従う
264
- 3. 下の発明について .pdg を書いて fig.pdg に保存する(図種は内容から最適なものを選ぶ。私が指定したらそれに従う)
265
- 4. npx @shibayama/pdgkit validate fig.pdg がエラーなしになるまで直す
266
- 5. npx @shibayama/pdgkit render fig.pdg -o fig.svg(PNG なら --to png、PDF なら --to pdf)で図を作り、
267
- ダウンロードできるようにする
268
-
269
- 発明:
270
- 撮像装置に関する。本装置は、被写体を撮像して画像信号を出力する撮像部と、
271
- 撮像部からの画像信号にノイズ低減とエッジ強調を行う画像処理部と、
272
- 画像処理部が処理した画像データを格納する記憶部と、
273
- 画像データを外部サーバへ送信する通信部と、これらを制御する制御部と、を備える。
274
- 画像は撮像部から画像処理部、画像処理部から記憶部の順に処理され、
275
- 記憶部の画像は制御部の指示で通信部を介して外部サーバへ送信される。
276
- ```
289
+ 依頼の仕方は、上の「[AI に自然文で頼む](#ai-に自然文で頼む)」の**明示手順プロンプト**をそのまま使ってください(「発明」だけ差し替え)。「図を描いて」とだけ頼むと AI が自前で図を生成してしまうことがあるため、pdgkit を使う手順を明示するこの形が確実です。
277
290
 
278
291
  注: PNG / JPEG / PDF / PPTX はネイティブ依存(`@resvg/resvg-js`)などを使うため、コンテナ環境に依存します。確実に動くのは依存ゼロの SVG です。Node が使える環境なら全形式が動きます。
279
292
 
@@ -500,12 +513,18 @@ command = "pdgkit-mcp"
500
513
 
501
514
  #### 3. 使う
502
515
 
503
- あとは普通に日本語で頼むだけです。例:
516
+ あとは普通に日本語で頼むだけです(1 つずつ送れます)。例:
517
+
518
+ ```text
519
+ 制御装置の構成図を描いて。制御装置は CPU、メモリ、I/O インターフェースを内蔵し、I/O インターフェースが外部機器へ信号を送る。
520
+ ```
521
+
522
+ ```text
523
+ さっきの図を PNG で見せて。
524
+ ```
504
525
 
505
526
  ```text
506
- 制御装置が CPU、メモリ、I/O インターフェースを内蔵し、I/O インターフェースが外部機器へ信号を送るブロック図を描いて
507
- さっきの図を PNG で見せて
508
- 編集できる PPTX にして
527
+ 編集できる PPTX にして。
509
528
  ```
510
529
 
511
530
  Claude は内容に応じて次のツールを使い分けます(あなたが意識する必要はありません)。
@@ -655,7 +674,7 @@ src/core 純粋・依存ゼロ(PatentDSL から移植、唯一の真実源)
655
674
 
656
675
  ## 本家との忠実性
657
676
 
658
- - [src/core](src/core) は PatentDSL のソースをそのまま移植している(唯一の変更は、コンテンツボックス計算で再利用するための `estimateTextWidth` の `export` 追加のみ)。
677
+ - [src/core](src/core) は PatentDSL のソースを基に移植している。本家からの変更は次の 2 点のみ: (1) コンテンツボックス計算で再利用するための `estimateTextWidth` の `export` 追加、(2) フローチャート・状態遷移図で前のランクへ戻る辺(ループ・リトライ)を側方のレーンへ回し、前進線との重なりを避けるレイアウト改良。
659
678
  - 本家のテスト(構文解析・レイアウト・レイアウト回帰・ラベル配置)を移植し、すべて合格している。
660
679
  - SVG の切り出し余白(3 単位)・表示寸法(長辺 1600px 以上)は本家の定数と一致する。
661
680
 
@@ -664,7 +683,7 @@ src/core 純粋・依存ゼロ(PatentDSL から移植、唯一の真実源)
664
683
  ```bash
665
684
  npm install
666
685
  npm run typecheck
667
- npm test # vitest(コア移植 + Node 層、計 126 テスト)
686
+ npm test # vitest(コア移植 + Node 層、計 137 テスト)
668
687
  npm run cli -- render --sample block -o /tmp/x.svg
669
688
  npm run mcp # MCP サーバを起動
670
689
  npm run build # tsup で dist/(ESM + CJS + 型定義)を生成
package/dist/core.cjs CHANGED
@@ -228,6 +228,8 @@ var THICK_ARROW_TERMINAL_CLEARANCE = 5.4;
228
228
  var VERTICAL_PORT_RATIO = 0.25;
229
229
  var PORT_STUB = 6;
230
230
  var MAX_ROUTE_LANES = 18;
231
+ var LOOP_LANE_GAP = 10;
232
+ var LOOP_LANE_STEP = 7;
231
233
  var EPS = 1e-3;
232
234
  function layout(doc) {
233
235
  switch (doc.kind) {
@@ -407,7 +409,6 @@ function layoutFlow(doc) {
407
409
  const positions = /* @__PURE__ */ new Map();
408
410
  const sortedRanks = [...byRank.keys()].sort((a, b) => a - b);
409
411
  let y = MARGIN;
410
- let maxX = 0;
411
412
  for (const r of sortedRanks) {
412
413
  const lane = byRank.get(r);
413
414
  const widths = lane.map((id) => shapeOf(id) === "diamond" ? NODE_W * 1.2 : NODE_W);
@@ -419,7 +420,6 @@ function layoutFlow(doc) {
419
420
  positions.set(lane[i], { x, y, w: widths[i], h: NODE_H });
420
421
  x += widths[i] + H_GAP;
421
422
  }
422
- if (x > maxX) maxX = x;
423
423
  y += NODE_H + V_GAP;
424
424
  }
425
425
  for (const id of ids) {
@@ -439,7 +439,8 @@ function layoutFlow(doc) {
439
439
  });
440
440
  }
441
441
  const edges = makeEdges(doc.edges, positions);
442
- return { nodes: placed, edges, width: maxX + MARGIN, height: y + MARGIN, kind: "flow" };
442
+ const { width, height } = flowExtent(placed, edges);
443
+ return { nodes: placed, edges, width, height, kind: "flow" };
443
444
  }
444
445
  function layoutState(doc) {
445
446
  const { byRank } = computeRanks(doc);
@@ -451,7 +452,6 @@ function layoutState(doc) {
451
452
  const positions = /* @__PURE__ */ new Map();
452
453
  const sortedRanks = [...byRank.keys()].sort((a, b) => a - b);
453
454
  let y = MARGIN;
454
- let maxX = 0;
455
455
  for (const r of sortedRanks) {
456
456
  const lane = byRank.get(r);
457
457
  const widths = lane.map((id) => shapeOf(id) === "circle" ? 6 : NODE_W);
@@ -463,7 +463,6 @@ function layoutState(doc) {
463
463
  positions.set(lane[i], { x, y: y + (NODE_H - heights[i]) / 2, w: widths[i], h: heights[i] });
464
464
  x += widths[i] + H_GAP;
465
465
  }
466
- if (x > maxX) maxX = x;
467
466
  y += NODE_H + V_GAP;
468
467
  }
469
468
  for (const id of doc.nodes.keys()) {
@@ -483,7 +482,8 @@ function layoutState(doc) {
483
482
  });
484
483
  }
485
484
  const edges = makeEdges(doc.edges, positions);
486
- return { nodes: placed, edges, width: maxX + MARGIN, height: y + MARGIN, kind: "state" };
485
+ const { width, height } = flowExtent(placed, edges);
486
+ return { nodes: placed, edges, width, height, kind: "state" };
487
487
  }
488
488
  function layoutSeq(doc) {
489
489
  const seen = /* @__PURE__ */ new Set();
@@ -602,21 +602,50 @@ function computeRanks(doc) {
602
602
  return { byRank };
603
603
  }
604
604
  function makeEdges(srcEdges, positions) {
605
- return srcEdges.map((e) => {
605
+ const boxes = [...positions.values()];
606
+ const rightLimit = boxes.length ? Math.max(...boxes.map((b) => b.x + b.w)) : MARGIN;
607
+ const feedback = srcEdges.map((e, index) => ({ index, a: positions.get(e.from), b: positions.get(e.to) })).filter((r) => !!r.a && !!r.b && r.b.y + r.b.h <= r.a.y + EPS).sort((p, q) => feedbackSpan(p.a, p.b) - feedbackSpan(q.a, q.b));
608
+ const laneOf = /* @__PURE__ */ new Map();
609
+ feedback.forEach((r, nest) => laneOf.set(r.index, nest));
610
+ return srcEdges.map((e, index) => {
606
611
  const a = positions.get(e.from);
607
612
  const b = positions.get(e.to);
608
613
  if (!a || !b) {
609
614
  return { from: e.from, to: e.to, points: [], label: e.label, op: e.op };
610
615
  }
611
- return {
612
- from: e.from,
613
- to: e.to,
614
- points: orthogonalRoute(a, b),
615
- label: e.label,
616
- op: e.op
617
- };
616
+ const nest = laneOf.get(index);
617
+ const points = nest === void 0 ? orthogonalRoute(a, b) : feedbackRoute(a, b, rightLimit + LOOP_LANE_GAP + nest * LOOP_LANE_STEP);
618
+ return { from: e.from, to: e.to, points, label: e.label, op: e.op };
618
619
  });
619
620
  }
621
+ function feedbackSpan(a, b) {
622
+ return a.y + a.h / 2 - (b.y + b.h / 2);
623
+ }
624
+ function feedbackRoute(a, b, laneX) {
625
+ const ay = a.y + a.h / 2;
626
+ const by = b.y + b.h / 2;
627
+ return [
628
+ [a.x + a.w, ay],
629
+ [laneX, ay],
630
+ [laneX, by],
631
+ [b.x + b.w, by]
632
+ ];
633
+ }
634
+ function flowExtent(placed, edges) {
635
+ let maxX = MARGIN;
636
+ let maxY = MARGIN;
637
+ for (const n of placed) {
638
+ if (n.x + n.w > maxX) maxX = n.x + n.w;
639
+ if (n.y + n.h > maxY) maxY = n.y + n.h;
640
+ }
641
+ for (const e of edges) {
642
+ for (const [x, y] of e.points) {
643
+ if (x > maxX) maxX = x;
644
+ if (y > maxY) maxY = y;
645
+ }
646
+ }
647
+ return { width: maxX + MARGIN, height: maxY + MARGIN };
648
+ }
620
649
  function hasLinearChildFlow(children, edges, childMap) {
621
650
  const childSet = new Set(children);
622
651
  const pairs = /* @__PURE__ */ new Set();
package/dist/core.js CHANGED
@@ -189,6 +189,8 @@ var THICK_ARROW_TERMINAL_CLEARANCE = 5.4;
189
189
  var VERTICAL_PORT_RATIO = 0.25;
190
190
  var PORT_STUB = 6;
191
191
  var MAX_ROUTE_LANES = 18;
192
+ var LOOP_LANE_GAP = 10;
193
+ var LOOP_LANE_STEP = 7;
192
194
  var EPS = 1e-3;
193
195
  function layout(doc) {
194
196
  switch (doc.kind) {
@@ -368,7 +370,6 @@ function layoutFlow(doc) {
368
370
  const positions = /* @__PURE__ */ new Map();
369
371
  const sortedRanks = [...byRank.keys()].sort((a, b) => a - b);
370
372
  let y = MARGIN;
371
- let maxX = 0;
372
373
  for (const r of sortedRanks) {
373
374
  const lane = byRank.get(r);
374
375
  const widths = lane.map((id) => shapeOf(id) === "diamond" ? NODE_W * 1.2 : NODE_W);
@@ -380,7 +381,6 @@ function layoutFlow(doc) {
380
381
  positions.set(lane[i], { x, y, w: widths[i], h: NODE_H });
381
382
  x += widths[i] + H_GAP;
382
383
  }
383
- if (x > maxX) maxX = x;
384
384
  y += NODE_H + V_GAP;
385
385
  }
386
386
  for (const id of ids) {
@@ -400,7 +400,8 @@ function layoutFlow(doc) {
400
400
  });
401
401
  }
402
402
  const edges = makeEdges(doc.edges, positions);
403
- return { nodes: placed, edges, width: maxX + MARGIN, height: y + MARGIN, kind: "flow" };
403
+ const { width, height } = flowExtent(placed, edges);
404
+ return { nodes: placed, edges, width, height, kind: "flow" };
404
405
  }
405
406
  function layoutState(doc) {
406
407
  const { byRank } = computeRanks(doc);
@@ -412,7 +413,6 @@ function layoutState(doc) {
412
413
  const positions = /* @__PURE__ */ new Map();
413
414
  const sortedRanks = [...byRank.keys()].sort((a, b) => a - b);
414
415
  let y = MARGIN;
415
- let maxX = 0;
416
416
  for (const r of sortedRanks) {
417
417
  const lane = byRank.get(r);
418
418
  const widths = lane.map((id) => shapeOf(id) === "circle" ? 6 : NODE_W);
@@ -424,7 +424,6 @@ function layoutState(doc) {
424
424
  positions.set(lane[i], { x, y: y + (NODE_H - heights[i]) / 2, w: widths[i], h: heights[i] });
425
425
  x += widths[i] + H_GAP;
426
426
  }
427
- if (x > maxX) maxX = x;
428
427
  y += NODE_H + V_GAP;
429
428
  }
430
429
  for (const id of doc.nodes.keys()) {
@@ -444,7 +443,8 @@ function layoutState(doc) {
444
443
  });
445
444
  }
446
445
  const edges = makeEdges(doc.edges, positions);
447
- return { nodes: placed, edges, width: maxX + MARGIN, height: y + MARGIN, kind: "state" };
446
+ const { width, height } = flowExtent(placed, edges);
447
+ return { nodes: placed, edges, width, height, kind: "state" };
448
448
  }
449
449
  function layoutSeq(doc) {
450
450
  const seen = /* @__PURE__ */ new Set();
@@ -563,21 +563,50 @@ function computeRanks(doc) {
563
563
  return { byRank };
564
564
  }
565
565
  function makeEdges(srcEdges, positions) {
566
- return srcEdges.map((e) => {
566
+ const boxes = [...positions.values()];
567
+ const rightLimit = boxes.length ? Math.max(...boxes.map((b) => b.x + b.w)) : MARGIN;
568
+ const feedback = srcEdges.map((e, index) => ({ index, a: positions.get(e.from), b: positions.get(e.to) })).filter((r) => !!r.a && !!r.b && r.b.y + r.b.h <= r.a.y + EPS).sort((p, q) => feedbackSpan(p.a, p.b) - feedbackSpan(q.a, q.b));
569
+ const laneOf = /* @__PURE__ */ new Map();
570
+ feedback.forEach((r, nest) => laneOf.set(r.index, nest));
571
+ return srcEdges.map((e, index) => {
567
572
  const a = positions.get(e.from);
568
573
  const b = positions.get(e.to);
569
574
  if (!a || !b) {
570
575
  return { from: e.from, to: e.to, points: [], label: e.label, op: e.op };
571
576
  }
572
- return {
573
- from: e.from,
574
- to: e.to,
575
- points: orthogonalRoute(a, b),
576
- label: e.label,
577
- op: e.op
578
- };
577
+ const nest = laneOf.get(index);
578
+ const points = nest === void 0 ? orthogonalRoute(a, b) : feedbackRoute(a, b, rightLimit + LOOP_LANE_GAP + nest * LOOP_LANE_STEP);
579
+ return { from: e.from, to: e.to, points, label: e.label, op: e.op };
579
580
  });
580
581
  }
582
+ function feedbackSpan(a, b) {
583
+ return a.y + a.h / 2 - (b.y + b.h / 2);
584
+ }
585
+ function feedbackRoute(a, b, laneX) {
586
+ const ay = a.y + a.h / 2;
587
+ const by = b.y + b.h / 2;
588
+ return [
589
+ [a.x + a.w, ay],
590
+ [laneX, ay],
591
+ [laneX, by],
592
+ [b.x + b.w, by]
593
+ ];
594
+ }
595
+ function flowExtent(placed, edges) {
596
+ let maxX = MARGIN;
597
+ let maxY = MARGIN;
598
+ for (const n of placed) {
599
+ if (n.x + n.w > maxX) maxX = n.x + n.w;
600
+ if (n.y + n.h > maxY) maxY = n.y + n.h;
601
+ }
602
+ for (const e of edges) {
603
+ for (const [x, y] of e.points) {
604
+ if (x > maxX) maxX = x;
605
+ if (y > maxY) maxY = y;
606
+ }
607
+ }
608
+ return { width: maxX + MARGIN, height: maxY + MARGIN };
609
+ }
581
610
  function hasLinearChildFlow(children, edges, childMap) {
582
611
  const childSet = new Set(children);
583
612
  const pairs = /* @__PURE__ */ new Set();
package/dist/index.cjs CHANGED
@@ -251,6 +251,8 @@ var THICK_ARROW_TERMINAL_CLEARANCE = 5.4;
251
251
  var VERTICAL_PORT_RATIO = 0.25;
252
252
  var PORT_STUB = 6;
253
253
  var MAX_ROUTE_LANES = 18;
254
+ var LOOP_LANE_GAP = 10;
255
+ var LOOP_LANE_STEP = 7;
254
256
  var EPS = 1e-3;
255
257
  function layout(doc) {
256
258
  switch (doc.kind) {
@@ -430,7 +432,6 @@ function layoutFlow(doc) {
430
432
  const positions = /* @__PURE__ */ new Map();
431
433
  const sortedRanks = [...byRank.keys()].sort((a, b) => a - b);
432
434
  let y = MARGIN;
433
- let maxX = 0;
434
435
  for (const r of sortedRanks) {
435
436
  const lane = byRank.get(r);
436
437
  const widths = lane.map((id) => shapeOf(id) === "diamond" ? NODE_W * 1.2 : NODE_W);
@@ -442,7 +443,6 @@ function layoutFlow(doc) {
442
443
  positions.set(lane[i], { x, y, w: widths[i], h: NODE_H });
443
444
  x += widths[i] + H_GAP;
444
445
  }
445
- if (x > maxX) maxX = x;
446
446
  y += NODE_H + V_GAP;
447
447
  }
448
448
  for (const id of ids) {
@@ -462,7 +462,8 @@ function layoutFlow(doc) {
462
462
  });
463
463
  }
464
464
  const edges = makeEdges(doc.edges, positions);
465
- return { nodes: placed, edges, width: maxX + MARGIN, height: y + MARGIN, kind: "flow" };
465
+ const { width, height } = flowExtent(placed, edges);
466
+ return { nodes: placed, edges, width, height, kind: "flow" };
466
467
  }
467
468
  function layoutState(doc) {
468
469
  const { byRank } = computeRanks(doc);
@@ -474,7 +475,6 @@ function layoutState(doc) {
474
475
  const positions = /* @__PURE__ */ new Map();
475
476
  const sortedRanks = [...byRank.keys()].sort((a, b) => a - b);
476
477
  let y = MARGIN;
477
- let maxX = 0;
478
478
  for (const r of sortedRanks) {
479
479
  const lane = byRank.get(r);
480
480
  const widths = lane.map((id) => shapeOf(id) === "circle" ? 6 : NODE_W);
@@ -486,7 +486,6 @@ function layoutState(doc) {
486
486
  positions.set(lane[i], { x, y: y + (NODE_H - heights[i]) / 2, w: widths[i], h: heights[i] });
487
487
  x += widths[i] + H_GAP;
488
488
  }
489
- if (x > maxX) maxX = x;
490
489
  y += NODE_H + V_GAP;
491
490
  }
492
491
  for (const id of doc.nodes.keys()) {
@@ -506,7 +505,8 @@ function layoutState(doc) {
506
505
  });
507
506
  }
508
507
  const edges = makeEdges(doc.edges, positions);
509
- return { nodes: placed, edges, width: maxX + MARGIN, height: y + MARGIN, kind: "state" };
508
+ const { width, height } = flowExtent(placed, edges);
509
+ return { nodes: placed, edges, width, height, kind: "state" };
510
510
  }
511
511
  function layoutSeq(doc) {
512
512
  const seen = /* @__PURE__ */ new Set();
@@ -625,21 +625,50 @@ function computeRanks(doc) {
625
625
  return { byRank };
626
626
  }
627
627
  function makeEdges(srcEdges, positions) {
628
- return srcEdges.map((e) => {
628
+ const boxes = [...positions.values()];
629
+ const rightLimit = boxes.length ? Math.max(...boxes.map((b) => b.x + b.w)) : MARGIN;
630
+ const feedback = srcEdges.map((e, index) => ({ index, a: positions.get(e.from), b: positions.get(e.to) })).filter((r) => !!r.a && !!r.b && r.b.y + r.b.h <= r.a.y + EPS).sort((p, q) => feedbackSpan(p.a, p.b) - feedbackSpan(q.a, q.b));
631
+ const laneOf = /* @__PURE__ */ new Map();
632
+ feedback.forEach((r, nest) => laneOf.set(r.index, nest));
633
+ return srcEdges.map((e, index) => {
629
634
  const a = positions.get(e.from);
630
635
  const b = positions.get(e.to);
631
636
  if (!a || !b) {
632
637
  return { from: e.from, to: e.to, points: [], label: e.label, op: e.op };
633
638
  }
634
- return {
635
- from: e.from,
636
- to: e.to,
637
- points: orthogonalRoute(a, b),
638
- label: e.label,
639
- op: e.op
640
- };
639
+ const nest = laneOf.get(index);
640
+ const points = nest === void 0 ? orthogonalRoute(a, b) : feedbackRoute(a, b, rightLimit + LOOP_LANE_GAP + nest * LOOP_LANE_STEP);
641
+ return { from: e.from, to: e.to, points, label: e.label, op: e.op };
641
642
  });
642
643
  }
644
+ function feedbackSpan(a, b) {
645
+ return a.y + a.h / 2 - (b.y + b.h / 2);
646
+ }
647
+ function feedbackRoute(a, b, laneX) {
648
+ const ay = a.y + a.h / 2;
649
+ const by = b.y + b.h / 2;
650
+ return [
651
+ [a.x + a.w, ay],
652
+ [laneX, ay],
653
+ [laneX, by],
654
+ [b.x + b.w, by]
655
+ ];
656
+ }
657
+ function flowExtent(placed, edges) {
658
+ let maxX = MARGIN;
659
+ let maxY = MARGIN;
660
+ for (const n of placed) {
661
+ if (n.x + n.w > maxX) maxX = n.x + n.w;
662
+ if (n.y + n.h > maxY) maxY = n.y + n.h;
663
+ }
664
+ for (const e of edges) {
665
+ for (const [x, y] of e.points) {
666
+ if (x > maxX) maxX = x;
667
+ if (y > maxY) maxY = y;
668
+ }
669
+ }
670
+ return { width: maxX + MARGIN, height: maxY + MARGIN };
671
+ }
643
672
  function hasLinearChildFlow(children, edges, childMap) {
644
673
  const childSet = new Set(children);
645
674
  const pairs = /* @__PURE__ */ new Set();
@@ -3500,7 +3529,7 @@ async function renderToPptx(source, opts = {}) {
3500
3529
  }
3501
3530
 
3502
3531
  // src/node/index.ts
3503
- var VERSION = "0.1.0";
3532
+ var VERSION = "0.1.2";
3504
3533
  function toSvgString(source, lang = "ja") {
3505
3534
  return renderToSvg(source, { lang }).svg;
3506
3535
  }