@knowvah/dot-engine 1.9.0 → 2.0.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.
Files changed (175) hide show
  1. package/README.md +232 -30
  2. package/dist/api/builder.d.ts +3 -0
  3. package/dist/api/builder.d.ts.map +1 -1
  4. package/dist/api/edge-ops.d.ts +7 -0
  5. package/dist/api/edge-ops.d.ts.map +1 -1
  6. package/dist/api/geometry.d.ts +5 -2
  7. package/dist/api/geometry.d.ts.map +1 -1
  8. package/dist/api.js +159 -31
  9. package/dist/api.js.map +3 -3
  10. package/dist/async/collect.d.ts +49 -0
  11. package/dist/async/collect.d.ts.map +1 -0
  12. package/dist/async/fonts.d.ts +20 -0
  13. package/dist/async/fonts.d.ts.map +1 -0
  14. package/dist/async/render-async.d.ts +91 -0
  15. package/dist/async/render-async.d.ts.map +1 -0
  16. package/dist/async/render-into.d.ts +38 -0
  17. package/dist/async/render-into.d.ts.map +1 -0
  18. package/dist/async/sanitize.d.ts +28 -0
  19. package/dist/async/sanitize.d.ts.map +1 -0
  20. package/dist/common/htmltable-types.d.ts +3 -3
  21. package/dist/common/htmltable-types.d.ts.map +1 -1
  22. package/dist/common/make-label.d.ts.map +1 -1
  23. package/dist/common/poly-shapes.d.ts.map +1 -1
  24. package/dist/common/textmeasure-factory.d.ts +2 -0
  25. package/dist/common/textmeasure-factory.d.ts.map +1 -1
  26. package/dist/common/utils-inputscale.d.ts +19 -0
  27. package/dist/common/utils-inputscale.d.ts.map +1 -0
  28. package/dist/errors.d.ts +61 -5
  29. package/dist/errors.d.ts.map +1 -1
  30. package/dist/gvc/context.d.ts +36 -4
  31. package/dist/gvc/context.d.ts.map +1 -1
  32. package/dist/gvc/device.d.ts +5 -2
  33. package/dist/gvc/device.d.ts.map +1 -1
  34. package/dist/gvc/image-resolver.d.ts +16 -16
  35. package/dist/gvc/image-resolver.d.ts.map +1 -1
  36. package/dist/gvc/job.d.ts +1 -9
  37. package/dist/gvc/job.d.ts.map +1 -1
  38. package/dist/gvc/usershape.d.ts +2 -12
  39. package/dist/gvc/usershape.d.ts.map +1 -1
  40. package/dist/index.d.ts +27 -8
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +7560 -5446
  43. package/dist/index.js.map +4 -4
  44. package/dist/label/index.d.ts.map +1 -1
  45. package/dist/label/node.d.ts +0 -6
  46. package/dist/label/node.d.ts.map +1 -1
  47. package/dist/label/rectangle.d.ts +1 -7
  48. package/dist/label/rectangle.d.ts.map +1 -1
  49. package/dist/layout/circo/circular.d.ts +8 -5
  50. package/dist/layout/circo/circular.d.ts.map +1 -1
  51. package/dist/layout/dot/pack-components.d.ts +0 -19
  52. package/dist/layout/dot/pack-components.d.ts.map +1 -1
  53. package/dist/layout/dot/position.d.ts +7 -2
  54. package/dist/layout/dot/position.d.ts.map +1 -1
  55. package/dist/layout/fdp/derive.d.ts.map +1 -1
  56. package/dist/layout/fdp/index.d.ts.map +1 -1
  57. package/dist/layout/fdp/init.d.ts.map +1 -1
  58. package/dist/layout/fdp/layout.d.ts.map +1 -1
  59. package/dist/layout/fdp/ports.d.ts +0 -10
  60. package/dist/layout/fdp/ports.d.ts.map +1 -1
  61. package/dist/layout/fdp/xlayout.d.ts +0 -16
  62. package/dist/layout/fdp/xlayout.d.ts.map +1 -1
  63. package/dist/layout/neato/adjust-info.d.ts +117 -0
  64. package/dist/layout/neato/adjust-info.d.ts.map +1 -0
  65. package/dist/layout/neato/cdt-surface.d.ts.map +1 -1
  66. package/dist/layout/neato/constraint-adjust.d.ts +40 -0
  67. package/dist/layout/neato/constraint-adjust.d.ts.map +1 -0
  68. package/dist/layout/neato/edge-len.d.ts +20 -0
  69. package/dist/layout/neato/edge-len.d.ts.map +1 -0
  70. package/dist/layout/neato/fdp-adjust.d.ts +32 -4
  71. package/dist/layout/neato/fdp-adjust.d.ts.map +1 -1
  72. package/dist/layout/neato/index.d.ts +9 -6
  73. package/dist/layout/neato/index.d.ts.map +1 -1
  74. package/dist/layout/neato/init.d.ts +4 -11
  75. package/dist/layout/neato/init.d.ts.map +1 -1
  76. package/dist/layout/neato/kk-paths.d.ts +50 -0
  77. package/dist/layout/neato/kk-paths.d.ts.map +1 -0
  78. package/dist/layout/neato/kk-solve.d.ts +14 -0
  79. package/dist/layout/neato/kk-solve.d.ts.map +1 -0
  80. package/dist/layout/neato/kk.d.ts +44 -0
  81. package/dist/layout/neato/kk.d.ts.map +1 -0
  82. package/dist/layout/neato/multispline-router.d.ts.map +1 -1
  83. package/dist/layout/neato/poly.d.ts +50 -0
  84. package/dist/layout/neato/poly.d.ts.map +1 -0
  85. package/dist/layout/neato/sgd-dijkstra.d.ts +20 -0
  86. package/dist/layout/neato/sgd-dijkstra.d.ts.map +1 -0
  87. package/dist/layout/neato/sgd.d.ts +14 -9
  88. package/dist/layout/neato/sgd.d.ts.map +1 -1
  89. package/dist/layout/neato/start.d.ts +51 -0
  90. package/dist/layout/neato/start.d.ts.map +1 -0
  91. package/dist/layout/neato/vpsc-adjust.d.ts +21 -0
  92. package/dist/layout/neato/vpsc-adjust.d.ts.map +1 -0
  93. package/dist/layout/sfdp/index.d.ts.map +1 -1
  94. package/dist/layout/sfdp/init.d.ts +0 -9
  95. package/dist/layout/sfdp/init.d.ts.map +1 -1
  96. package/dist/layout/sfdp/spring-driver.d.ts.map +1 -1
  97. package/dist/layout/twopi/circle.d.ts +0 -7
  98. package/dist/layout/twopi/circle.d.ts.map +1 -1
  99. package/dist/ortho/ortho-parallel.d.ts.map +1 -1
  100. package/dist/ortho/trap-query.d.ts.map +1 -1
  101. package/dist/parser/index.d.ts +9 -5
  102. package/dist/parser/index.d.ts.map +1 -1
  103. package/dist/render/index.d.ts +2 -0
  104. package/dist/render/index.d.ts.map +1 -1
  105. package/dist/render/public.d.ts +9 -3
  106. package/dist/render/public.d.ts.map +1 -1
  107. package/dist/render/xdot-public.d.ts +7 -2
  108. package/dist/render/xdot-public.d.ts.map +1 -1
  109. package/dist/render.js +7355 -5476
  110. package/dist/render.js.map +4 -4
  111. package/dist/util/xml.d.ts.map +1 -1
  112. package/dist/vpsc/Solver.d.ts +1 -0
  113. package/dist/vpsc/Solver.d.ts.map +1 -1
  114. package/package.json +1 -1
  115. package/src/api/builder.ts +73 -5
  116. package/src/api/edge-ops.ts +19 -0
  117. package/src/api/geometry.ts +22 -5
  118. package/src/async/collect.ts +204 -0
  119. package/src/async/fonts.ts +61 -0
  120. package/src/async/render-async.ts +205 -0
  121. package/src/async/render-into.ts +115 -0
  122. package/src/async/sanitize.ts +150 -0
  123. package/src/common/htmltable-types.ts +4 -5
  124. package/src/common/make-label.ts +10 -1
  125. package/src/common/poly-shapes.ts +4 -1
  126. package/src/common/textmeasure-factory.ts +11 -0
  127. package/src/common/utils-inputscale.ts +31 -0
  128. package/src/errors.ts +188 -5
  129. package/src/gvc/context.ts +96 -17
  130. package/src/gvc/device.ts +11 -6
  131. package/src/gvc/image-resolver.ts +25 -2
  132. package/src/gvc/job.ts +3 -1
  133. package/src/gvc/usershape.ts +6 -0
  134. package/src/index.ts +59 -43
  135. package/src/label/index.ts +6 -2
  136. package/src/label/node.ts +2 -1
  137. package/src/label/rectangle.ts +4 -2
  138. package/src/layout/circo/circular.ts +9 -6
  139. package/src/layout/dot/pack-components.ts +6 -2
  140. package/src/layout/dot/position.ts +21 -3
  141. package/src/layout/fdp/derive.ts +7 -6
  142. package/src/layout/fdp/index.ts +45 -5
  143. package/src/layout/fdp/init.ts +7 -6
  144. package/src/layout/fdp/layout.ts +2 -1
  145. package/src/layout/fdp/ports.ts +3 -2
  146. package/src/layout/fdp/xlayout.ts +3 -1
  147. package/src/layout/neato/adjust-info.ts +332 -0
  148. package/src/layout/neato/cdt-surface.ts +50 -30
  149. package/src/layout/neato/constraint-adjust.ts +466 -0
  150. package/src/layout/neato/edge-len.ts +36 -0
  151. package/src/layout/neato/fdp-adjust.ts +117 -12
  152. package/src/layout/neato/index.ts +36 -54
  153. package/src/layout/neato/init.ts +21 -36
  154. package/src/layout/neato/kk-paths.ts +146 -0
  155. package/src/layout/neato/kk-solve.ts +64 -0
  156. package/src/layout/neato/kk.ts +322 -0
  157. package/src/layout/neato/multispline-router.ts +4 -3
  158. package/src/layout/neato/poly.ts +493 -0
  159. package/src/layout/neato/sgd-dijkstra.ts +125 -0
  160. package/src/layout/neato/sgd.ts +63 -40
  161. package/src/layout/neato/start.ts +222 -0
  162. package/src/layout/neato/vpsc-adjust.ts +93 -0
  163. package/src/layout/sfdp/index.ts +4 -5
  164. package/src/layout/sfdp/init.ts +40 -4
  165. package/src/layout/sfdp/spring-driver.ts +30 -1
  166. package/src/layout/twopi/circle.ts +2 -1
  167. package/src/ortho/ortho-parallel.ts +2 -1
  168. package/src/ortho/trap-query.ts +2 -1
  169. package/src/parser/index.ts +11 -11
  170. package/src/render/index.ts +5 -0
  171. package/src/render/public.ts +24 -24
  172. package/src/render/svg.ts +1 -1
  173. package/src/render/xdot-public.ts +19 -18
  174. package/src/util/xml.ts +24 -30
  175. package/src/vpsc/Solver.ts +5 -3
