@knowvah/dot-engine 2.6.11 → 2.7.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 (270) hide show
  1. package/README.md +1 -1
  2. package/dist/api.js +18 -4
  3. package/dist/api.js.map +3 -3
  4. package/dist/async/render-async.d.ts.map +1 -1
  5. package/dist/common/agerr.d.ts +37 -0
  6. package/dist/common/agerr.d.ts.map +1 -0
  7. package/dist/common/arrows-shapes-poly.d.ts.map +1 -1
  8. package/dist/common/color.d.ts.map +1 -1
  9. package/dist/common/crand.d.ts +0 -20
  10. package/dist/common/crand.d.ts.map +1 -1
  11. package/dist/common/emit-once.d.ts +6 -0
  12. package/dist/common/emit-once.d.ts.map +1 -0
  13. package/dist/common/graph-init.d.ts.map +1 -1
  14. package/dist/common/htmltable-attr-warn.d.ts.map +1 -1
  15. package/dist/common/htmltable-emit-fill.d.ts +29 -2
  16. package/dist/common/htmltable-emit-fill.d.ts.map +1 -1
  17. package/dist/common/htmltable-emit-rules.d.ts.map +1 -1
  18. package/dist/common/htmltable-emit.d.ts.map +1 -1
  19. package/dist/common/htmltable-lex-expat.d.ts +1 -6
  20. package/dist/common/htmltable-lex-expat.d.ts.map +1 -1
  21. package/dist/common/htmltable-pos-runs.d.ts +7 -4
  22. package/dist/common/htmltable-pos-runs.d.ts.map +1 -1
  23. package/dist/common/htmltable.d.ts +1 -7
  24. package/dist/common/htmltable.d.ts.map +1 -1
  25. package/dist/common/libm-trig.d.ts +5 -0
  26. package/dist/common/libm-trig.d.ts.map +1 -0
  27. package/dist/common/multicolor.d.ts +21 -13
  28. package/dist/common/multicolor.d.ts.map +1 -1
  29. package/dist/common/poly-gencode.d.ts +1 -2
  30. package/dist/common/poly-gencode.d.ts.map +1 -1
  31. package/dist/common/poly-init.d.ts.map +1 -1
  32. package/dist/common/postproc.d.ts.map +1 -1
  33. package/dist/common/random.d.ts.map +1 -1
  34. package/dist/common/render-state.d.ts +8 -0
  35. package/dist/common/render-state.d.ts.map +1 -0
  36. package/dist/common/splines-clip.d.ts +7 -0
  37. package/dist/common/splines-clip.d.ts.map +1 -1
  38. package/dist/common/splines-geom.d.ts +8 -0
  39. package/dist/common/splines-geom.d.ts.map +1 -1
  40. package/dist/common/splines-routespl.d.ts.map +1 -1
  41. package/dist/common/textmeasure-lookup.d.ts.map +1 -1
  42. package/dist/common/textmeasure.d.ts.map +1 -1
  43. package/dist/gvc/anchor.d.ts.map +1 -1
  44. package/dist/gvc/context.d.ts +6 -0
  45. package/dist/gvc/context.d.ts.map +1 -1
  46. package/dist/gvc/device-cluster.d.ts +15 -0
  47. package/dist/gvc/device-cluster.d.ts.map +1 -1
  48. package/dist/gvc/device-labels.d.ts +14 -0
  49. package/dist/gvc/device-labels.d.ts.map +1 -1
  50. package/dist/gvc/device.d.ts.map +1 -1
  51. package/dist/gvc/first-layer.d.ts +9 -0
  52. package/dist/gvc/first-layer.d.ts.map +1 -0
  53. package/dist/gvc/gvrender-set-style.d.ts +28 -0
  54. package/dist/gvc/gvrender-set-style.d.ts.map +1 -0
  55. package/dist/gvc/usershape.d.ts +18 -0
  56. package/dist/gvc/usershape.d.ts.map +1 -1
  57. package/dist/index.d.ts.map +1 -1
  58. package/dist/index.js +3169 -1296
  59. package/dist/index.js.map +4 -4
  60. package/dist/layout/dot/cluster.d.ts.map +1 -1
  61. package/dist/layout/dot/compound.d.ts.map +1 -1
  62. package/dist/layout/dot/conc.d.ts.map +1 -1
  63. package/dist/layout/dot/decomp.d.ts.map +1 -1
  64. package/dist/layout/dot/edge-route-clip.d.ts +32 -14
  65. package/dist/layout/dot/edge-route-clip.d.ts.map +1 -1
  66. package/dist/layout/dot/edge-route-routing.d.ts.map +1 -1
  67. package/dist/layout/dot/mincross-build.d.ts.map +1 -1
  68. package/dist/layout/dot/mincross-cross.d.ts.map +1 -1
  69. package/dist/layout/dot/rank.d.ts.map +1 -1
  70. package/dist/layout/dot/splines.d.ts.map +1 -1
  71. package/dist/layout/fdp/index.d.ts.map +1 -1
  72. package/dist/layout/fdp/tlayout-parms.d.ts.map +1 -1
  73. package/dist/layout/fdp/xlayout.d.ts.map +1 -1
  74. package/dist/layout/neato/__fixtures__/dijkstra-f.d.ts +11 -0
  75. package/dist/layout/neato/__fixtures__/dijkstra-f.d.ts.map +1 -0
  76. package/dist/layout/neato/__fixtures__/hier-imds.d.ts +36 -0
  77. package/dist/layout/neato/__fixtures__/hier-imds.d.ts.map +1 -0
  78. package/dist/layout/neato/__fixtures__/hier-majorization.d.ts +32 -0
  79. package/dist/layout/neato/__fixtures__/hier-majorization.d.ts.map +1 -0
  80. package/dist/layout/neato/cdt-surface.d.ts.map +1 -1
  81. package/dist/layout/neato/compute-hierarchy.d.ts +24 -0
  82. package/dist/layout/neato/compute-hierarchy.d.ts.map +1 -0
  83. package/dist/layout/neato/conjgrad-mkernel.d.ts +20 -0
  84. package/dist/layout/neato/conjgrad-mkernel.d.ts.map +1 -0
  85. package/dist/layout/neato/conjgrad.d.ts +19 -0
  86. package/dist/layout/neato/conjgrad.d.ts.map +1 -1
  87. package/dist/layout/neato/constrained-majorization.d.ts +26 -0
  88. package/dist/layout/neato/constrained-majorization.d.ts.map +1 -0
  89. package/dist/layout/neato/dijkstra.d.ts +33 -16
  90. package/dist/layout/neato/dijkstra.d.ts.map +1 -1
  91. package/dist/layout/neato/edge-len.d.ts.map +1 -1
  92. package/dist/layout/neato/fdp-adjust.d.ts.map +1 -1
  93. package/dist/layout/neato/gts-triangle.d.ts +14 -0
  94. package/dist/layout/neato/gts-triangle.d.ts.map +1 -0
  95. package/dist/layout/neato/init.d.ts +6 -22
  96. package/dist/layout/neato/init.d.ts.map +1 -1
  97. package/dist/layout/neato/kk.d.ts.map +1 -1
  98. package/dist/layout/neato/kkutils.d.ts +20 -0
  99. package/dist/layout/neato/kkutils.d.ts.map +1 -0
  100. package/dist/layout/neato/make-graph-data.d.ts +43 -0
  101. package/dist/layout/neato/make-graph-data.d.ts.map +1 -0
  102. package/dist/layout/neato/matrix-ops.d.ts +46 -0
  103. package/dist/layout/neato/matrix-ops.d.ts.map +1 -1
  104. package/dist/layout/neato/multispline-router.d.ts.map +1 -1
  105. package/dist/layout/neato/multispline.d.ts.map +1 -1
  106. package/dist/layout/neato/opt-arrangement.d.ts +20 -0
  107. package/dist/layout/neato/opt-arrangement.d.ts.map +1 -0
  108. package/dist/layout/neato/poly.d.ts.map +1 -1
  109. package/dist/layout/neato/quad-prog-solve.d.ts +44 -0
  110. package/dist/layout/neato/quad-prog-solve.d.ts.map +1 -0
  111. package/dist/layout/neato/sgd.d.ts.map +1 -1
  112. package/dist/layout/neato/smart-ini-x.d.ts +9 -0
  113. package/dist/layout/neato/smart-ini-x.d.ts.map +1 -0
  114. package/dist/layout/neato/splines.d.ts +1 -1
  115. package/dist/layout/neato/splines.d.ts.map +1 -1
  116. package/dist/layout/neato/start.d.ts +4 -1
  117. package/dist/layout/neato/start.d.ts.map +1 -1
  118. package/dist/layout/neato/stress.d.ts +8 -0
  119. package/dist/layout/neato/stress.d.ts.map +1 -1
  120. package/dist/model/edge.d.ts +1 -1
  121. package/dist/model/edge.d.ts.map +1 -1
  122. package/dist/model/lazy-attrs.d.ts +10 -0
  123. package/dist/model/lazy-attrs.d.ts.map +1 -0
  124. package/dist/model/node.d.ts +1 -1
  125. package/dist/model/node.d.ts.map +1 -1
  126. package/dist/ortho/index.d.ts.map +1 -1
  127. package/dist/parser/index.d.ts.map +1 -1
  128. package/dist/render/color-resolve.d.ts +49 -1
  129. package/dist/render/color-resolve.d.ts.map +1 -1
  130. package/dist/render/dot/edge-draw.d.ts +1 -1
  131. package/dist/render/dot/edge-draw.d.ts.map +1 -1
  132. package/dist/render/public.d.ts.map +1 -1
  133. package/dist/render/svg-edge-split.d.ts +2 -0
  134. package/dist/render/svg-edge-split.d.ts.map +1 -1
  135. package/dist/render/svg-multicolor.d.ts +2 -2
  136. package/dist/render/svg-multicolor.d.ts.map +1 -1
  137. package/dist/render/xdot-public.d.ts.map +1 -1
  138. package/dist/render.js +2799 -927
  139. package/dist/render.js.map +4 -4
  140. package/package.json +1 -1
  141. package/src/async/render-async.ts +6 -0
  142. package/src/common/agerr.ts +96 -0
  143. package/src/common/arrows-shapes-poly.ts +19 -5
  144. package/src/common/color.ts +38 -10
  145. package/src/common/crand.ts +8 -2
  146. package/src/common/emit-once.ts +27 -0
  147. package/src/common/graph-init.ts +2 -1
  148. package/src/common/htmltable-attr-warn.ts +3 -4
  149. package/src/common/htmltable-emit-fill.ts +104 -11
  150. package/src/common/htmltable-emit-rules.ts +2 -0
  151. package/src/common/htmltable-emit.ts +4 -3
  152. package/src/common/htmltable-lex-expat.ts +96 -23
  153. package/src/common/htmltable-pos-runs.ts +11 -6
  154. package/src/common/htmltable.ts +28 -117
  155. package/src/common/libm-trig.ts +43 -0
  156. package/src/common/multicolor.ts +88 -24
  157. package/src/common/poly-gencode.ts +30 -10
  158. package/src/common/poly-init.ts +20 -1
  159. package/src/common/postproc.ts +4 -1
  160. package/src/common/random.ts +11 -1
  161. package/src/common/render-state.ts +36 -0
  162. package/src/common/splines-clip.ts +15 -12
  163. package/src/common/splines-geom.ts +8 -4
  164. package/src/common/splines-routespl.ts +2 -1
  165. package/src/common/textmeasure-lookup.ts +9 -2
  166. package/src/common/textmeasure.ts +45 -2
  167. package/src/gvc/anchor.ts +1 -12
  168. package/src/gvc/context.ts +8 -0
  169. package/src/gvc/device-cluster.ts +93 -10
  170. package/src/gvc/device-labels.ts +27 -0
  171. package/src/gvc/device.ts +34 -8
  172. package/src/gvc/first-layer.ts +26 -0
  173. package/src/gvc/gvrender-set-style.ts +114 -0
  174. package/src/gvc/usershape.ts +26 -0
  175. package/src/index.ts +3 -0
  176. package/src/layout/dot/cluster.ts +2 -1
  177. package/src/layout/dot/compound.ts +3 -4
  178. package/src/layout/dot/conc.ts +6 -5
  179. package/src/layout/dot/decomp.ts +3 -0
  180. package/src/layout/dot/edge-route-clip.ts +46 -28
  181. package/src/layout/dot/edge-route-routing.ts +11 -3
  182. package/src/layout/dot/fastgr.ts +2 -2
  183. package/src/layout/dot/mincross-build.ts +2 -0
  184. package/src/layout/dot/mincross-cross.ts +2 -0
  185. package/src/layout/dot/rank.ts +2 -0
  186. package/src/layout/dot/splines.ts +2 -1
  187. package/src/layout/fdp/index.ts +2 -1
  188. package/src/layout/fdp/tlayout-parms.ts +9 -2
  189. package/src/layout/fdp/xlayout.ts +10 -2
  190. package/src/layout/neato/__fixtures__/dijkstra-f.ts +17 -0
  191. package/src/layout/neato/__fixtures__/hier/c1554.gv +33 -0
  192. package/src/layout/neato/__fixtures__/hier/c1554.plain +26 -0
  193. package/src/layout/neato/__fixtures__/hier/c1554.xdot +161 -0
  194. package/src/layout/neato/__fixtures__/hier/cases/cycle.gv +1 -0
  195. package/src/layout/neato/__fixtures__/hier/cases/cycle.plain +10 -0
  196. package/src/layout/neato/__fixtures__/hier/cases/cycle.stderr +0 -0
  197. package/src/layout/neato/__fixtures__/hier/cases/dirnone.gv +1 -0
  198. package/src/layout/neato/__fixtures__/hier/cases/dirnone.plain +11 -0
  199. package/src/layout/neato/__fixtures__/hier/cases/dirnone.stderr +0 -0
  200. package/src/layout/neato/__fixtures__/hier/cases/epsilon.gv +1 -0
  201. package/src/layout/neato/__fixtures__/hier/cases/epsilon.plain +13 -0
  202. package/src/layout/neato/__fixtures__/hier/cases/epsilon.stderr +0 -0
  203. package/src/layout/neato/__fixtures__/hier/cases/levelsgap.gv +1 -0
  204. package/src/layout/neato/__fixtures__/hier/cases/levelsgap.plain +8 -0
  205. package/src/layout/neato/__fixtures__/hier/cases/levelsgap.stderr +0 -0
  206. package/src/layout/neato/__fixtures__/hier/cases/mds.gv +1 -0
  207. package/src/layout/neato/__fixtures__/hier/cases/mds.plain +10 -0
  208. package/src/layout/neato/__fixtures__/hier/cases/mds.stderr +0 -0
  209. package/src/layout/neato/__fixtures__/hier/cases/multiedge.gv +1 -0
  210. package/src/layout/neato/__fixtures__/hier/cases/multiedge.plain +10 -0
  211. package/src/layout/neato/__fixtures__/hier/cases/multiedge.stderr +0 -0
  212. package/src/layout/neato/__fixtures__/hier/cases/neglevelsgap.gv +1 -0
  213. package/src/layout/neato/__fixtures__/hier/cases/neglevelsgap.plain +8 -0
  214. package/src/layout/neato/__fixtures__/hier/cases/neglevelsgap.stderr +0 -0
  215. package/src/layout/neato/__fixtures__/hier/cases/startrandom.gv +1 -0
  216. package/src/layout/neato/__fixtures__/hier/cases/startrandom.plain +10 -0
  217. package/src/layout/neato/__fixtures__/hier/cases/startrandom.stderr +0 -0
  218. package/src/layout/neato/__fixtures__/hier/cases/startregular.gv +1 -0
  219. package/src/layout/neato/__fixtures__/hier/cases/startregular.plain +10 -0
  220. package/src/layout/neato/__fixtures__/hier/cases/startregular.stderr +0 -0
  221. package/src/layout/neato/__fixtures__/hier/cases/tdist.gv +1 -0
  222. package/src/layout/neato/__fixtures__/hier/cases/tdist.plain +13 -0
  223. package/src/layout/neato/__fixtures__/hier/cases/tdist.stderr +0 -0
  224. package/src/layout/neato/__fixtures__/hier/cases/undirected.gv +1 -0
  225. package/src/layout/neato/__fixtures__/hier/cases/undirected.plain +10 -0
  226. package/src/layout/neato/__fixtures__/hier/cases/undirected.stderr +0 -0
  227. package/src/layout/neato/__fixtures__/hier/hier-probe.gv +1 -0
  228. package/src/layout/neato/__fixtures__/hier/hier-probe.plain +16 -0
  229. package/src/layout/neato/__fixtures__/hier/hier-probe.xdot +62 -0
  230. package/src/layout/neato/__fixtures__/hier-imds.ts +151 -0
  231. package/src/layout/neato/__fixtures__/hier-majorization.ts +145 -0
  232. package/src/layout/neato/cdt-surface.ts +12 -22
  233. package/src/layout/neato/compute-hierarchy.ts +90 -0
  234. package/src/layout/neato/conjgrad-mkernel.ts +141 -0
  235. package/src/layout/neato/conjgrad.ts +208 -0
  236. package/src/layout/neato/constrained-majorization.ts +479 -0
  237. package/src/layout/neato/dijkstra.ts +218 -92
  238. package/src/layout/neato/edge-len.ts +2 -1
  239. package/src/layout/neato/fdp-adjust.ts +3 -2
  240. package/src/layout/neato/gts-triangle.ts +42 -0
  241. package/src/layout/neato/init.ts +68 -68
  242. package/src/layout/neato/kk.ts +2 -1
  243. package/src/layout/neato/kkutils.ts +96 -0
  244. package/src/layout/neato/make-graph-data.ts +166 -0
  245. package/src/layout/neato/matrix-ops.ts +138 -0
  246. package/src/layout/neato/multispline-router.ts +3 -3
  247. package/src/layout/neato/multispline.ts +3 -2
  248. package/src/layout/neato/opt-arrangement.ts +86 -0
  249. package/src/layout/neato/poly.ts +3 -2
  250. package/src/layout/neato/quad-prog-solve.ts +489 -0
  251. package/src/layout/neato/sgd.ts +5 -2
  252. package/src/layout/neato/smart-ini-x.ts +503 -0
  253. package/src/layout/neato/splines.ts +4 -4
  254. package/src/layout/neato/start.ts +19 -14
  255. package/src/layout/neato/stress.ts +28 -0
  256. package/src/model/edge.ts +3 -2
  257. package/src/model/lazy-attrs.ts +36 -0
  258. package/src/model/node.ts +3 -2
  259. package/src/ortho/index.ts +2 -1
  260. package/src/parser/dot.js +4 -1
  261. package/src/parser/dot.pegjs +5 -1
  262. package/src/parser/index.ts +4 -3
  263. package/src/render/color-resolve.ts +193 -4
  264. package/src/render/dot/edge-draw.ts +6 -3
  265. package/src/render/dot.ts +1 -1
  266. package/src/render/public.ts +2 -0
  267. package/src/render/svg-edge-split.ts +6 -4
  268. package/src/render/svg-multicolor.ts +4 -3
  269. package/src/render/svg-parallel-edge.ts +2 -2
  270. package/src/render/xdot-public.ts +2 -0
