@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/dist/index.d.cts CHANGED
@@ -1,21 +1,16 @@
1
1
  import { Lang, DiagramKind, Diagnostic } from './core.cjs';
2
2
  export { Bilingual, Box, Containment, Doc, Edge, EdgeOp, LabelPlacement, LaidOut, LaidOutEdge, LaidOutNode, Node, OP_TABLE, PATTERN_LABEL, PATTERN_SOURCE, PatternId, RenderOptions, SAMPLES, SAMPLE_ORDER, SampleId, Shape, chooseLabelPlacement, estimateTextWidth, layout, parse, refsToCsv, refsToMarkdown, render, splitBilingual, stripComment } from './core.cjs';
3
3
 
4
- /**
5
- * Minimal, zero-dependency SVG DOM shim. Implements the few element operations the
6
- * renderer needs (createElementNS, setAttribute, appendChild, textContent) and
7
- * serializes the tree to XML. `withShimDocument()` scopes the shim around a render
8
- * call, so rendering also works in a real browser without altering the page's document.
9
- */
10
- /** A serializable SVG node. The shape `render()` actually produces and consumes. */
4
+ /** Minimal, zero-dependency SVG DOM shim: the element operations render() needs, plus XML serialization. */
5
+ /** A serializable SVG node. */
11
6
  interface SvgNode {
12
7
  readonly nodeType: 'element';
13
8
  tagName: string;
14
9
  namespaceURI: string;
15
- /** Attribute names in insertion order, mapped to string values. */
10
+ /** Attributes, in insertion order. */
16
11
  attrs: Map<string, string>;
17
12
  children: SvgNode[];
18
- /** Text content, or null when the element holds child elements instead. */
13
+ /** Text content, or null when the node has element children. */
19
14
  text: string | null;
20
15
  setAttribute(name: string, value: string): void;
21
16
  getAttribute(name: string): string | null;
@@ -24,24 +19,12 @@ interface SvgNode {
24
19
  set textContent(value: string);
25
20
  get textContent(): string;
26
21
  }
27
- /**
28
- * Install the shim as `globalThis.document`, idempotently, but only when no DOM
29
- * with `createElementNS` is already present. In a real browser this is a no-op,
30
- * so the same code path serves both environments.
31
- */
22
+ /** Install the shim as `globalThis.document`, unless a real DOM is already present. Idempotent. */
32
23
  declare function installDomShim(): void;
33
- /**
34
- * Serialize a shim element tree to an XML string. Produces self-closing tags for
35
- * empty elements and correctly escapes attribute values and text content.
36
- */
24
+ /** Serialize a shim element tree to an XML string (self-closing empty tags, escaped values). */
37
25
  declare function serializeSvg(node: SvgNode): string;
38
26
 
39
- /**
40
- * Compute the tight content bounding box of a rendered SVG tree analytically
41
- * (without `getBBox`), by walking the element tree and measuring each primitive;
42
- * text width reuses {@link estimateTextWidth}. Used to crop exports to the drawn
43
- * extent. Also provides raster and display dimension helpers.
44
- */
27
+ /** Tight content bounding box of a rendered SVG tree, computed analytically (no `getBBox`), reusing {@link estimateTextWidth} for text. Also provides raster/display dimension helpers. */
45
28
 
46
29
  interface ViewBox {
47
30
  minX: number;
@@ -49,26 +32,16 @@ interface ViewBox {
49
32
  width: number;
50
33
  height: number;
51
34
  }
52
- /**
53
- * Compute the tight content bounding box of a rendered SVG tree, padded by `bleed`
54
- * (default 3 units).
55
- */
35
+ /** Tight content bounding box of a rendered SVG tree, padded by `bleed` (default 3 units). */
56
36
  declare function computeContentBox(root: SvgNode, bleed?: number): ViewBox;
57
- /**
58
- * Display dimensions for a viewBox so the long side is at least `targetSide` px
59
- * (browser default: 1600). Mirrors `svgDisplayDimensionsForViewBox()`.
60
- */
37
+ /** Display dimensions for a viewBox so the long side is at least `targetSide` px (default 1600). */
61
38
  declare function svgDisplayDimensions(viewBox: Pick<ViewBox, 'width' | 'height'>, targetSide?: number): {
62
39
  width: number;
63
40
  height: number;
64
41
  scale: number;
65
42
  };
66
43
 
67
- /**
68
- * Headless SVG rendering: `.pdg` source -> standalone SVG string.
69
- * Crops to the real drawn extent (bleed 3) and sizes the document so the long side
70
- * is at least `targetSide` px (default 1600).
71
- */
44
+ /** Headless SVG rendering: `.pdg` source → standalone SVG string. */
72
45
 
73
46
  interface RenderToSvgOptions {
74
47
  /** Display language: 'ja' (default), 'en', or 'both' (bilingual two-line labels). */
@@ -100,16 +73,10 @@ interface RenderToSvgResult {
100
73
  declare function renderToSvg(source: string, opts?: RenderToSvgOptions): RenderToSvgResult;
101
74
 
102
75
  /**
103
- * Validation for AI authoring. Runs the core parser and adds a kind assertion and
104
- * friendly lints, returning all diagnostics.
105
- *
106
- * - Kind assertion: an optional `#! kind: block|flow|state|seq` directive (written
107
- * in a comment) is checked against the kind inferred from structure; a mismatch
108
- * is reported as an error. Guards against producing the wrong diagram type.
109
- * - Lints: clearer hints for chained connections (`A -> B -> C`) and operators
110
- * without surrounding spaces (`11-12`).
111
- *
112
- * Run on AI-generated `.pdg` and feed the diagnostics back until there are no errors.
76
+ * Validation for AI authoring: runs the core parser, checks the optional
77
+ * `#! kind: block|flow|state|seq` directive against the kind inferred from
78
+ * structure (mismatch = error), and adds lints for chained connections and
79
+ * unspaced operators. Feed the diagnostics back to the author until no errors remain.
113
80
  */
114
81
 
115
82
  interface ValidateResult {
@@ -135,11 +102,7 @@ interface ValidateResult {
135
102
  */
136
103
  declare function validate(source: string): ValidateResult;
137
104
 
138
- /**
139
- * Raster output (PNG / JPEG) via `@resvg/resvg-js`, with the bundled IPAex Gothic
140
- * font embedded and a white background. Default scale is 8x the content box (capped
141
- * to 24000 px/side and 1e8 px total). Native dependencies are imported lazily.
142
- */
105
+ /** Raster output (PNG / JPEG) via `@resvg/resvg-js` with the bundled IPAex Gothic font and a white background. Native dependencies are imported lazily. */
143
106
 
144
107
  interface RasterOptions {
145
108
  lang?: Lang;
@@ -153,12 +116,7 @@ declare function renderToPng(source: string, opts?: RasterOptions): Promise<Uint
153
116
  /** Render `.pdg` source to a JPEG (8× by default, white background). */
154
117
  declare function renderToJpeg(source: string, opts?: RasterOptions): Promise<Uint8Array>;
155
118
 
156
- /**
157
- * PDF output (A4, IPAex font embedded). Vector by default (jsPDF + svg2pdf.js under
158
- * jsdom, with analytic getBBox / getComputedTextLength shims), falling back to a
159
- * high-resolution raster PDF if the vector path fails. The figure is centered with
160
- * a 10 mm margin and the page orientation follows its aspect ratio.
161
- */
119
+ /** PDF output (A4, IPAex font embedded). Vector by default (jsPDF + svg2pdf.js under jsdom, with analytic getBBox/getComputedTextLength shims), falling back to a high-resolution raster PDF if the vector path fails. */
162
120
 
163
121
  interface PdfOptions {
164
122
  lang?: Lang;
@@ -172,14 +130,7 @@ interface PdfOptions {
172
130
  /** Render `.pdg` source to a PDF (A4, IPAex font, vector preferred). */
173
131
  declare function renderToPdf(source: string, opts?: PdfOptions): Promise<Uint8Array>;
174
132
 
175
- /**
176
- * PPTX output: `.pdg` → PowerPoint presentation (one 16:9 slide).
177
- *
178
- * - image mode (default): the figure is rasterized (resvg) and placed as a
179
- * centered picture — highest visual fidelity.
180
- * - editable mode (`editable: true`): every SVG primitive is converted to an
181
- * editable PowerPoint shape / connector / text box, for later tweaking.
182
- */
133
+ /** PPTX output: `.pdg` → PowerPoint (one 16:9 slide). Image mode (default) places a rasterized picture; editable mode converts each SVG primitive to an editable PowerPoint shape, connector, or text box. */
183
134
 
184
135
  interface PptxOptions {
185
136
  lang?: Lang;
@@ -193,22 +144,13 @@ interface PptxOptions {
193
144
  /** Render `.pdg` source to a PPTX (image by default, or editable shapes). */
194
145
  declare function renderToPptx(source: string, opts?: PptxOptions): Promise<Uint8Array>;
195
146
 
196
- /**
197
- * pdgkit Node API — the browser-free rendering surface a host tool calls.
198
- *
199
- * Implemented: SVG, PNG, JPEG, PDF, PPTX (image + editable), validation, and the
200
- * reference-sign table exporters. All run with no browser.
201
- */
147
+ /** pdgkit Node API — the browser-free rendering surface a host tool calls. */
202
148
 
203
149
  /** The pdgkit package version. Keep in sync with package.json. */
204
- declare const VERSION = "0.1.0";
150
+ declare const VERSION = "0.1.2";
205
151
  /** Convenience: render to SVG and return just the string. */
206
152
  declare function toSvgString(source: string, lang?: Lang): string;
207
- /**
208
- * Read the bundled AI authoring guide (docs/ai-authoring-guide.md) as a string.
209
- * Inject this into an LLM's system prompt so it can write valid `.pdg`, instead of
210
- * pasting the guide by hand.
211
- */
153
+ /** Read the bundled AI authoring guide (docs/ai-authoring-guide.md) as a string, for injecting into an LLM's system prompt. */
212
154
  declare function loadAuthoringGuide(): string;
213
155
 
214
156
  export { Diagnostic, DiagramKind, Lang, type PdfOptions, type PptxOptions, type RasterOptions, type RenderToSvgOptions, type RenderToSvgResult, type SvgNode, VERSION, type ValidateResult, type ViewBox, computeContentBox, installDomShim, loadAuthoringGuide, renderToJpeg, renderToPdf, renderToPng, renderToPptx, renderToSvg, serializeSvg, svgDisplayDimensions, toSvgString, validate };
package/dist/index.d.ts CHANGED
@@ -1,21 +1,16 @@
1
1
  import { Lang, DiagramKind, Diagnostic } from './core.js';
2
2
  export { Bilingual, Box, Containment, Doc, Edge, EdgeOp, LabelPlacement, LaidOut, LaidOutEdge, LaidOutNode, Node, OP_TABLE, PATTERN_LABEL, PATTERN_SOURCE, PatternId, RenderOptions, SAMPLES, SAMPLE_ORDER, SampleId, Shape, chooseLabelPlacement, estimateTextWidth, layout, parse, refsToCsv, refsToMarkdown, render, splitBilingual, stripComment } from './core.js';
3
3
 
4
- /**
5
- * Minimal, zero-dependency SVG DOM shim. Implements the few element operations the
6
- * renderer needs (createElementNS, setAttribute, appendChild, textContent) and
7
- * serializes the tree to XML. `withShimDocument()` scopes the shim around a render
8
- * call, so rendering also works in a real browser without altering the page's document.
9
- */
10
- /** A serializable SVG node. The shape `render()` actually produces and consumes. */
4
+ /** Minimal, zero-dependency SVG DOM shim: the element operations render() needs, plus XML serialization. */
5
+ /** A serializable SVG node. */
11
6
  interface SvgNode {
12
7
  readonly nodeType: 'element';
13
8
  tagName: string;
14
9
  namespaceURI: string;
15
- /** Attribute names in insertion order, mapped to string values. */
10
+ /** Attributes, in insertion order. */
16
11
  attrs: Map<string, string>;
17
12
  children: SvgNode[];
18
- /** Text content, or null when the element holds child elements instead. */
13
+ /** Text content, or null when the node has element children. */
19
14
  text: string | null;
20
15
  setAttribute(name: string, value: string): void;
21
16
  getAttribute(name: string): string | null;
@@ -24,24 +19,12 @@ interface SvgNode {
24
19
  set textContent(value: string);
25
20
  get textContent(): string;
26
21
  }
27
- /**
28
- * Install the shim as `globalThis.document`, idempotently, but only when no DOM
29
- * with `createElementNS` is already present. In a real browser this is a no-op,
30
- * so the same code path serves both environments.
31
- */
22
+ /** Install the shim as `globalThis.document`, unless a real DOM is already present. Idempotent. */
32
23
  declare function installDomShim(): void;
33
- /**
34
- * Serialize a shim element tree to an XML string. Produces self-closing tags for
35
- * empty elements and correctly escapes attribute values and text content.
36
- */
24
+ /** Serialize a shim element tree to an XML string (self-closing empty tags, escaped values). */
37
25
  declare function serializeSvg(node: SvgNode): string;
38
26
 
39
- /**
40
- * Compute the tight content bounding box of a rendered SVG tree analytically
41
- * (without `getBBox`), by walking the element tree and measuring each primitive;
42
- * text width reuses {@link estimateTextWidth}. Used to crop exports to the drawn
43
- * extent. Also provides raster and display dimension helpers.
44
- */
27
+ /** Tight content bounding box of a rendered SVG tree, computed analytically (no `getBBox`), reusing {@link estimateTextWidth} for text. Also provides raster/display dimension helpers. */
45
28
 
46
29
  interface ViewBox {
47
30
  minX: number;
@@ -49,26 +32,16 @@ interface ViewBox {
49
32
  width: number;
50
33
  height: number;
51
34
  }
52
- /**
53
- * Compute the tight content bounding box of a rendered SVG tree, padded by `bleed`
54
- * (default 3 units).
55
- */
35
+ /** Tight content bounding box of a rendered SVG tree, padded by `bleed` (default 3 units). */
56
36
  declare function computeContentBox(root: SvgNode, bleed?: number): ViewBox;
57
- /**
58
- * Display dimensions for a viewBox so the long side is at least `targetSide` px
59
- * (browser default: 1600). Mirrors `svgDisplayDimensionsForViewBox()`.
60
- */
37
+ /** Display dimensions for a viewBox so the long side is at least `targetSide` px (default 1600). */
61
38
  declare function svgDisplayDimensions(viewBox: Pick<ViewBox, 'width' | 'height'>, targetSide?: number): {
62
39
  width: number;
63
40
  height: number;
64
41
  scale: number;
65
42
  };
66
43
 
67
- /**
68
- * Headless SVG rendering: `.pdg` source -> standalone SVG string.
69
- * Crops to the real drawn extent (bleed 3) and sizes the document so the long side
70
- * is at least `targetSide` px (default 1600).
71
- */
44
+ /** Headless SVG rendering: `.pdg` source → standalone SVG string. */
72
45
 
73
46
  interface RenderToSvgOptions {
74
47
  /** Display language: 'ja' (default), 'en', or 'both' (bilingual two-line labels). */
@@ -100,16 +73,10 @@ interface RenderToSvgResult {
100
73
  declare function renderToSvg(source: string, opts?: RenderToSvgOptions): RenderToSvgResult;
101
74
 
102
75
  /**
103
- * Validation for AI authoring. Runs the core parser and adds a kind assertion and
104
- * friendly lints, returning all diagnostics.
105
- *
106
- * - Kind assertion: an optional `#! kind: block|flow|state|seq` directive (written
107
- * in a comment) is checked against the kind inferred from structure; a mismatch
108
- * is reported as an error. Guards against producing the wrong diagram type.
109
- * - Lints: clearer hints for chained connections (`A -> B -> C`) and operators
110
- * without surrounding spaces (`11-12`).
111
- *
112
- * Run on AI-generated `.pdg` and feed the diagnostics back until there are no errors.
76
+ * Validation for AI authoring: runs the core parser, checks the optional
77
+ * `#! kind: block|flow|state|seq` directive against the kind inferred from
78
+ * structure (mismatch = error), and adds lints for chained connections and
79
+ * unspaced operators. Feed the diagnostics back to the author until no errors remain.
113
80
  */
114
81
 
115
82
  interface ValidateResult {
@@ -135,11 +102,7 @@ interface ValidateResult {
135
102
  */
136
103
  declare function validate(source: string): ValidateResult;
137
104
 
138
- /**
139
- * Raster output (PNG / JPEG) via `@resvg/resvg-js`, with the bundled IPAex Gothic
140
- * font embedded and a white background. Default scale is 8x the content box (capped
141
- * to 24000 px/side and 1e8 px total). Native dependencies are imported lazily.
142
- */
105
+ /** Raster output (PNG / JPEG) via `@resvg/resvg-js` with the bundled IPAex Gothic font and a white background. Native dependencies are imported lazily. */
143
106
 
144
107
  interface RasterOptions {
145
108
  lang?: Lang;
@@ -153,12 +116,7 @@ declare function renderToPng(source: string, opts?: RasterOptions): Promise<Uint
153
116
  /** Render `.pdg` source to a JPEG (8× by default, white background). */
154
117
  declare function renderToJpeg(source: string, opts?: RasterOptions): Promise<Uint8Array>;
155
118
 
156
- /**
157
- * PDF output (A4, IPAex font embedded). Vector by default (jsPDF + svg2pdf.js under
158
- * jsdom, with analytic getBBox / getComputedTextLength shims), falling back to a
159
- * high-resolution raster PDF if the vector path fails. The figure is centered with
160
- * a 10 mm margin and the page orientation follows its aspect ratio.
161
- */
119
+ /** PDF output (A4, IPAex font embedded). Vector by default (jsPDF + svg2pdf.js under jsdom, with analytic getBBox/getComputedTextLength shims), falling back to a high-resolution raster PDF if the vector path fails. */
162
120
 
163
121
  interface PdfOptions {
164
122
  lang?: Lang;
@@ -172,14 +130,7 @@ interface PdfOptions {
172
130
  /** Render `.pdg` source to a PDF (A4, IPAex font, vector preferred). */
173
131
  declare function renderToPdf(source: string, opts?: PdfOptions): Promise<Uint8Array>;
174
132
 
175
- /**
176
- * PPTX output: `.pdg` → PowerPoint presentation (one 16:9 slide).
177
- *
178
- * - image mode (default): the figure is rasterized (resvg) and placed as a
179
- * centered picture — highest visual fidelity.
180
- * - editable mode (`editable: true`): every SVG primitive is converted to an
181
- * editable PowerPoint shape / connector / text box, for later tweaking.
182
- */
133
+ /** PPTX output: `.pdg` → PowerPoint (one 16:9 slide). Image mode (default) places a rasterized picture; editable mode converts each SVG primitive to an editable PowerPoint shape, connector, or text box. */
183
134
 
184
135
  interface PptxOptions {
185
136
  lang?: Lang;
@@ -193,22 +144,13 @@ interface PptxOptions {
193
144
  /** Render `.pdg` source to a PPTX (image by default, or editable shapes). */
194
145
  declare function renderToPptx(source: string, opts?: PptxOptions): Promise<Uint8Array>;
195
146
 
196
- /**
197
- * pdgkit Node API — the browser-free rendering surface a host tool calls.
198
- *
199
- * Implemented: SVG, PNG, JPEG, PDF, PPTX (image + editable), validation, and the
200
- * reference-sign table exporters. All run with no browser.
201
- */
147
+ /** pdgkit Node API — the browser-free rendering surface a host tool calls. */
202
148
 
203
149
  /** The pdgkit package version. Keep in sync with package.json. */
204
- declare const VERSION = "0.1.0";
150
+ declare const VERSION = "0.1.2";
205
151
  /** Convenience: render to SVG and return just the string. */
206
152
  declare function toSvgString(source: string, lang?: Lang): string;
207
- /**
208
- * Read the bundled AI authoring guide (docs/ai-authoring-guide.md) as a string.
209
- * Inject this into an LLM's system prompt so it can write valid `.pdg`, instead of
210
- * pasting the guide by hand.
211
- */
153
+ /** Read the bundled AI authoring guide (docs/ai-authoring-guide.md) as a string, for injecting into an LLM's system prompt. */
212
154
  declare function loadAuthoringGuide(): string;
213
155
 
214
156
  export { Diagnostic, DiagramKind, Lang, type PdfOptions, type PptxOptions, type RasterOptions, type RenderToSvgOptions, type RenderToSvgResult, type SvgNode, VERSION, type ValidateResult, type ViewBox, computeContentBox, installDomShim, loadAuthoringGuide, renderToJpeg, renderToPdf, renderToPng, renderToPptx, renderToSvg, serializeSvg, svgDisplayDimensions, toSvgString, validate };
package/dist/index.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();
@@ -3437,7 +3466,7 @@ async function renderToPptx(source, opts = {}) {
3437
3466
  }
3438
3467
 
3439
3468
  // src/node/index.ts
3440
- var VERSION = "0.1.0";
3469
+ var VERSION = "0.1.2";
3441
3470
  function toSvgString(source, lang = "ja") {
3442
3471
  return renderToSvg(source, { lang }).svg;
3443
3472
  }
@@ -320,6 +320,8 @@ var THICK_ARROW_TERMINAL_CLEARANCE = 5.4;
320
320
  var VERTICAL_PORT_RATIO = 0.25;
321
321
  var PORT_STUB = 6;
322
322
  var MAX_ROUTE_LANES = 18;
323
+ var LOOP_LANE_GAP = 10;
324
+ var LOOP_LANE_STEP = 7;
323
325
  var EPS = 1e-3;
324
326
  function layout(doc) {
325
327
  switch (doc.kind) {
@@ -499,7 +501,6 @@ function layoutFlow(doc) {
499
501
  const positions = /* @__PURE__ */ new Map();
500
502
  const sortedRanks = [...byRank.keys()].sort((a, b) => a - b);
501
503
  let y = MARGIN;
502
- let maxX = 0;
503
504
  for (const r of sortedRanks) {
504
505
  const lane = byRank.get(r);
505
506
  const widths = lane.map((id) => shapeOf(id) === "diamond" ? NODE_W * 1.2 : NODE_W);
@@ -511,7 +512,6 @@ function layoutFlow(doc) {
511
512
  positions.set(lane[i], { x, y, w: widths[i], h: NODE_H });
512
513
  x += widths[i] + H_GAP;
513
514
  }
514
- if (x > maxX) maxX = x;
515
515
  y += NODE_H + V_GAP;
516
516
  }
517
517
  for (const id of ids) {
@@ -531,7 +531,8 @@ function layoutFlow(doc) {
531
531
  });
532
532
  }
533
533
  const edges = makeEdges(doc.edges, positions);
534
- return { nodes: placed, edges, width: maxX + MARGIN, height: y + MARGIN, kind: "flow" };
534
+ const { width, height } = flowExtent(placed, edges);
535
+ return { nodes: placed, edges, width, height, kind: "flow" };
535
536
  }
536
537
  function layoutState(doc) {
537
538
  const { byRank } = computeRanks(doc);
@@ -543,7 +544,6 @@ function layoutState(doc) {
543
544
  const positions = /* @__PURE__ */ new Map();
544
545
  const sortedRanks = [...byRank.keys()].sort((a, b) => a - b);
545
546
  let y = MARGIN;
546
- let maxX = 0;
547
547
  for (const r of sortedRanks) {
548
548
  const lane = byRank.get(r);
549
549
  const widths = lane.map((id) => shapeOf(id) === "circle" ? 6 : NODE_W);
@@ -555,7 +555,6 @@ function layoutState(doc) {
555
555
  positions.set(lane[i], { x, y: y + (NODE_H - heights[i]) / 2, w: widths[i], h: heights[i] });
556
556
  x += widths[i] + H_GAP;
557
557
  }
558
- if (x > maxX) maxX = x;
559
558
  y += NODE_H + V_GAP;
560
559
  }
561
560
  for (const id of doc.nodes.keys()) {
@@ -575,7 +574,8 @@ function layoutState(doc) {
575
574
  });
576
575
  }
577
576
  const edges = makeEdges(doc.edges, positions);
578
- return { nodes: placed, edges, width: maxX + MARGIN, height: y + MARGIN, kind: "state" };
577
+ const { width, height } = flowExtent(placed, edges);
578
+ return { nodes: placed, edges, width, height, kind: "state" };
579
579
  }
580
580
  function layoutSeq(doc) {
581
581
  const seen = /* @__PURE__ */ new Set();
@@ -694,21 +694,50 @@ function computeRanks(doc) {
694
694
  return { byRank };
695
695
  }
696
696
  function makeEdges(srcEdges, positions) {
697
- return srcEdges.map((e) => {
697
+ const boxes = [...positions.values()];
698
+ const rightLimit = boxes.length ? Math.max(...boxes.map((b) => b.x + b.w)) : MARGIN;
699
+ 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));
700
+ const laneOf = /* @__PURE__ */ new Map();
701
+ feedback.forEach((r, nest) => laneOf.set(r.index, nest));
702
+ return srcEdges.map((e, index) => {
698
703
  const a = positions.get(e.from);
699
704
  const b = positions.get(e.to);
700
705
  if (!a || !b) {
701
706
  return { from: e.from, to: e.to, points: [], label: e.label, op: e.op };
702
707
  }
703
- return {
704
- from: e.from,
705
- to: e.to,
706
- points: orthogonalRoute(a, b),
707
- label: e.label,
708
- op: e.op
709
- };
708
+ const nest = laneOf.get(index);
709
+ const points = nest === void 0 ? orthogonalRoute(a, b) : feedbackRoute(a, b, rightLimit + LOOP_LANE_GAP + nest * LOOP_LANE_STEP);
710
+ return { from: e.from, to: e.to, points, label: e.label, op: e.op };
710
711
  });
711
712
  }
713
+ function feedbackSpan(a, b) {
714
+ return a.y + a.h / 2 - (b.y + b.h / 2);
715
+ }
716
+ function feedbackRoute(a, b, laneX) {
717
+ const ay = a.y + a.h / 2;
718
+ const by = b.y + b.h / 2;
719
+ return [
720
+ [a.x + a.w, ay],
721
+ [laneX, ay],
722
+ [laneX, by],
723
+ [b.x + b.w, by]
724
+ ];
725
+ }
726
+ function flowExtent(placed, edges) {
727
+ let maxX = MARGIN;
728
+ let maxY = MARGIN;
729
+ for (const n of placed) {
730
+ if (n.x + n.w > maxX) maxX = n.x + n.w;
731
+ if (n.y + n.h > maxY) maxY = n.y + n.h;
732
+ }
733
+ for (const e of edges) {
734
+ for (const [x, y] of e.points) {
735
+ if (x > maxX) maxX = x;
736
+ if (y > maxY) maxY = y;
737
+ }
738
+ }
739
+ return { width: maxX + MARGIN, height: maxY + MARGIN };
740
+ }
712
741
  function hasLinearChildFlow(children, edges, childMap) {
713
742
  const childSet = new Set(children);
714
743
  const pairs = /* @__PURE__ */ new Set();
@@ -3131,7 +3160,7 @@ var langSchema = import_zod.z.enum(["ja", "en", "both"]).default("ja");
3131
3160
  function textResult(text) {
3132
3161
  return { content: [{ type: "text", text }] };
3133
3162
  }
3134
- function buildServer(version = "0.1.0") {
3163
+ function buildServer(version = "0.1.2") {
3135
3164
  const server = new import_mcp.McpServer({ name: "pdgkit", version });
3136
3165
  server.registerTool(
3137
3166
  "pdg_validate",
@@ -3205,7 +3234,7 @@ function buildServer(version = "0.1.0") {
3205
3234
 
3206
3235
  // src/node/index.ts
3207
3236
  var import_node_fs3 = require("fs");
3208
- var VERSION = "0.1.0";
3237
+ var VERSION = "0.1.2";
3209
3238
 
3210
3239
  // bin/pdgkit-mcp.ts
3211
3240
  async function main() {