package/src/index.ts CHANGED
@@ -10,46 +10,42 @@
10
10
  */
11
11
 
12
12
  import { parse } from './parser/index.js';
13
- import { RenderError } from './errors.js';
13
+ import {
14
+ InternalError,
15
+ invalidArgType,
16
+ isGvError,
17
+ isUsageError,
18
+ messageOf,
19
+ rethrowAtBoundary,
20
+ } from './errors.js';
14
21
  import type { GvError, RenderResult } from './errors.js';
15
22
  import type { EngineName } from './gvc/context.js';
16
23
  import { render as deviceRender } from './gvc/device.js';
17
24
  import { createDefaultContext } from './gvc/default-context.js';
18
25
 
19
- /**
20
- * Duck-type a thrown value as a {@link GvError}: an object carrying a string
21
- * `type` and a string `code`. Covers `ParseError`, `HtmlParseError`, and
22
- * `RenderError` without per-subclass `instanceof`.
23
- */
24
- function isGvErrorLike(err: unknown): err is GvError {
25
- return (
26
- typeof err === 'object' &&
27
- err !== null &&
28
- typeof (err as { type?: unknown }).type === 'string' &&
29
- typeof (err as { code?: unknown }).code === 'string'
30
- );
31
- }
32
26
 
33
- /* v8 ignore start -- defensive normalizers for non-Error / genuinely-unknown
34
- throws. Unreachable via the public API (renderSvg normalizes every throw to
35
- GvError-like; parse only throws ParseError) but mandated by ADR-3 and the
36
- render wrap. */
37
- function messageOf(err: unknown): string {
38
- return err instanceof Error ? err.message : String(err);
39
- }
40
- function renderErrorFromUnknown(err: unknown): RenderError {
41
- return new RenderError(messageOf(err), 'GENERIC_ERROR');
27
+
28
+ /** Reject non-string `dotSource` / `engine` before any work starts. */
29
+ function checkRenderArgs(dotSource: unknown, engine: unknown): void {
30
+ if (typeof dotSource !== 'string') {
31
+ throw invalidArgType('dotSource', 'string', dotSource);
32
+ }
33
+ if (typeof engine !== 'string') {
34
+ throw invalidArgType('engine', 'string', engine);
35
+ }
42
36
  }
43
- /* v8 ignore stop */
44
37
 
45
38
  /**
46
- * Normalize any thrown value to a plain, JSON-serializable {@link GvError}
47
- * (no stack). Structured throws are copied; unknown throws → `GENERIC_ERROR`.
39
+ * Normalize a thrown {@link GvError} to a plain, JSON-serializable data object
40
+ * (no stack, no `cause`). A value that is not a GvError is wrapped as an
41
+ * `InternalError` first.
48
42
  */
49
43
  function classifyError(err: unknown): GvError {
50
- /* v8 ignore next -- the unknown-throw fallback is unreachable via the public
51
- API (see helpers above); ADR-3 still mandates it. */
52
- const gv: GvError = isGvErrorLike(err) ? err : renderErrorFromUnknown(err);
44
+ /* v8 ignore next 3 -- renderSvg already normalizes every non-usage throw to
45
+ a GvError, so only a throw escaping its context setup could land here. */
46
+ const gv: GvError = isGvError(err)
47
+ ? err
48
+ : new InternalError(messageOf(err), { cause: err });
53
49
  const out: GvError = {
54
50
  type: gv.type, code: gv.code, message: gv.message, friendlyMessage: gv.friendlyMessage,
55
51
  };
@@ -61,9 +57,14 @@ function classifyError(err: unknown): GvError {
61
57
  /**
62
58
  * Render a DOT-language string to SVG using the specified layout engine.
63
59
  *
64
- * Always throws a value implementing {@link GvError}: parse failures throw
65
- * `ParseError`; layout/render failures surface as `RenderError`
66
- * (`RENDER_ERROR`).
60
+ * Throws a {@link DotEngineError} for any problem with the input: `ParseError`
61
+ * (`SYNTAX_*`, `EDGE_OP_*`, `GENERIC_ERROR` for nesting too deep) for invalid
62
+ * DOT, `RenderError`
63
+ * (`RENDER_ERROR`, `UNKNOWN_LAYOUT`, `UNSUPPORTED_FEATURE`) for layout/render
64
+ * failures, and `InternalError` (`INTERNAL_ERROR`, `cause` = original) for a
65
+ * dot-engine bug. Invalid arguments throw a `TypeError` carrying a `code`. A
66
+ * malformed HTML-like label does not throw: as in C (htmlparse.y YYABORT) the
67
+ * label renders empty.
67
68
  *
68
69
  * @remarks
69
70
  * Security: when `dotSource` is untrusted, treat the returned SVG as
@@ -78,15 +79,21 @@ function classifyError(err: unknown): GvError {
78
79
  * ('dot', 'neato', 'fdp', 'sfdp', 'circo', 'twopi',
79
80
  * 'osage', 'patchwork') or any custom-registered name
80
81
  * @returns SVG string
81
- * @throws ParseError if dotSource is not valid DOT
82
- * @throws RenderError if layout or rendering fails
82
+ * @throws TypeError `ERR_INVALID_ARG_TYPE` if `dotSource` or `engine` is not a string
83
+ * @throws TypeError `ERR_INVALID_ARG_VALUE` if `engine` names no registered engine
84
+ * @throws ParseError `SYNTAX_ERROR`, `SYNTAX_UNEXPECTED_EOF`,
85
+ * `EDGE_OP_DIRECTED_IN_UNDIRECTED`, `EDGE_OP_UNDIRECTED_IN_DIRECTED` if
86
+ * `dotSource` is not valid DOT
87
+ * @throws RenderError `RENDER_ERROR`, `UNKNOWN_LAYOUT` or `UNSUPPORTED_FEATURE`
88
+ * if layout or rendering fails
89
+ * @throws InternalError `INTERNAL_ERROR` on a dot-engine bug
83
90
  */
84
91
  export function renderSvg(dotSource: string, engine: EngineName): string {
92
+ checkRenderArgs(dotSource, engine);
85
93
  const ctx = createDefaultContext();
86
94
  try {
87
95
  // parse() is inside the try so any non-ParseError throw (e.g. a raw
88
- // RangeError from stack exhaustion) is still normalized to a GvError,
89
- // honoring the "always throws a value implementing GvError" contract.
96
+ // RangeError from stack exhaustion) is still normalized (InternalError).
90
97
  const g = parse(dotSource);
91
98
  ctx.layout(g, engine);
92
99
  const svg = deviceRender(ctx, g, 'svg');
@@ -94,37 +101,40 @@ export function renderSvg(dotSource: string, engine: EngineName): string {
94
101
  ctx.freeLayout(g, engine);
95
102
  return svg;
96
103
  } catch (err: unknown) {
97
- // A render-stage throw already implementing GvError (e.g. HtmlParseError)
98
- // is re-surfaced unchanged; only genuinely-unknown throws become RENDER_ERROR.
99
- /* v8 ignore next -- current engines don't throw a GvError-like value here */
100
- if (isGvErrorLike(err)) throw err;
101
- throw new RenderError(messageOf(err), 'RENDER_ERROR');
104
+ return rethrowAtBoundary(err);
102
105
  }
103
106
  }
104
107
 
105
108
  /**
106
109
  * Result-style render: returns `{ svg }` on success or `{ errors: [one] }` on
107
110
  * the first failure (svg XOR errors). Errors are plain JSON-serializable
108
- * {@link GvError} data objects.
111
+ * {@link GvError} data objects. Returns (never throws) for any DOT input and
112
+ * any failure of the graph itself; throws only for invalid arguments.
109
113
  *
110
114
  * @remarks
111
115
  * Security: same untrusted-input caveat as {@link renderSvg} — the returned
112
116
  * `svg` is attacker-controlled markup for untrusted `dotSource`; apply a CSP or
113
117
  * sanitize before embedding. See the README "Security" section.
118
+ *
119
+ * @throws TypeError `ERR_INVALID_ARG_TYPE` if `dotSource` or `engine` is not a string
120
+ * @throws TypeError `ERR_INVALID_ARG_VALUE` if `engine` names no registered engine
114
121
  */
115
122
  export function tryRenderSvg(dotSource: string, engine: EngineName): RenderResult {
123
+ checkRenderArgs(dotSource, engine);
116
124
  try {
117
125
  return { svg: renderSvg(dotSource, engine) };
118
126
  } catch (err: unknown) {
127
+ if (isUsageError(err)) throw err;
119
128
  return { errors: [classifyError(err)] };
120
129
  }
121
130
  }
122
131
 
123
132
  export { parse } from './parser/index.js';
124
133
  export { ParseError } from './parser/index.js';
125
- export { RenderError } from './errors.js';
134
+ export { DotEngineError, InternalError, RenderError, isGvError } from './errors.js';
126
135
  export type {
127
136
  GvError,
137
+ UsageErrorCode,
128
138
  GvErrorType,
129
139
  GvErrorCode,
130
140
  GvExpectation,
@@ -163,3 +173,9 @@ export { render as renderWithContext } from './gvc/device.js';
163
173
  // rather than `render`. See decisions.md ADR-5 and the decision journal.
164
174
  export * from './api/index.js';
165
175
  export * from './render/index.js';
176
+
177
+ // Async API (async-api ADR-7): renderAsync is re-exported via ./render above.
178
+ export { renderSvgAsync } from './async/render-async.js';
179
+ export type { AsyncSvgOptions, AsyncSvgResult } from './async/render-async.js';
180
+ export { renderSvgInto } from './async/render-into.js';
181
+ export type { RenderSvgIntoOptions, RenderSvgIntoResult } from './async/render-into.js';
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  import { type Rect, overlap, combineRect, NUMDIMS } from './rectangle.js';
10
+ import { InternalError } from '../errors.js';
10
11
  import {
11
12
  NODECARD,
12
13
  type Branch,
@@ -256,11 +257,14 @@ function rTreeInsert2(
256
257
  return 0; // should never happen
257
258
  }
258
259
 
259
- /** Validate rect: low side must be <= high side per dimension. */
260
+ /**
261
+ * Validate rect: low side must be <= high side per dimension.
262
+ * @see label/index.c:RTreeInsert (assert at index.c:178)
263
+ */
260
264
  function validateRect(r: Rect): void {
261
265
  for (let i = 0; i < NUMDIMS; i++) {
262
266
  if (r.boundary[i] > r.boundary[NUMDIMS + i]) {
263
- throw new Error('rTreeInsert: rect low > high');
267
+ throw new InternalError('rTreeInsert: rect low > high');
264
268
  }
265
269
  }
266
270
  }
package/src/label/node.ts CHANGED
@@ -7,6 +7,7 @@
7
7
  * @see label/index.h
8
8
  */
9
9
 
10
+ import { InternalError } from '../errors.js';
10
11
  import { type Rect, initRect, combineRect, rectArea, nullRect } from './rectangle.js';
11
12
 
12
13
  // C node.c uses uint64_t for bestIncr, bestArea, increase — subtraction wraps.
@@ -261,7 +262,7 @@ let _splitNodeImpl: (
261
262
  b: Branch,
262
263
  newNodeOut: { value: Node | null },
263
264
  ) => void = () => {
264
- throw new Error('splitNode not yet registered — import split-q.ts first');
265
+ throw new InternalError('splitNode not yet registered — import split-q.ts first');
265
266
  };
266
267
 
267
268
  /** Register the splitNode implementation. Called by split-q.ts at load time. */
@@ -7,6 +7,8 @@
7
7
  * @see label/index.h
8
8
  */
9
9
 
10
+ import { RenderError } from '../errors.js';
11
+
10
12
  /** @see label/index.h:NUMDIMS */
11
13
  export const NUMDIMS = 2;
12
14
 
@@ -71,7 +73,7 @@ export function nullRect(): Rect {
71
73
  * Returns 0 for undefined rects or zero-width dimensions.
72
74
  * Throws if area overflows Number.MAX_SAFE_INTEGER (browser-safe substitute
73
75
  * for C's graphviz_exit on UINT64_MAX overflow).
74
- * @see label/rectangle.c:RectArea
76
+ * @see label/rectangle.c:RectArea (agerrorf + graphviz_exit at rectangle.c:73)
75
77
  */
76
78
  export function rectArea(r: Rect): number {
77
79
  if (isUndefined(r)) return 0;
@@ -81,7 +83,7 @@ export function rectArea(r: Rect): number {
81
83
  const dim = r.boundary[i + NUMDIMS] - r.boundary[i];
82
84
  if (dim === 0) return 0;
83
85
  if (Number.MAX_SAFE_INTEGER / dim < area) {
84
- throw new Error('label: area too large for rtree');
86
+ throw new RenderError('label: area too large for rtree', 'RENDER_ERROR');
85
87
  }
86
88
  area *= dim;
87
89
  }
@@ -159,14 +159,17 @@ export function copyPositions(_sg: SubGraph): void {
159
159
  // ---------------------------------------------------------------------------
160
160
 
161
161
  /**
162
- * Multi-component overlap adjustment stub. C runs adjustNodes(sg) on each
163
- * DERIVED component subgraph before packing (circularinit.c:212); porting
164
- * that requires width/pos plumbing on derived nodes and no divergent
165
- * corpus case reaches it yet. The single-component path (adjustNodesFull
166
- * on the real graph, circularinit.c:203) IS ported — see circoLayout.
162
+ * Per-component adjustNodes(sg). C calls it on each DERIVED component
163
+ * (circularinit.c:212), but the derived graph is a fresh root that never
164
+ * declares `overlap`, `normalize` or `scale`: agget(sg, "overlap") is NULL
165
+ * (AM_NONE) and removeOverlapWith only runs the no-op normalize/simpleScale.
166
+ * Verified against the native binary (T8, v2-silent-gaps): output is
167
+ * independent of those attributes, so this is a faithful no-op, not a gap.
168
+ * The single-component path (adjustNodesFull on the real graph,
169
+ * circularinit.c:203) IS ported — see circoLayout.
167
170
  */
168
171
  export function adjustNodes(_sg: SubGraph): void {
169
- // deferred: see plans/xdot-conformance/decision-journal.md (PRISM entry)
172
+ // faithful no-op: the derived graph carries no overlap/normalize/scale attrs
170
173
  }
171
174
 
172
175
  // ---------------------------------------------------------------------------
@@ -20,6 +20,7 @@
20
20
  * @see lib/dotgen/dotinit.c:copyClusterInfo
21
21
  */
22
22
 
23
+ import { InternalError } from '../../errors.js';
23
24
  import type { Graph } from '../../model/graph.js';
24
25
  import type { Node } from '../../model/node.js';
25
26
  import type { Point, Box } from '../../model/geom.js';
@@ -253,10 +254,13 @@ export function dotLayoutComponent(sg: Graph, root: Graph): void {
253
254
  // @see lib/dotgen/dotinit.c:copyClusterInfo (412)
254
255
  // ---------------------------------------------------------------------------
255
256
 
256
- /** Map a cluster clone back to its original root cluster. C's `mapClust`. */
257
+ /**
258
+ * Map a cluster clone back to its original root cluster. C's `mapClust`.
259
+ * @see lib/pack/ccomps.c:mapClust (assert(op) at ccomps.c:366)
260
+ */
257
261
  function mapClust(clone: Graph, origOf: Map<Graph, Graph>): Graph {
258
262
  const orig = origOf.get(clone);
259
- if (orig === undefined) throw new Error(`mapClust: no original for cluster ${clone.name}`);
263
+ if (orig === undefined) throw new InternalError(`mapClust: no original for cluster ${clone.name}`);
260
264
  return orig;
261
265
  }
262
266
 
@@ -10,7 +10,11 @@ import type { Graph } from '../../model/graph.js';
10
10
  import type { Node } from '../../model/node.js';
11
11
  import type { Edge } from '../../model/edge.js';
12
12
  import { virtualNode } from './fastgr.js';
13
- import { SLACKNODE, LEAFSET } from './rank.js';
13
+ import { SLACKNODE, LEAFSET, scaleClamp } from './rank.js';
14
+ import { agGraphAttr } from '../../model/cgraph-ops.js';
15
+
16
+ /** @see C INT_MAX — default network simplex iteration cap */
17
+ const INT_MAX = 2147483647;
14
18
  import { rank } from './ns.js';
15
19
  import { markLowclusters } from './cluster.js';
16
20
  import {
@@ -103,8 +107,22 @@ export function connectGraph(g: Graph): void {
103
107
  // nsiter2 — @see lib/dotgen/position.c:nsiter2
104
108
  // ---------------------------------------------------------------------------
105
109
 
106
- /** @see lib/dotgen/position.c:nsiter2 — nslimit attribute not yet ported */
107
- export function nsiter2(_g: Graph): number { return 2147483647; }
110
+ /**
111
+ * C's agget(g, "nslimit") INHERITS the root default, so a graph whose own
112
+ * attrs lack the key falls back to the parse-time snapshot, then the root
113
+ * (the same chain rank1 uses for nslimit1).
114
+ * @see lib/dotgen/position.c:nsiter2
115
+ */
116
+ export function nsiter2(g: Graph): number {
117
+ const s = g.attrs.get('nslimit')
118
+ ?? g.graphDefaultsSnapshot?.get('nslimit')
119
+ ?? g.root.attrs.get('nslimit')
120
+ // Set only inside a subgraph: C declared it on the root with default "".
121
+ ?? agGraphAttr(g.root, 'nslimit');
122
+ if (s === undefined) return INT_MAX;
123
+ // atof: a non-numeric prefix parses as 0, not NaN.
124
+ return scaleClamp(g.nodes.size, parseFloat(s) || 0);
125
+ }
108
126
 
109
127
  // ---------------------------------------------------------------------------
110
128
  // make_leafslots / expand_leaves — @see lib/dotgen/position.c
@@ -22,6 +22,7 @@ import { Edge } from '../../model/edge.js';
22
22
  import type { Box } from '../../model/geom.js';
23
23
  import { doGraphLabel } from '../dot/graph-label.js';
24
24
  import { layoutMeasurer } from '../../common/nodeinit.js';
25
+ import { inputscaleDivisor } from '../../common/utils-inputscale.js';
25
26
  import {
26
27
  type Bport,
27
28
  gdata,
@@ -79,8 +80,7 @@ function copyAttr(g: Graph, dg: Graph, attr: string): void {
79
80
 
80
81
  /**
81
82
  * Use a cluster's "coords" attribute, if any, for the initial position
82
- * of its derived node. PSinputscale (inputscale attr) is not ported —
83
- * no supported input sets it.
83
+ * of its derived node, divided by PSinputscale when > 0.
84
84
  * @see lib/fdpgen/layout.c:chkPos
85
85
  */
86
86
  function chkPos(g: Graph, n: Node, infop: LayoutInfo, bbp: Box): void {
@@ -92,10 +92,11 @@ function chkPos(g: Graph, n: Node, infop: LayoutInfo, bbp: Box): void {
92
92
  }
93
93
  const parsed = scanCoords(p);
94
94
  if (parsed === null) return; // C warns "expected four doubles"
95
- bbp.ll.x = parsed.box.ll.x;
96
- bbp.ll.y = parsed.box.ll.y;
97
- bbp.ur.x = parsed.box.ur.x;
98
- bbp.ur.y = parsed.box.ur.y;
95
+ const div = inputscaleDivisor(infop.rootg); // layout.c:320-324
96
+ bbp.ll.x = parsed.box.ll.x / div;
97
+ bbp.ll.y = parsed.box.ll.y / div;
98
+ bbp.ur.x = parsed.box.ur.x / div;
99
+ bbp.ur.y = parsed.box.ur.y / div;
99
100
  dndata(n).pinned = parsed.pinned;
100
101
  }
101
102
 
@@ -14,8 +14,10 @@ import type { Graph } from '../../model/graph.js';
14
14
  import type { LayoutEngine } from '../../gvc/context.js';
15
15
  import { setEdgeTypeFromAttr } from '../dot/index.js';
16
16
  import {
17
- EDGETYPE_LINE, EDGETYPE_NONE, splineEdges, injectOraclePositions,
17
+ EDGETYPE_LINE, EDGETYPE_NONE, EDGETYPE_ORTHO, splineEdges, injectOraclePositions,
18
18
  } from '../neato/splines.js';
19
+ import { EDGETYPE_COMPOUND } from '../dot/splines.js';
20
+ import { RenderError } from '../../errors.js';
19
21
  import { neatoSetAspect } from '../neato/init.js';
20
22
  import { placeGraphLabel } from '../dot/position-bbox.js';
21
23
  import { gvPostprocess } from '../../common/postproc.js';
@@ -34,6 +36,10 @@ export { deriveGraph, type LayoutInfo } from './derive.js';
34
36
  export { fdpInitNodeEdge, fdpCleanup } from './init.js';
35
37
  export { fdpInitParams, fdpParms } from './tlayout-parms.js';
36
38
 
39
+ /** @see lib/fdpgen/layout.c:fdpSplines (agwarningf text) */
40
+ const CLUSTER_EDGE_WARNING =
41
+ 'splines and cluster edges not supported - using line segments';
42
+
37
43
  /**
38
44
  * Graph-level initialization: edge type, gdata, clusters, parameters,
39
45
  * nodes and edges. GD_ndim is fixed at 2 (the "dim" attribute's 3D+
@@ -54,13 +60,47 @@ export function fdpInitGraph(g: Graph): void {
54
60
  }
55
61
 
56
62
  /**
57
- * Route edges by the resolved edge type. Compound (cluster) edges and
58
- * the HAS_CLUST_EDGE warning path are not ported — no supported input
59
- * has cluster-endpoint edges.
63
+ * Route edges with edge type `et`: splineEdges reads the type from GD_flags,
64
+ * so the C local `et = EDGETYPE_LINE` is applied for the call and restored.
65
+ * @see lib/neatogen/neatosplines.c:spline_edges1
66
+ */
67
+ function splineEdges1(g: Graph, et: number): void {
68
+ const saved = g.info.flags;
69
+ g.info.flags = (saved & ~0xf) | et;
70
+ try {
71
+ splineEdges(g);
72
+ } finally {
73
+ g.info.flags = saved;
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Route edges by the resolved edge type. C's State/Nop globals are not
79
+ * modeled: State < GVSPLINES at the tail holds exactly when the
80
+ * `et > EDGETYPE_ORTHO` block did not route (types <= ORTHO, or the
81
+ * HAS_CLUST_EDGE fallback), and Nop = 2 is only set on the compoundEdges
82
+ * path, which throws here. So the tail is the `else` of the block.
60
83
  * @see lib/fdpgen/layout.c:fdpSplines
61
84
  */
62
85
  function fdpSplines(g: Graph): void {
63
- splineEdges(g); // spline_edges1(g, et) — straight lines + clipping
86
+ let et = g.info.flags & 0xf;
87
+ if (et > EDGETYPE_ORTHO) {
88
+ if (et === EDGETYPE_COMPOUND) {
89
+ // C: splineEdges(g, compoundEdges, EDGETYPE_SPLINE) — unported router.
90
+ throw new RenderError(
91
+ 'splines=compound: cluster-avoiding edge routing (compoundEdges) is not supported yet',
92
+ 'UNSUPPORTED_FEATURE',
93
+ );
94
+ }
95
+ if (g.info.n_cluster_edges) { // HAS_CLUST_EDGE(g)
96
+ console.warn(CLUSTER_EDGE_WARNING);
97
+ et = EDGETYPE_LINE;
98
+ } else {
99
+ splineEdges1(g, et);
100
+ return; // State == GVSPLINES
101
+ }
102
+ }
103
+ splineEdges1(g, et); // State < GVSPLINES
64
104
  }
65
105
 
66
106
  /**
@@ -17,6 +17,7 @@ import type { Graph } from '../../model/graph.js';
17
17
  import type { Node } from '../../model/node.js';
18
18
  import type { Edge } from '../../model/edge.js';
19
19
  import { commonInitNodeEdge, lateDouble, layoutMeasurer } from '../../common/nodeinit.js';
20
+ import { inputscaleDivisor } from '../../common/utils-inputscale.js';
20
21
  import { initEdgeLabels } from '../../common/edge-label-init.js';
21
22
  import type { TextMeasurer } from '../../common/textmeasure.js';
22
23
  import { fdpParms } from './tlayout-parms.js';
@@ -25,24 +26,24 @@ import { processClusterEdges } from './cluster-edges.js';
25
26
 
26
27
  /**
27
28
  * Read user-supplied positions from the pos attribute (P_SET; P_PIN
28
- * with a "!" suffix or pin=true). PSinputscale (inputscale) is not
29
- * ported — no supported input sets it.
29
+ * with a "!" suffix or pin=true), divided by PSinputscale when > 0.
30
30
  * @see lib/fdpgen/fdpinit.c:initialPositions
31
31
  */
32
32
  function initialPositions(g: Graph): void {
33
+ const div = inputscaleDivisor(g);
33
34
  for (const np of g.nodes.values()) {
34
35
  const p = np.attrs.get('pos');
35
36
  if (p === undefined || p === '') continue;
36
- applyPosAttr(np, p);
37
+ applyPosAttr(np, p, div);
37
38
  }
38
39
  }
39
40
 
40
41
  /** Parse one "x,y[!]" pos value onto np. @see fdpinit.c:47-66 */
41
- function applyPosAttr(np: Node, p: string): void {
42
+ function applyPosAttr(np: Node, p: string, div: number): void {
42
43
  const m = /^(-?[\d.]+),(-?[\d.]+)\s*(!)?/.exec(p);
43
44
  if (m === null) return; // C warns "expected two floats"
44
- np.info.pos![0] = parseFloat(m[1]!);
45
- np.info.pos![1] = parseFloat(m[2]!);
45
+ np.info.pos![0] = parseFloat(m[1]!) / div;
46
+ np.info.pos![1] = parseFloat(m[2]!) / div;
46
47
  const pinAttr = np.attrs.get('pin');
47
48
  const pinned = m[3] === '!' ||
48
49
  (pinAttr !== undefined && /^(true|yes|1)$/i.test(pinAttr));
@@ -16,6 +16,7 @@ import type { Box, Point } from '../../model/geom.js';
16
16
  import type { TextlabelT } from '../../common/types.js';
17
17
  import { lateInt, lateDouble } from '../../common/nodeinit.js';
18
18
  import type { PolygonT } from '../../common/types.js';
19
+ import { agGraphAttr } from '../../model/cgraph-ops.js';
19
20
  import { isACluster } from '../dot/rank.js';
20
21
  import { BOTTOM_IX, TOP_IX } from '../dot/position-aux.js';
21
22
  import {
@@ -60,7 +61,7 @@ export function initInfo(g: Graph): LayoutInfo {
60
61
  getPackInfo(g, PackMode.Node, CL_OFFSET / 2, pack);
61
62
  return {
62
63
  rootg: g,
63
- hasCoords: g.attrs.has('coords'),
64
+ hasCoords: agGraphAttr(g, 'coords') !== undefined,
64
65
  gid: 0,
65
66
  pack,
66
67
  };
@@ -11,6 +11,7 @@
11
11
  * (15.0.0)
12
12
  */
13
13
 
14
+ import { InternalError } from '../../errors.js';
14
15
  import type { Graph } from '../../model/graph.js';
15
16
  import type { Node } from '../../model/node.js';
16
17
  import type { Edge } from '../../model/edge.js';
@@ -78,7 +79,7 @@ export function getEdgeList(n: Node, g: Graph): Erec[] {
78
79
  erecs.push({ e, alpha: Math.atan2(dy, dx), dist2: dx * dx + dy * dy });
79
80
  }
80
81
  if (erecs.length !== deg) {
81
- throw new Error(`fdp getEdgeList: ${erecs.length} edges vs deg ${deg}`);
82
+ throw new InternalError(`fdp getEdgeList: ${erecs.length} edges vs deg ${deg}`);
82
83
  }
83
84
  erecs.sort(ecmp);
84
85
 
@@ -151,7 +152,7 @@ export function expandCluster(n: Node, cg: Graph): Graph {
151
152
  idx = genPorts(n, es[k]!, pp, idx, bnd);
152
153
  }
153
154
  if (idx !== sz) {
154
- throw new Error(`fdp expandCluster: ${idx} ports vs wdeg ${sz}`);
155
+ throw new InternalError(`fdp expandCluster: ${idx} ports vs wdeg ${sz}`);
155
156
  }
156
157
 
157
158
  gdata(sg).ports = pp;
@@ -17,6 +17,7 @@
17
17
  * @see lib/fdpgen/xlayout.c (15.0.0)
18
18
  */
19
19
 
20
+ import { RenderError } from '../../errors.js';
20
21
  import type { Graph } from '../../model/graph.js';
21
22
  import type { Node } from '../../model/node.js';
22
23
  import { sepFactor, type ExpandT } from '../neato/sep-factor.js';
@@ -333,9 +334,10 @@ function removeOverlapAs(g: Graph, flag: string): void {
333
334
  // Genuinely unported adjust algorithms: throw rather than silently leave
334
335
  // overlaps (no supported corpus input reaches these).
335
336
  if (UNPORTED_MODES.has(mode)) {
336
- throw new Error(
337
+ throw new RenderError(
337
338
  `fdp: removeOverlapAs mode "${mode}" reached for graph "${g.name}" — ` +
338
339
  'that adjust algorithm (voronoi/oscale/vpsc/ortho/ipsep) is not ported',
340
+ 'UNSUPPORTED_FEATURE',
339
341
  );
340
342
  }
341
343
  // AM_NONE ('', 'true', any boolean-true) → no overlap removal.