@@ -6,14 +6,19 @@
6
6
 
7
7
  import type { Graph } from '../model/graph.js';
8
8
  import type { ObjState } from './job.js';
9
+ import type { RendererPlugin } from './context.js';
10
+ import { transformPoint } from './device-transform.js';
11
+ import { stripedBox } from '../render/svg-multicolor.js';
9
12
  import { RenderJob } from './job.js';
10
13
  import { FillType } from './context.js';
11
14
  import type { ResolvedFill, ClusterAttrs } from '../common/style-resolve.js';
12
15
  import {
13
16
  parseStyleFlags, resolvePenColor, resolvePenType, resolvePenWidth,
14
- resolveClusterFillEx,
17
+ resolveClusterFillEx, styleTokens,
15
18
  } from '../common/style-resolve.js';
16
- import { resolveRenderColor, withColorScheme } from '../render/color-resolve.js';
19
+ import { gvrenderSetStyle, checkClusterStyleTokens } from './gvrender-set-style.js';
20
+ import { resolveRenderColor, withColorScheme, reportColorNames } from '../render/color-resolve.js';
21
+ import { parseSegs } from '../common/multicolor.js';
17
22
 
18
23
  /**
19
24
  * Copy gradient fields from a linear/radial ResolvedFill onto obj.
@@ -99,16 +104,94 @@ export function applyClusterObjState(sg: Graph, job: RenderJob): boolean {
99
104
  const fillRes = resolveClusterFillEx(clusterAttrsOf(sg));
100
105
  if (job.obj === null) return false;
101
106
  const obj = job.obj;
107
+ // emit_clusters: gvrender_set_style(job, checkClusterStyle list) precedes the
108
+ // colour resolution. @see lib/common/emit.c:3808
109
+ gvrenderSetStyle(checkClusterStyleTokens(styleTokens(clusterStyle(sg))));
102
110
  // C wraps the cluster color block with setColorScheme. @see emit.c:3800/3943
103
111
  return withColorScheme(clusterAttr(sg, 'colorscheme'), () => {
112
+ // C resolves the fill (set_fillcolor / gradient vals) before the pen,
113
+ // which is set just before gvrender_box. @see emit.c:3846-3868 vs 3907-3913
114
+ const filled = applyClusterFill(obj, fillRes);
104
115
  applyClusterPenState(obj, sg);
105
- if (fillRes.kind === 'none') { obj.fill = FillType.None; return false; }
106
- if (fillRes.kind === 'solid') {
107
- obj.fill = FillType.Solid;
108
- obj.fillColor = resolveRenderColor(fillRes.color);
109
- return true;
110
- }
111
- applyClusterGradient(obj, fillRes);
112
- return true;
116
+ reportColorNames(stripedClusterColors(sg));
117
+ return filled;
113
118
  });
114
119
  }
120
+
121
+ /**
122
+ * True when emit_clusters takes the striped branch: `style=striped` and not
123
+ * `rounded` (the rounded branch is tested first).
124
+ * @see lib/common/emit.c:3877-3892 emit_clusters (rounded / striped / plain)
125
+ */
126
+ export function isStripedCluster(sg: Graph): boolean {
127
+ const flags = parseStyleFlags(clusterStyle(sg));
128
+ return flags.striped && !flags.rounded;
129
+ }
130
+
131
+ /**
132
+ * The fillcolor string emit_clusters hands to stripedBox.
133
+ * @see lib/common/emit.c:3820-3850 emit_clusters (fillcolor selection)
134
+ */
135
+ function stripedFillSpec(sg: Graph): string {
136
+ const flags = parseStyleFlags(clusterStyle(sg));
137
+ const color = clusterAttr(sg, 'color') ?? '';
138
+ let fill = clusterAttr(sg, 'fillcolor') || color;
139
+ const bg = clusterAttr(sg, 'bgcolor') ?? '';
140
+ if ((!flags.filled || fill === '') && bg !== '') fill = bg;
141
+ return fill === '' ? 'lightgrey' : fill;
142
+ }
143
+
144
+ /**
145
+ * The colours stripedBox sets for a `style=striped` cluster (not rounded): each
146
+ * segment of the fill spec with a positive width, after the pen.
147
+ * @see lib/common/emit.c:3896-3906 emit_clusters (striped branch)
148
+ * @see lib/common/emit.c:595 stripedBox
149
+ */
150
+ function stripedClusterColors(sg: Graph): string[] {
151
+ if (!isStripedCluster(sg)) return [];
152
+ const { segs, error } = parseSegs(stripedFillSpec(sg), () => `in cluster ${sg.name}`);
153
+ if (error === 1 || error === 2) return [];
154
+ return segs.flatMap((seg) => (seg.color !== null && seg.t > 0 ? [seg.color] : []));
155
+ }
156
+
157
+ /**
158
+ * Draw a `style=striped` cluster box: stripedBox over the bb corners, then the
159
+ * unfilled bb outline (pen already "transparent" for `peripheries=0`). The
160
+ * once-per-render Total-size warning is claimed by stripedClusterColors when
161
+ * the pen state is applied, so the parse here does not print twice.
162
+ * @see lib/common/emit.c:3892-3906 emit_clusters (striped branch)
163
+ */
164
+ export function renderStripedClusterBox(
165
+ sg: Graph, renderer: RendererPlugin, job: RenderJob,
166
+ ): void {
167
+ // stripedBox runs inside emit_clusters' setColorScheme window (3800/3943).
168
+ withColorScheme(clusterAttr(sg, 'colorscheme'), () => {
169
+ const { ll, ur } = sg.info.bb!;
170
+ const af = [
171
+ { x: ll.x, y: ll.y }, { x: ur.x, y: ll.y }, { x: ur.x, y: ur.y }, { x: ll.x, y: ur.y },
172
+ ].map((p) => transformPoint(p, job));
173
+ // stripedBox passes filled=1 (never GRADIENT) to gvrender_polygon; the port's
174
+ // renderers read the gradient from obj.fill, so show them a solid fill.
175
+ const obj = job.obj;
176
+ const savedFill = obj?.fill;
177
+ if (obj !== null && savedFill !== undefined) obj.fill = FillType.Solid;
178
+ stripedBox(job, af, stripedFillSpec(sg), false, renderer, () => `in cluster ${sg.name}`);
179
+ if (obj !== null && savedFill !== undefined) obj.fill = savedFill;
180
+ const rawPts = [
181
+ { x: ll.x, y: ll.y }, { x: ll.x, y: ur.y }, { x: ur.x, y: ur.y }, { x: ur.x, y: ll.y },
182
+ ];
183
+ renderer.polygon(rawPts.map((p) => transformPoint(p, job)), false, job);
184
+ });
185
+ }
186
+
187
+ /** The fill half of {@link applyClusterObjState}; true when the box is filled. */
188
+ function applyClusterFill(obj: ObjState, fillRes: ResolvedFill): boolean {
189
+ if (fillRes.kind === 'none') { obj.fill = FillType.None; return false; }
190
+ if (fillRes.kind === 'solid') {
191
+ obj.fill = FillType.Solid;
192
+ obj.fillColor = resolveRenderColor(fillRes.color);
193
+ return true;
194
+ }
195
+ applyClusterGradient(obj, fillRes);
196
+ return true;
197
+ }
@@ -10,6 +10,7 @@
10
10
 
11
11
  import type { Graph } from '../model/graph.js';
12
12
  import type { Node } from '../model/node.js';
13
+ import type { Edge } from '../model/edge.js';
13
14
  import type { RendererPlugin } from './context.js';
14
15
  import type { RenderJob } from './job.js';
15
16
  import type { TextlabelT } from '../common/types.js';
@@ -17,6 +18,8 @@ import type { TextSpan } from '../common/emit-types.js';
17
18
  import type { PlacedHtml } from '../common/htmltable-pos.js';
18
19
  import { gvrenderTextspan, withLabelEmitState } from './textspan-emit.js';
19
20
  import { emitHtmlLabel } from '../common/htmltable-emit.js';
21
+ import { reportColorNames } from '../render/color-resolve.js';
22
+ import { htmlLabelColors } from '../common/htmltable-emit-fill.js';
20
23
 
21
24
  /** valign codes stored on textlabel_t. @see lib/common/types.h:textlabel_t.valign */
22
25
  const VALIGN_TOP = 't'.charCodeAt(0);
@@ -38,6 +41,29 @@ function labelSpanX(lp: TextlabelT, just: 'l' | 'n' | 'r'): number {
38
41
  return lp.pos.x;
39
42
  }
40
43
 
44
+ /**
45
+ * The names emit_label hands to gvrender_resolve_color: an HTML label's walk,
46
+ * or a plain label's fontcolor (set once, before the spans, when there is a
47
+ * span).
48
+ * @see lib/common/labels.c:emit_label
49
+ */
50
+ export function labelColorNames(lp: TextlabelT): string[] {
51
+ if (lp.html) return lp.u.kind === 'html' ? htmlLabelColors(lp.u.html as PlacedHtml) : [];
52
+ return lp.u.kind === 'txt' && lp.u.nspans >= 1 ? [lp.fontcolor] : [];
53
+ }
54
+
55
+ /**
56
+ * Report the unknown colours of an edge's four labels in emit_end_edge order
57
+ * (label, xlabel, head, tail), each only when placed.
58
+ * @see lib/common/emit.c:emit_edge_label
59
+ */
60
+ export function reportEdgeLabelColors(e: Edge): void {
61
+ const labels = [e.info.label, e.info.xlabel, e.info.head_label, e.info.tail_label];
62
+ for (const lp of labels as Array<TextlabelT | undefined>) {
63
+ if (lp?.set) reportColorNames(labelColorNames(lp));
64
+ }
65
+ }
66
+
41
67
  /**
42
68
  * Emit one label's text spans if present and placed.
43
69
  * Shared by edge-label, node-xlabel, and graph-label slots.
@@ -59,6 +85,7 @@ export function renderOneLabel(
59
85
  // default pos. Callers mirroring the existence-only sites pass false.
60
86
  if (!lp) return;
61
87
  if (requireSet && !lp.set) return;
88
+ reportColorNames(labelColorNames(lp));
62
89
  // HTML branch: @see lib/common/labels.c:emit_label (226-230)
63
90
  // C routes to emit_html_label(job, lp->u.html, lp) using lp->pos as anchor.
64
91
  if (lp.html) {
package/src/gvc/device.ts CHANGED
@@ -26,6 +26,7 @@ import type { ShapeDesc, TextlabelT } from '../common/types.js';
26
26
  import { type LayerInfo, parseLayers } from '../common/layers.js';
27
27
  import { RenderJob, EMIT_CLUSTERS_LAST, createObjState, ObjType, EmitState } from './job.js';
28
28
  import { walkNodesAndEdges } from './emit-walk.js';
29
+ import { warnLayersUnsupported } from './first-layer.js';
29
30
  import { polyInit } from '../common/poly-init.js';
30
31
  import {
31
32
  setHtmlAnchorObj,
@@ -42,11 +43,15 @@ import {
42
43
  styleTokens,
43
44
  } from '../common/style-resolve.js';
44
45
  import { isPointNode } from '../common/poly-gencode.js';
45
- import { resolveRenderColor, withColorScheme } from '../render/color-resolve.js';
46
+ import {
47
+ resolveRenderColor, withColorScheme, reportEdgeColors, reportColorNames, backgroundColors,
48
+ setRendererKnownColors,
49
+ } from '../render/color-resolve.js';
46
50
  import { emitRoundedBezier } from '../common/poly-shapes.js';
47
- import { applyClusterObjState, clusterStyle, clusterPeripheries } from './device-cluster.js';
51
+ import { applyClusterObjState, clusterStyle, clusterPeripheries, isStripedCluster, renderStripedClusterBox } from './device-cluster.js';
48
52
  import { transformPoint } from './device-transform.js';
49
- import { renderOneLabel, renderNodeXLabel, renderClusterLabel } from './device-labels.js';
53
+ import { gvrenderSetStyle, edgeStyleResets } from './gvrender-set-style.js';
54
+ import { renderOneLabel, renderNodeXLabel, renderClusterLabel, reportEdgeLabelColors } from './device-labels.js';
50
55
  export { transformPoint, applyRotation, applyScale, buildPoint } from './device-transform.js';
51
56
  export { renderOneLabel, renderNodeXLabel, renderClusterLabel } from './device-labels.js';
52
57
  import { svgNodeId, svgEdgeId, svgClusterId, svgGraphId } from '../render/svg-id.js';
@@ -156,12 +161,15 @@ function setEdgePen(e: Edge, job: RenderJob): void {
156
161
  const obj = job.obj;
157
162
  const flags = parseStyleFlags(e.attrs.get('style'));
158
163
  obj.penColor = withColorScheme(e.attrs.get('colorscheme'),
159
- () => resolveRenderColor(resolvePenColor(e.attrs.get('color'))));
164
+ () => resolveRenderColor(resolvePenColor(e.attrs.get('color')), false));
160
165
  obj.pen = resolvePenType(flags);
161
166
  obj.penWidth = resolvePenWidth(flags, e.attrs.get('penwidth'));
162
167
  // Raw style tokens for xdot's `S` ops (e.g. explicit style="solid"); SVG
163
168
  // reads only obj.pen, so this is xdot-only. @see gvrender_core_dot.c xdot_style
164
169
  obj.rawStyle = styleTokens(e.attrs.get('style'));
170
+ // emit_begin_edge: `if (styles && ED_spl(e)) gvrender_set_style(job, styles)`
171
+ // @see lib/common/emit.c:2726
172
+ if (e.info.spl !== undefined) gvrenderSetStyle(obj.rawStyle);
165
173
  }
166
174
 
167
175
  export function renderEdge(e: Edge, renderer: RendererPlugin, job: RenderJob): void {
@@ -189,6 +197,14 @@ export function renderEdge(e: Edge, renderer: RendererPlugin, job: RenderJob): v
189
197
  // @see lib/common/emit.c:emit_edge (begin/end color context)
190
198
  withColorScheme(e.attrs.get('colorscheme'), () => {
191
199
  renderer.beginEdge(e, job);
200
+ // emit_edge_graphics, then emit_end_edge's labels: C resolves these names
201
+ // here whatever the renderer draws (the non-drawing ones included).
202
+ reportEdgeColors(e);
203
+ // arrow_gen resets the style; C sets the edge's styles again after each
204
+ // flagged bezier of a multi-bezier spline. @see emit.c:2047, :2677
205
+ const styles = styleTokens(e.attrs.get('style'));
206
+ for (let i = edgeStyleResets(e, styles); i > 0; i--) gvrenderSetStyle(styles);
207
+ reportEdgeLabelColors(e);
192
208
  renderer.endEdge(e, job);
193
209
  });
194
210
  } finally {
@@ -223,6 +239,10 @@ function renderClusterBox(
223
239
  sg: Graph, filled: boolean, renderer: RendererPlugin, job: RenderJob,
224
240
  ): void {
225
241
  const { ll, ur } = sg.info.bb!;
242
+ if (isStripedCluster(sg)) {
243
+ renderStripedClusterBox(sg, renderer, job);
244
+ return;
245
+ }
226
246
  if (parseStyleFlags(clusterStyle(sg)).rounded) {
227
247
  const af = [{ x: ll.x, y: ll.y }, { x: ur.x, y: ll.y }, { x: ur.x, y: ur.y }, { x: ll.x, y: ur.y }];
228
248
  emitRoundedBezier(af, { x: 0, y: 0 }, filled, { renderer, job });
@@ -273,7 +293,7 @@ function renderOneCluster(sg: Graph, renderer: RendererPlugin, job: RenderJob):
273
293
  // style=invis sets PEN_NONE and gvrender_polygon skips the whole box
274
294
  // (fill included); the group/title still emit.
275
295
  // @see lib/common/emit.c:3907-3917, lib/gvc/gvrender.c:543
276
- if ((clusterPeripheries(sg) !== 0 || filled) && job.obj?.pen !== PenType.None) {
296
+ if ((clusterPeripheries(sg) !== 0 || filled || isStripedCluster(sg)) && job.obj?.pen !== PenType.None) {
277
297
  renderClusterBox(sg, filled, renderer, job);
278
298
  }
279
299
  renderClusterLabel(sg, renderer, job);
@@ -327,6 +347,7 @@ export function renderGraph(g: Graph, job: RenderJob, renderer: RendererPlugin):
327
347
  job.numLayers = layered ? info.numLayers : 1;
328
348
  job.layerIDs = info.layerIDs;
329
349
  renderer.beginGraph(g, job);
350
+ warnLayersUnsupported(job.format, info.numLayers, renderer.beginLayer !== undefined);
330
351
  for (let ln = 1; ln <= job.numLayers; ln++) {
331
352
  job.layerNum = ln;
332
353
  if (layered) renderer.beginLayer!(info.layerIDs[ln]!, job);
@@ -351,9 +372,13 @@ function renderPage(g: Graph, renderer: RendererPlugin, job: RenderJob, info: La
351
372
  resolveObjAnchor(g, labelTextOf(g.info.label), svgGraphId(g, job), job.obj);
352
373
  }
353
374
  const anchored = beginAnchorIf(renderer, job);
354
- renderer.pageBackground?.(g, job);
355
- // emit root-graph label before clusters/nodes @see lib/common/emit.c:emit_page
356
- renderGraphLabel(g, renderer, job);
375
+ // emit_page opens the graph's colorscheme around the background and label.
376
+ withColorScheme(g.attrs.get('colorscheme'), () => {
377
+ reportColorNames(backgroundColors(g.attrs.get('bgcolor'), renderer.type === 'svg'));
378
+ renderer.pageBackground?.(g, job);
379
+ // emit root-graph label before clusters/nodes @see lib/common/emit.c:emit_page
380
+ renderGraphLabel(g, renderer, job);
381
+ });
357
382
  if (anchored) renderer.endAnchor?.(job);
358
383
  // emit_view: clusters are drawn BEFORE nodes/edges normally, but the map
359
384
  // device (EMIT_CLUSTERS_LAST) emits their <area> AFTER, so a node/edge area
@@ -459,6 +484,7 @@ export function render(ctx: GvcContext, g: Graph, format: string, inlineImages =
459
484
  // @see lib/common/emit.c:3260 (gvc->rotation=90); :3390 (job->rotation)
460
485
  job.rotation = parseLandscape(g) ? 90 : 0;
461
486
  job.renderer = renderer;
487
+ setRendererKnownColors(renderer.type === 'svg'); // svg_knowncolors; others have none
462
488
  resetHtmlAnchorIds(); // C: fresh anchorId per dot invocation
463
489
  for (const n of g.nodes.values()) polyInit(n, g, job.measurer);
464
490
  return renderGraph(g, job, renderer);
@@ -0,0 +1,26 @@
1
+ // SPDX-License-Identifier: EPL-2.0
2
+ /**
3
+ * The `GVDEVICE_DOES_LAYERS` check of `firstlayer`: a device without it prints
4
+ * one layer and warns. The one-layer effect is `renderGraph`'s `layered` flag
5
+ * (only svg has `beginLayer`; the other native layer-capable devices, ps/pov,
6
+ * are not ported); this file owns the warning.
7
+ */
8
+ import { agwarningf } from '../common/agerr.js';
9
+
10
+ /** Port format ids that spell a native `-T` name differently (`output_langname`). */
11
+ const NATIVE_LANGNAME: ReadonlyMap<string, string> = new Map([
12
+ ['cmapx-np', 'cmapx_np'],
13
+ ['imap-np', 'imap_np'],
14
+ ]);
15
+
16
+ /**
17
+ * Print `layers not supported in <output_langname> output` when the graph has
18
+ * more than one layer and the device cannot draw layers. Called after
19
+ * beginGraph, as emit_graph calls firstlayer after emit_begin_graph: for
20
+ * `-Tjson` the nested xdot render in json_begin_graph therefore warns first.
21
+ * @see lib/common/emit.c:firstlayer
22
+ */
23
+ export function warnLayersUnsupported(format: string, numLayers: number, doesLayers: boolean): void {
24
+ if (numLayers <= 1 || doesLayers) return;
25
+ agwarningf(`layers not supported in ${NATIVE_LANGNAME.get(format) ?? format} output`);
26
+ }
@@ -0,0 +1,114 @@
1
+ // SPDX-License-Identifier: EPL-2.0
2
+
3
+ /**
4
+ * The warning half of `gvrender_set_style`, plus the style-list filters its
5
+ * callers apply first. The pen/width/fill mapping of the same C function is
6
+ * not duplicated here: the port resolves it through style-resolve.ts
7
+ * (parseStyleFlags, resolvePenType, resolvePenWidth), which already mirrors
8
+ * the token cases below. Every caller in the port runs inside a render with a
9
+ * render engine (`if (gvre)` in C), so no engine test is needed here.
10
+ *
11
+ * @see lib/gvc/gvrender.c:480 gvrender_set_style
12
+ */
13
+
14
+ import { agwarningf } from '../common/agerr.js';
15
+ import { parseSegsSilent } from '../common/multicolor.js';
16
+ import type { PolygonT } from '../common/types.js';
17
+ import type { Edge } from '../model/edge.js';
18
+
19
+ /** Tokens gvrender_set_style maps to pen, pen width or fill state. */
20
+ const SUPPORTED_STYLES: ReadonlySet<string> = new Set([
21
+ 'solid', 'dashed', 'dotted', 'invis', 'invisible', 'bold', 'setlinewidth',
22
+ 'filled', 'unfilled', 'tapered',
23
+ ]);
24
+
25
+ /** `fmod(orientation, 90)` bound of shapes.c:isBox. @see lib/common/shapes.c:isBox */
26
+ const BOX_ORIENTATION_TOLERANCE = 0.5;
27
+ const BOX_ORIENTATION_STEP = 90;
28
+ const BOX_SIDES = 4;
29
+ /** polygon_t.sides bound of shapes.c:isEllipse. @see lib/common/shapes.c:isEllipse */
30
+ const ELLIPSE_MAX_SIDES = 2;
31
+
32
+ /** The name C compares: parse_style stores a token's `(args)` after its NUL. */
33
+ function styleName(token: string): string {
34
+ const paren = token.indexOf('(');
35
+ return paren < 0 ? token : token.slice(0, paren);
36
+ }
37
+
38
+ /**
39
+ * Warn for every token the render engine cannot map, on every call.
40
+ * @see lib/gvc/gvrender.c:480 gvrender_set_style
41
+ */
42
+ export function gvrenderSetStyle(styles: readonly string[] | null): void {
43
+ if (styles === null) return;
44
+ for (const token of styles) {
45
+ const name = styleName(token);
46
+ if (!SUPPORTED_STYLES.has(name)) {
47
+ agwarningf(`gvrender_set_style: unsupported style ${name} - ignoring`);
48
+ }
49
+ }
50
+ }
51
+
52
+ /** @see lib/common/shapes.c:isBox */
53
+ function isBox(p: PolygonT | null): boolean {
54
+ if (p === null) return false;
55
+ return p.sides === BOX_SIDES
56
+ && Math.abs(p.orientation % BOX_ORIENTATION_STEP) < BOX_ORIENTATION_TOLERANCE
57
+ && p.distortion === 0 && p.skew === 0;
58
+ }
59
+
60
+ /** @see lib/common/shapes.c:isEllipse */
61
+ function isEllipse(p: PolygonT | null): boolean {
62
+ return p !== null && p.sides <= ELLIPSE_MAX_SIDES;
63
+ }
64
+
65
+ /**
66
+ * The node style list passed to the renderer: checkStyle removes the tokens
67
+ * it turns into polygon flags. `shape` is `ND_shape(n)->polygon` (null for
68
+ * records).
69
+ * @see lib/common/shapes.c:465 checkStyle
70
+ */
71
+ export function checkStyleTokens(styles: readonly string[], shape: PolygonT | null): string[] {
72
+ return styles.filter((p) => {
73
+ if (p === 'rounded' || p === 'diagonals' || p === 'radial') return false;
74
+ if (p === 'striped') return !isBox(shape);
75
+ if (p === 'wedged') return !isEllipse(shape);
76
+ return true;
77
+ });
78
+ }
79
+
80
+ /**
81
+ * The cluster style list passed to the renderer.
82
+ * @see lib/common/emit.c:367 checkClusterStyle
83
+ */
84
+ export function checkClusterStyleTokens(styles: readonly string[]): string[] {
85
+ return styles.filter((p) => p !== 'radial' && p !== 'striped' && p !== 'rounded');
86
+ }
87
+
88
+ /** Count of `ch` in `s`. */
89
+ function countChar(s: string, ch: string): number {
90
+ return [...s].filter((c) => c === ch).length;
91
+ }
92
+
93
+ /** True when emit_edge_graphics' multicolor() accepts the colour list. */
94
+ function multicolorTaken(color: string): boolean {
95
+ if (countChar(color, ';') === 0 || countChar(color, ':') === 0) return false;
96
+ const { error } = parseSegsSilent(color);
97
+ return error !== 1 && error !== 2;
98
+ }
99
+
100
+ /**
101
+ * How many extra `gvrender_set_style(job, styles)` calls emit_edge_graphics
102
+ * makes: one per bezier carrying an arrow, when the spline has several
103
+ * beziers, in the `;` multicolor branch (when its colour list parses) and in
104
+ * the plain branch. The tapered and parallel `:` branches make none.
105
+ * @see lib/common/emit.c:2047 multicolor · :2677 emit_edge_graphics
106
+ */
107
+ export function edgeStyleResets(e: Edge, styles: readonly string[]): number {
108
+ const spl = e.info.spl;
109
+ if (spl === undefined || spl.size <= 1 || styles.length === 0) return 0;
110
+ const color = e.attrs.get('color') ?? '';
111
+ const otherBranch = styles.includes('tapered') || countChar(color, ':') > 0;
112
+ if (otherBranch && !multicolorTaken(color)) return 0;
113
+ return spl.list.filter((bz) => bz.sflag !== 0 || bz.eflag !== 0).length;
114
+ }
@@ -15,6 +15,7 @@
15
15
 
16
16
  import { invalidArgType } from '../errors.js';
17
17
  import type { ImageSizer } from '../common/htmltable-types.js';
18
+ import { findImageBytes, type ImageResolver } from './image-resolver.js';
18
19
 
19
20
  export type { ImageSizer } from '../common/htmltable-types.js';
20
21
 
@@ -37,3 +38,28 @@ export function setImageSizer(sizer: ImageSizer | null): void {
37
38
  export function findImageSize(src: string): { w: number; h: number } | null {
38
39
  return activeSizer !== null ? activeSizer(src) : null;
39
40
  }
41
+
42
+ /**
43
+ * `strerror(ENOENT)`: the port has no filesystem, so an image the caller
44
+ * cannot supply is C's missing file (AD-11).
45
+ */
46
+ export const STRERROR_ENOENT = 'No such file or directory';
47
+
48
+ /**
49
+ * Whether the caller supplies `name` at all: a size from `sizer` (default: the
50
+ * global one) or resolver bytes (a per-context `resolver` replaces the global). Mirrors gvusershape_open
51
+ * finding the file.
52
+ */
53
+ export function imageIsSupplied(name: string, sizer?: ImageSizer, resolver?: ImageResolver): boolean {
54
+ const size = sizer !== undefined ? sizer(name) : findImageSize(name);
55
+ return size !== null || findImageBytes(name, resolver) !== null;
56
+ }
57
+
58
+ /**
59
+ * The `agwarningf` text `gvusershape_file_access` prints when it cannot open
60
+ * `name`. C does not cache a failed open, so every sizing request repeats it.
61
+ * @see lib/gvc/gvusershape.c:gvusershape_file_access
62
+ */
63
+ export function imageOpenWarning(name: string): string {
64
+ return `${STRERROR_ENOENT} while opening ${name}`;
65
+ }
package/src/index.ts CHANGED
@@ -22,6 +22,7 @@ import type { GvError, RenderResult } from './errors.js';
22
22
  import type { EngineName } from './gvc/context.js';
23
23
  import { render as deviceRender } from './gvc/device.js';
24
24
  import { createDefaultContext } from './gvc/default-context.js';
25
+ import { resetRenderState } from './common/render-state.js';
25
26
 
26
27
 
27
28
 
@@ -90,6 +91,7 @@ function classifyError(err: unknown): GvError {
90
91
  */
91
92
  export function renderSvg(dotSource: string, engine: EngineName): string {
92
93
  checkRenderArgs(dotSource, engine);
94
+ resetRenderState(); // AD-4: one render = one native process
93
95
  const ctx = createDefaultContext();
94
96
  try {
95
97
  // parse() is inside the try so any non-ParseError throw (e.g. a raw
@@ -122,6 +124,7 @@ export function renderSvg(dotSource: string, engine: EngineName): string {
122
124
  export function tryRenderSvg(dotSource: string, engine: EngineName): RenderResult {
123
125
  checkRenderArgs(dotSource, engine);
124
126
  try {
127
+ // renderSvg calls resetRenderState first (AD-4).
125
128
  return { svg: renderSvg(dotSource, engine) };
126
129
  } catch (err: unknown) {
127
130
  if (isUsageError(err)) throw err;
@@ -27,6 +27,7 @@ import { CLUSTER } from './rank.js';
27
27
  import { makeSlots, safeOtherEdge, makeInterclustChain } from './cluster-path.js';
28
28
  // mergeChain lives in classify.ts (circular: classify ↔ cluster — safe for function refs)
29
29
  import { mergeChain, portsEq } from './classify.js';
30
+ import { agwarningf } from '../../common/agerr.js';
30
31
 
31
32
  export { CLUSTER_EDGE } from './cluster-path.js';
32
33
  export { mapInterclustNode } from './cluster-path.js';
@@ -297,7 +298,7 @@ export function markClusterNode(clust: Graph, n: Node, g: Graph): void {
297
298
  if ((n.info.ranktype ?? 0) !== 0) {
298
299
  // C warns with the ENCLOSING graph's name, not the cluster's.
299
300
  // @see lib/dotgen/cluster.c:317-320
300
- console.error(`Warning: ${n.name} was already in a rankset, deleted from cluster ${g.name}`);
301
+ agwarningf(`${n.name} was already in a rankset, deleted from cluster ${g.name}`);
301
302
  agDeleteFromCluster(clust, n);
302
303
  return;
303
304
  }
@@ -12,10 +12,9 @@ import type { Edge } from '../../model/edge.js';
12
12
  import {
13
13
  inBoxf, midPointf, splineIntersectf, boxIntersectf,
14
14
  } from './compound-clip.js';
15
- import { arrowEndClip, tailArrowEndClip, arrowClipLength } from './edge-route-clip.js';
15
+ import { arrowEndClip, tailArrowEndClip, arrowClipLength, distFused } from './edge-route-clip.js';
16
16
  import { arrowDrawOpsForEnd, edgeArrowName, edgeArrowsize } from './edge-route-arrow.js';
17
17
  import { edgePenwidthAttr, edgeRenderPenwidth } from './edge-route-helpers.js';
18
- import { dist } from '../../common/arrows-geometry.js';
19
18
 
20
19
  // Re-export geometry helpers so existing callers (tests) import from one place.
21
20
  export {
@@ -67,7 +66,7 @@ function arrowEndClipIdx(state: ClipState, endp: number): number {
67
66
  const elen = arrowClipLength(edgeArrowName(e, 'head'), edgeArrowsize(e), edgePenwidthAttr(e));
68
67
  bez.eflag = bez.eflag || 1;
69
68
  bez.ep = { ...bez.list[endp + 3] };
70
- if (endp > state.starti && dist(bez.list[endp], bez.list[endp + 3]) < elen) endp -= 3;
69
+ if (endp > state.starti && distFused(bez.list[endp], bez.list[endp + 3]) < elen) endp -= 3;
71
70
  const seg = [bez.list[endp], bez.list[endp + 1], bez.list[endp + 2], bez.list[endp + 3]];
72
71
  const clipped = arrowEndClip(seg, bez.ep, elen);
73
72
  for (let k = 0; k < 4; k++) bez.list[endp + k] = clipped[k];
@@ -83,7 +82,7 @@ function arrowStartClipIdx(state: ClipState, startp: number, endp: number): numb
83
82
  const slen = arrowClipLength(edgeArrowName(e, 'tail'), edgeArrowsize(e), edgePenwidthAttr(e));
84
83
  bez.sflag = bez.sflag || 1;
85
84
  bez.sp = { ...bez.list[startp] };
86
- if (endp > startp && dist(bez.list[startp], bez.list[startp + 3]) < slen) startp += 3;
85
+ if (endp > startp && distFused(bez.list[startp], bez.list[startp + 3]) < slen) startp += 3;
87
86
  const seg = [bez.list[startp], bez.list[startp + 1], bez.list[startp + 2], bez.list[startp + 3]];
88
87
  const clipped = tailArrowEndClip(seg, bez.sp, slen);
89
88
  for (let k = 0; k < 4; k++) bez.list[startp + k] = clipped[k];
@@ -20,6 +20,7 @@ import { dotScanRanks } from './rank.js';
20
20
  import { dotRoot, rankGet } from './mincross-utils.js';
21
21
  import { nodesInSeq } from './decomp.js';
22
22
  import { graphMinrank, graphMaxrank } from './position-aux.js';
23
+ import { agerr, agerrorf, agwarningf } from '../../common/agerr.js';
23
24
 
24
25
  const UP = 0;
25
26
  const DOWN = 1;
@@ -263,13 +264,13 @@ export function fillRankVlist(g: Graph, r: number, root: Graph): number {
263
264
  const rankArr = g.info.rank!;
264
265
  const lead = g.info.rankleader![r];
265
266
  if (lead === undefined || lead === null) {
266
- console.error(`Error: rebuild_vlists: lead is null for rank ${r}`);
267
+ agerrorf(`rebuild_vlists: lead is null for rank ${r}`);
267
268
  return -1;
268
269
  }
269
270
  const rootRank = root.info.rank![r];
270
271
  if (rootRank.v[lead.info.order ?? 0] !== lead) {
271
- console.error(
272
- `Error: rebuild_vlists: rank lead ${lead.name} not in order ` +
272
+ agerrorf(
273
+ `rebuild_vlists: rank lead ${lead.name} not in order ` +
273
274
  `${lead.info.order ?? 0} of rank ${r}`,
274
275
  );
275
276
  return -1;
@@ -292,7 +293,7 @@ export function fillRankVlist(g: Graph, r: number, root: Graph): number {
292
293
  rankArr[r].v = rootRank.v;
293
294
  rankArr[r].vStart = lead.info.order ?? 0;
294
295
  const maxi = computeMaxi(g, r);
295
- if (maxi === -1) console.error(`Warning: degenerate concentrated rank ${g.name},${r}`);
296
+ if (maxi === -1) agwarningf(`degenerate concentrated rank ${g.name},${r}`);
296
297
  rankArr[r].n = maxi + 1;
297
298
  return 0;
298
299
  }
@@ -425,7 +426,7 @@ export function dotConcentrate(g: Graph): number {
425
426
  const clust = g.info.clust ?? [];
426
427
  for (let c = 0; c < nCluster; c++) {
427
428
  if (rebuildVlists(clust[c]) !== 0) {
428
- console.error('concentrate=true may not work correctly.');
429
+ agerr('prev', 'concentrate=true may not work correctly.');
429
430
  return -1;
430
431
  }
431
432
  }
@@ -3,6 +3,7 @@
3
3
  import type { Node } from '../../model/node.js';
4
4
  import type { Graph } from '../../model/graph.js';
5
5
  import type { EdgeList } from '../../model/nodeInfo.js';
6
+ import { registerRenderState } from '../../common/render-state.js';
6
7
 
7
8
  // ---------------------------------------------------------------------------
8
9
  // AGSEQ-order node iteration (agfstnode/agnxtnode mirror)
@@ -135,7 +136,9 @@ export function decompSearch(g: Graph, start: Node, cmark: number): void {
135
136
  }
136
137
 
137
138
  // Cmark as const-object property avoids module-level let that confuses Lizard.
139
+ /** @see lib/dotgen/decomp.c:Cmark (C static, 0 at process start) */
138
140
  const _dc = { cmark: 0 };
141
+ registerRenderState(() => { _dc.cmark = 0; });
139
142
 
140
143
  // ONE private function immediately before decompose — the acyclic.ts pattern.
141
144
  function resolveNode(n: Node, pass: 0 | 1): Node | undefined {