@knowvah/dot-engine 2.6.12 → 2.7.1

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 (198) 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/common/attr-get.d.ts +30 -0
  5. package/dist/common/attr-get.d.ts.map +1 -0
  6. package/dist/common/htmltable-emit-fill.d.ts +20 -2
  7. package/dist/common/htmltable-emit-fill.d.ts.map +1 -1
  8. package/dist/common/htmltable-emit.d.ts.map +1 -1
  9. package/dist/common/htmltable-lex-expat.d.ts.map +1 -1
  10. package/dist/common/htmltable-pos-runs.d.ts +7 -4
  11. package/dist/common/htmltable-pos-runs.d.ts.map +1 -1
  12. package/dist/common/htmltable.d.ts.map +1 -1
  13. package/dist/common/libm-trig.d.ts +5 -0
  14. package/dist/common/libm-trig.d.ts.map +1 -0
  15. package/dist/common/make-label.d.ts.map +1 -1
  16. package/dist/common/multicolor.d.ts +21 -13
  17. package/dist/common/multicolor.d.ts.map +1 -1
  18. package/dist/common/poly-gencode.d.ts +1 -2
  19. package/dist/common/poly-gencode.d.ts.map +1 -1
  20. package/dist/common/poly-init.d.ts +2 -12
  21. package/dist/common/poly-init.d.ts.map +1 -1
  22. package/dist/common/splines-clip.d.ts +7 -0
  23. package/dist/common/splines-clip.d.ts.map +1 -1
  24. package/dist/gvc/context.d.ts +6 -0
  25. package/dist/gvc/context.d.ts.map +1 -1
  26. package/dist/gvc/device-cluster.d.ts +15 -0
  27. package/dist/gvc/device-cluster.d.ts.map +1 -1
  28. package/dist/gvc/device.d.ts.map +1 -1
  29. package/dist/gvc/first-layer.d.ts +9 -0
  30. package/dist/gvc/first-layer.d.ts.map +1 -0
  31. package/dist/gvc/gvrender-set-style.d.ts +28 -0
  32. package/dist/gvc/gvrender-set-style.d.ts.map +1 -0
  33. package/dist/gvc/usershape.d.ts +18 -0
  34. package/dist/gvc/usershape.d.ts.map +1 -1
  35. package/dist/index.js +2478 -912
  36. package/dist/index.js.map +4 -4
  37. package/dist/layout/dot/compound.d.ts.map +1 -1
  38. package/dist/layout/dot/edge-route-clip.d.ts +14 -4
  39. package/dist/layout/dot/edge-route-clip.d.ts.map +1 -1
  40. package/dist/layout/dot/edge-route-routing.d.ts.map +1 -1
  41. package/dist/layout/dot/graph-label.d.ts.map +1 -1
  42. package/dist/layout/neato/__fixtures__/dijkstra-f.d.ts +11 -0
  43. package/dist/layout/neato/__fixtures__/dijkstra-f.d.ts.map +1 -0
  44. package/dist/layout/neato/__fixtures__/hier-imds.d.ts +36 -0
  45. package/dist/layout/neato/__fixtures__/hier-imds.d.ts.map +1 -0
  46. package/dist/layout/neato/__fixtures__/hier-majorization.d.ts +32 -0
  47. package/dist/layout/neato/__fixtures__/hier-majorization.d.ts.map +1 -0
  48. package/dist/layout/neato/cdt-surface.d.ts.map +1 -1
  49. package/dist/layout/neato/compute-hierarchy.d.ts +24 -0
  50. package/dist/layout/neato/compute-hierarchy.d.ts.map +1 -0
  51. package/dist/layout/neato/conjgrad-mkernel.d.ts +20 -0
  52. package/dist/layout/neato/conjgrad-mkernel.d.ts.map +1 -0
  53. package/dist/layout/neato/conjgrad.d.ts +19 -0
  54. package/dist/layout/neato/conjgrad.d.ts.map +1 -1
  55. package/dist/layout/neato/constrained-majorization.d.ts +26 -0
  56. package/dist/layout/neato/constrained-majorization.d.ts.map +1 -0
  57. package/dist/layout/neato/dijkstra.d.ts +33 -16
  58. package/dist/layout/neato/dijkstra.d.ts.map +1 -1
  59. package/dist/layout/neato/gts-triangle.d.ts +14 -0
  60. package/dist/layout/neato/gts-triangle.d.ts.map +1 -0
  61. package/dist/layout/neato/init.d.ts +2 -12
  62. package/dist/layout/neato/init.d.ts.map +1 -1
  63. package/dist/layout/neato/kkutils.d.ts +20 -0
  64. package/dist/layout/neato/kkutils.d.ts.map +1 -0
  65. package/dist/layout/neato/make-graph-data.d.ts +43 -0
  66. package/dist/layout/neato/make-graph-data.d.ts.map +1 -0
  67. package/dist/layout/neato/matrix-ops.d.ts +46 -0
  68. package/dist/layout/neato/matrix-ops.d.ts.map +1 -1
  69. package/dist/layout/neato/opt-arrangement.d.ts +20 -0
  70. package/dist/layout/neato/opt-arrangement.d.ts.map +1 -0
  71. package/dist/layout/neato/quad-prog-solve.d.ts +44 -0
  72. package/dist/layout/neato/quad-prog-solve.d.ts.map +1 -0
  73. package/dist/layout/neato/smart-ini-x.d.ts +9 -0
  74. package/dist/layout/neato/smart-ini-x.d.ts.map +1 -0
  75. package/dist/layout/neato/splines.d.ts +1 -1
  76. package/dist/layout/neato/splines.d.ts.map +1 -1
  77. package/dist/layout/neato/start.d.ts +4 -1
  78. package/dist/layout/neato/start.d.ts.map +1 -1
  79. package/dist/layout/neato/stress.d.ts +8 -0
  80. package/dist/layout/neato/stress.d.ts.map +1 -1
  81. package/dist/model/edge.d.ts +1 -1
  82. package/dist/model/edge.d.ts.map +1 -1
  83. package/dist/model/lazy-attrs.d.ts +10 -0
  84. package/dist/model/lazy-attrs.d.ts.map +1 -0
  85. package/dist/model/node.d.ts +1 -1
  86. package/dist/model/node.d.ts.map +1 -1
  87. package/dist/ortho/index.d.ts.map +1 -1
  88. package/dist/render/dot/edge-draw.d.ts +1 -1
  89. package/dist/render/dot/edge-draw.d.ts.map +1 -1
  90. package/dist/render/svg-multicolor.d.ts +2 -2
  91. package/dist/render/svg-multicolor.d.ts.map +1 -1
  92. package/dist/render.js +2358 -792
  93. package/dist/render.js.map +4 -4
  94. package/package.json +1 -1
  95. package/src/common/attr-get.ts +46 -0
  96. package/src/common/graph-init.ts +1 -1
  97. package/src/common/htmltable-emit-fill.ts +56 -11
  98. package/src/common/htmltable-emit.ts +4 -3
  99. package/src/common/htmltable-lex-expat.ts +21 -3
  100. package/src/common/htmltable-pos-runs.ts +11 -6
  101. package/src/common/htmltable.ts +28 -116
  102. package/src/common/libm-trig.ts +43 -0
  103. package/src/common/make-label.ts +14 -2
  104. package/src/common/multicolor.ts +88 -24
  105. package/src/common/poly-gencode.ts +16 -8
  106. package/src/common/poly-init.ts +23 -19
  107. package/src/common/postproc.ts +2 -2
  108. package/src/common/splines-clip.ts +15 -12
  109. package/src/common/splines-routespl.ts +2 -2
  110. package/src/common/textmeasure-lookup.ts +1 -1
  111. package/src/gvc/context.ts +8 -0
  112. package/src/gvc/device-cluster.ts +63 -8
  113. package/src/gvc/device.ts +16 -2
  114. package/src/gvc/first-layer.ts +26 -0
  115. package/src/gvc/gvrender-set-style.ts +114 -0
  116. package/src/gvc/usershape.ts +26 -0
  117. package/src/layout/dot/cluster.ts +1 -1
  118. package/src/layout/dot/compound.ts +3 -4
  119. package/src/layout/dot/conc.ts +1 -1
  120. package/src/layout/dot/edge-route-clip.ts +19 -7
  121. package/src/layout/dot/edge-route-routing.ts +11 -3
  122. package/src/layout/dot/fastgr.ts +2 -2
  123. package/src/layout/dot/graph-label.ts +1 -15
  124. package/src/layout/neato/__fixtures__/dijkstra-f.ts +17 -0
  125. package/src/layout/neato/__fixtures__/hier/c1554.gv +33 -0
  126. package/src/layout/neato/__fixtures__/hier/c1554.plain +26 -0
  127. package/src/layout/neato/__fixtures__/hier/c1554.xdot +161 -0
  128. package/src/layout/neato/__fixtures__/hier/cases/cycle.gv +1 -0
  129. package/src/layout/neato/__fixtures__/hier/cases/cycle.plain +10 -0
  130. package/src/layout/neato/__fixtures__/hier/cases/cycle.stderr +0 -0
  131. package/src/layout/neato/__fixtures__/hier/cases/dirnone.gv +1 -0
  132. package/src/layout/neato/__fixtures__/hier/cases/dirnone.plain +11 -0
  133. package/src/layout/neato/__fixtures__/hier/cases/dirnone.stderr +0 -0
  134. package/src/layout/neato/__fixtures__/hier/cases/epsilon.gv +1 -0
  135. package/src/layout/neato/__fixtures__/hier/cases/epsilon.plain +13 -0
  136. package/src/layout/neato/__fixtures__/hier/cases/epsilon.stderr +0 -0
  137. package/src/layout/neato/__fixtures__/hier/cases/levelsgap.gv +1 -0
  138. package/src/layout/neato/__fixtures__/hier/cases/levelsgap.plain +8 -0
  139. package/src/layout/neato/__fixtures__/hier/cases/levelsgap.stderr +0 -0
  140. package/src/layout/neato/__fixtures__/hier/cases/mds.gv +1 -0
  141. package/src/layout/neato/__fixtures__/hier/cases/mds.plain +10 -0
  142. package/src/layout/neato/__fixtures__/hier/cases/mds.stderr +0 -0
  143. package/src/layout/neato/__fixtures__/hier/cases/multiedge.gv +1 -0
  144. package/src/layout/neato/__fixtures__/hier/cases/multiedge.plain +10 -0
  145. package/src/layout/neato/__fixtures__/hier/cases/multiedge.stderr +0 -0
  146. package/src/layout/neato/__fixtures__/hier/cases/neglevelsgap.gv +1 -0
  147. package/src/layout/neato/__fixtures__/hier/cases/neglevelsgap.plain +8 -0
  148. package/src/layout/neato/__fixtures__/hier/cases/neglevelsgap.stderr +0 -0
  149. package/src/layout/neato/__fixtures__/hier/cases/startrandom.gv +1 -0
  150. package/src/layout/neato/__fixtures__/hier/cases/startrandom.plain +10 -0
  151. package/src/layout/neato/__fixtures__/hier/cases/startrandom.stderr +0 -0
  152. package/src/layout/neato/__fixtures__/hier/cases/startregular.gv +1 -0
  153. package/src/layout/neato/__fixtures__/hier/cases/startregular.plain +10 -0
  154. package/src/layout/neato/__fixtures__/hier/cases/startregular.stderr +0 -0
  155. package/src/layout/neato/__fixtures__/hier/cases/tdist.gv +1 -0
  156. package/src/layout/neato/__fixtures__/hier/cases/tdist.plain +13 -0
  157. package/src/layout/neato/__fixtures__/hier/cases/tdist.stderr +0 -0
  158. package/src/layout/neato/__fixtures__/hier/cases/undirected.gv +1 -0
  159. package/src/layout/neato/__fixtures__/hier/cases/undirected.plain +10 -0
  160. package/src/layout/neato/__fixtures__/hier/cases/undirected.stderr +0 -0
  161. package/src/layout/neato/__fixtures__/hier/hier-probe.gv +1 -0
  162. package/src/layout/neato/__fixtures__/hier/hier-probe.plain +16 -0
  163. package/src/layout/neato/__fixtures__/hier/hier-probe.xdot +62 -0
  164. package/src/layout/neato/__fixtures__/hier-imds.ts +151 -0
  165. package/src/layout/neato/__fixtures__/hier-majorization.ts +145 -0
  166. package/src/layout/neato/cdt-surface.ts +6 -16
  167. package/src/layout/neato/compute-hierarchy.ts +90 -0
  168. package/src/layout/neato/conjgrad-mkernel.ts +141 -0
  169. package/src/layout/neato/conjgrad.ts +208 -0
  170. package/src/layout/neato/constrained-majorization.ts +479 -0
  171. package/src/layout/neato/dijkstra.ts +218 -92
  172. package/src/layout/neato/edge-len.ts +2 -2
  173. package/src/layout/neato/fdp-adjust.ts +3 -3
  174. package/src/layout/neato/gts-triangle.ts +42 -0
  175. package/src/layout/neato/init.ts +57 -57
  176. package/src/layout/neato/kk.ts +2 -2
  177. package/src/layout/neato/kkutils.ts +96 -0
  178. package/src/layout/neato/make-graph-data.ts +166 -0
  179. package/src/layout/neato/matrix-ops.ts +138 -0
  180. package/src/layout/neato/opt-arrangement.ts +86 -0
  181. package/src/layout/neato/quad-prog-solve.ts +489 -0
  182. package/src/layout/neato/smart-ini-x.ts +503 -0
  183. package/src/layout/neato/splines.ts +4 -4
  184. package/src/layout/neato/start.ts +7 -6
  185. package/src/layout/neato/stress.ts +28 -0
  186. package/src/model/edge.ts +3 -2
  187. package/src/model/lazy-attrs.ts +36 -0
  188. package/src/model/node.ts +3 -2
  189. package/src/ortho/index.ts +2 -3
  190. package/src/parser/dot.js +1 -1
  191. package/src/parser/dot.pegjs +1 -1
  192. package/src/parser/index.ts +1 -1
  193. package/src/render/color-resolve.ts +2 -2
  194. package/src/render/dot/edge-draw.ts +6 -3
  195. package/src/render/dot.ts +1 -1
  196. package/src/render/svg-edge-split.ts +2 -2
  197. package/src/render/svg-multicolor.ts +4 -3
  198. package/src/render/svg-parallel-edge.ts +2 -2
@@ -13,6 +13,10 @@
13
13
  * @see lib/common/emit.c:470 parseSegs
14
14
  */
15
15
 
16
+ import { agerr, agerrorf, agwarningf } from './agerr.js';
17
+ import type { Edge } from '../model/edge.js';
18
+ import { registerRenderState } from './render-state.js';
19
+
16
20
  // ---------------------------------------------------------------------------
17
21
  // Types
18
22
  // ---------------------------------------------------------------------------
@@ -38,6 +42,28 @@ export interface ColorSeg {
38
42
  // Constants
39
43
  // ---------------------------------------------------------------------------
40
44
 
45
+ /**
46
+ * C's `static atomic_flag warned` in parseSegs: set by the first Total-size
47
+ * warning or Illegal-value error in the process, silencing both afterwards.
48
+ * Reset per render (AD-4).
49
+ * @see lib/common/emit.c:470 parseSegs
50
+ */
51
+ const parseSegsState = { warned: false };
52
+
53
+ registerRenderState(() => {
54
+ parseSegsState.warned = false;
55
+ });
56
+
57
+ /** `atomic_flag_test_and_set(&warned)` negated: true when this call claimed it. */
58
+ function claimWarned(): boolean {
59
+ if (parseSegsState.warned) return false;
60
+ parseSegsState.warned = true;
61
+ return true;
62
+ }
63
+
64
+ /** A claim that never fires: parses without touching or consuming the flag. */
65
+ const claimNever = (): boolean => false;
66
+
41
67
  /** @see lib/common/emit.c:451 EPS */
42
68
  const EPS = 1e-5;
43
69
 
@@ -76,7 +102,7 @@ function appendSeg(
76
102
  token: string,
77
103
  left: number,
78
104
  segs: ColorSeg[],
79
- warnedExceeded: { v: boolean },
105
+ claim: () => boolean,
80
106
  ): { left: number; rval: 0 | 3 } | null {
81
107
  const { color, v } = getSegLen(token);
82
108
  if (v < 0) return null;
@@ -85,10 +111,7 @@ function appendSeg(
85
111
  let rval: 0 | 3 = 0;
86
112
  const del = t - left;
87
113
  if (del > 0) {
88
- if (!aeq0(del) && !warnedExceeded.v) {
89
- warnedExceeded.v = true;
90
- rval = 3;
91
- }
114
+ if (!aeq0(del) && claim()) rval = 3;
92
115
  t = left;
93
116
  }
94
117
  left -= t;
@@ -128,34 +151,20 @@ function trimTrailing(segs: ColorSeg[]): void {
128
151
  // ---------------------------------------------------------------------------
129
152
 
130
153
  /**
131
- * Parse `"c1;f1:c2;f2:…"` into segments.
132
- *
133
- * Fractions are optional and non-negative; their sum must not exceed 1.
134
- * Segments without an explicit fraction share the remaining weight equally;
135
- * if none exist the remainder is added to the last segment.
136
- *
137
- * Return codes (mirror C):
138
- * 0 → ok; 1 → silent error; 2 → error with message; 3 → warning (sum > 1).
139
- * On rv 1 or 2 the returned segs array is empty.
140
- *
154
+ * The parse of `parseSegs` with the once-per-process flag abstracted as `claim`.
141
155
  * @see lib/common/emit.c:470 parseSegs
142
156
  */
143
- export function parseSegs(
157
+ function scanSegs(
144
158
  colorlist: string,
159
+ claim: () => boolean,
145
160
  ): { segs: ColorSeg[]; error: 0 | 1 | 2 | 3 } {
146
161
  const segs: ColorSeg[] = [];
147
162
  let left = 1;
148
163
  let rval: 0 | 3 = 0;
149
- let badFloatCount = 0;
150
- const warnedExceeded = { v: false };
151
164
 
152
165
  for (const token of colorlist.split(':')) {
153
- const result = appendSeg(token, left, segs, warnedExceeded);
154
- if (result === null) {
155
- const errCode: 1 | 2 = badFloatCount === 0 ? 2 : 1;
156
- badFloatCount++;
157
- return { segs: [], error: errCode };
158
- }
166
+ const result = appendSeg(token, left, segs, claim);
167
+ if (result === null) return { segs: [], error: claim() ? 2 : 1 };
159
168
  if (result.rval === 3) rval = 3;
160
169
  left = result.left;
161
170
  if (aeq0(left)) { left = 0; break; }
@@ -165,3 +174,58 @@ export function parseSegs(
165
174
  trimTrailing(segs);
166
175
  return { segs, error: rval };
167
176
  }
177
+
178
+ /**
179
+ * Parse `"c1;f1:c2;f2:…"` into segments.
180
+ *
181
+ * Fractions are optional and non-negative; their sum must not exceed 1.
182
+ * Segments without an explicit fraction share the remaining weight equally;
183
+ * if none exist the remainder is added to the last segment.
184
+ *
185
+ * Return codes (mirror C):
186
+ * 0 → ok; 1 → silent error; 2 → error with message; 3 → warning (sum > 1).
187
+ * On rv 1 or 2 the returned segs array is empty.
188
+ *
189
+ * The rv 2 error and rv 3 warning are printed here, once per process (the
190
+ * shared `warned` flag). C's callers then append an AGPREV line for rv > 1;
191
+ * `context` supplies it (e.g. `in edge a -> b`): the warning continues the
192
+ * same line as C's AGPREV does, the error's context follows on its own line.
193
+ *
194
+ * @see lib/common/emit.c:470 parseSegs
195
+ */
196
+ export function parseSegs(
197
+ colorlist: string,
198
+ context?: () => string,
199
+ ): { segs: ColorSeg[]; error: 0 | 1 | 2 | 3 } {
200
+ const result = scanSegs(colorlist, claimWarned);
201
+ if (result.error === 3) {
202
+ const tail = context === undefined ? '' : context();
203
+ agwarningf(`Total size > 1 in "${colorlist}" color spec ${tail}`);
204
+ } else if (result.error === 2) {
205
+ agerrorf(`Illegal value in "${colorlist}" color attribute; float expected after ';'`);
206
+ if (context !== undefined) agerr('prev', context());
207
+ }
208
+ return result;
209
+ }
210
+
211
+ /**
212
+ * {@link parseSegs} for port-internal re-parses that have no C counterpart
213
+ * call (pre-passes, parallel edges): same segments and error class, but never
214
+ * prints and never consumes the once-per-process flag.
215
+ */
216
+ export function parseSegsSilent(
217
+ colorlist: string,
218
+ ): { segs: ColorSeg[]; error: 0 | 1 | 2 | 3 } {
219
+ return scanSegs(colorlist, claimNever);
220
+ }
221
+
222
+ /**
223
+ * The AGPREV line C's `multicolor` appends for an rv > 1 parseSegs.
224
+ * @see lib/common/emit.c:1985 multicolor
225
+ */
226
+ export function edgeSegsContext(e: Edge): () => string {
227
+ return () => {
228
+ const directed = !e.tail.root.kind.endsWith('undirected');
229
+ return `in edge ${e.tail.name}${directed ? ' -> ' : ' -- '}${e.head.name}`;
230
+ };
231
+ }
@@ -22,6 +22,7 @@ import { FillType } from '../gvc/context.js';
22
22
  import { emitHtmlLabel } from './htmltable-emit.js';
23
23
  import { transformPoint } from '../gvc/device.js';
24
24
  import { nodeAttr } from './poly-init.js';
25
+ import { gvrenderSetStyle, checkStyleTokens } from '../gvc/gvrender-set-style.js';
25
26
  import { substObjAnchor, preprocessTooltip } from './subst.js';
26
27
  import type { ResolvedFill, PolyStyleFlags } from './style-resolve.js';
27
28
  import {
@@ -96,6 +97,7 @@ interface RingCtx {
96
97
  job: RenderJob;
97
98
  style: PolyStyleFlags;
98
99
  fillcolor: string;
100
+ nodeName: string; // agnameof(n): the `in node %s` AGPREV of a parseSegs rv > 1
99
101
  shape: number;
100
102
  diagonals: boolean;
101
103
  rounded: boolean;
@@ -114,7 +116,7 @@ function renderEllipseRing(
114
116
  const pf = ring.map(
115
117
  (v) => transformPoint({ x: v.x + coord.x, y: v.y + coord.y }, ctx.job),
116
118
  );
117
- wedgedEllipse(ctx.job, pf, ctx.fillcolor, ctx.renderer);
119
+ wedgedEllipse(ctx.job, pf, ctx.fillcolor, ctx.renderer, () => `in node ${ctx.nodeName}`);
118
120
  renderEllipse(ring, coord, ctx.renderer, ctx.job, false); // boundary unfilled
119
121
  } else {
120
122
  renderEllipse(ring, coord, ctx.renderer, ctx.job, filled);
@@ -124,14 +126,12 @@ function renderEllipseRing(
124
126
  }
125
127
 
126
128
  /**
127
- * Render one polygon periphery ring, dispatching to stripedBox when
128
- * style.striped + j==0.
129
+ * Striped j===0 ring: the multicolor box plus an unfilled boundary polygon.
129
130
  * @see lib/common/shapes.c:poly_gencode :3037-3043
130
131
  */
131
- /** Striped j===0 ring: the multicolor box plus an unfilled boundary polygon. */
132
132
  function renderStripedRing(ring: Point[], coord: Point, ctx: RingCtx): void {
133
133
  const af = ring.map((v) => transformPoint({ x: v.x + coord.x, y: v.y + coord.y }, ctx.job));
134
- stripedBox(ctx.job, af, ctx.fillcolor, true, ctx.renderer);
134
+ stripedBox(ctx.job, af, ctx.fillcolor, true, ctx.renderer, () => `in node ${ctx.nodeName}`);
135
135
  renderPolygon(ring, coord, ctx.renderer, ctx.job, false); // boundary unfilled
136
136
  }
137
137
 
@@ -367,6 +367,13 @@ function applyPenState(obj: ObjState, styleAttr: string | undefined,
367
367
  export function applyNodeStyle(obj: ObjState, n: Node, job: RenderJob): boolean {
368
368
  const styleAttr = nodeAttr(n, n.root, 'style');
369
369
  const colorAttr = nodeAttr(n, n.root, 'color');
370
+ // stylenode: gvrender_set_style(job, checkStyle list), before any colour is
371
+ // resolved; point_gencode passes its own fixed list instead (never unknown).
372
+ // @see lib/common/shapes.c:536 stylenode · :3257 point_gencode
373
+ if (!isPointNode(n)) {
374
+ const shape = n.info.shape as ShapeDesc | undefined;
375
+ gvrenderSetStyle(checkStyleTokens(styleTokens(styleAttr), shape?.polygon ?? null));
376
+ }
370
377
  // The node's `colorscheme` window is NOT opened here: C opens it once around
371
378
  // the whole node emission (emit_begin_node..emit_end_node), which covers both
372
379
  // this color block and the label textspans. Both callers of this function are
@@ -391,7 +398,8 @@ export function applyNodeStyle(obj: ObjState, n: Node, job: RenderJob): boolean
391
398
  */
392
399
  function prepareDrawPoly(poly: PolygonT, filled: boolean, obj: ObjState): PolygonT {
393
400
  if (poly.peripheries !== 0 || !filled) return poly;
394
- obj.penColor = { type: 'string', s: 'transparent' };
401
+ // gvrender_set_pencolor resolves (and may report) the name like any other.
402
+ obj.penColor = resolveRenderColor('transparent');
395
403
  return { ...poly, peripheries: 1 };
396
404
  }
397
405
 
@@ -446,6 +454,7 @@ function resolveNodeDrawCtx(job: RenderJob, n: Node): NodeDrawCtx | null {
446
454
  job,
447
455
  style: styleFlags,
448
456
  fillcolor: nodeAttr(n, n.root, 'fillcolor') ?? '',
457
+ nodeName: n.name,
449
458
  ...specialCornerFlags(drawPoly.option, styleFlags),
450
459
  };
451
460
  return { poly: drawPoly, coord, filled, ringCtx };
@@ -467,8 +476,7 @@ function specialCornerFlags(
467
476
  }
468
477
 
469
478
  /**
470
- * Render node shape (polygon/ellipse) and label.
471
- * Shape codefn — called from walkNodes.
479
+ * Render node shape (polygon/ellipse) and label; shape codefn, from walkNodes.
472
480
  * @see lib/common/shapes.c:poly_gencode
473
481
  */
474
482
  export function polyGencode(rawJob: unknown, rawNode: unknown): void {
@@ -10,6 +10,7 @@
10
10
  import type { Node } from '../model/node.js';
11
11
  import type { Graph } from '../model/graph.js';
12
12
  import type { TextMeasurer } from './textmeasure.js';
13
+ import { nodeAttr } from './attr-get.js';
13
14
  import type { PolygonT, ShapeDesc } from './types.js';
14
15
  import { ShapeKind } from './types.js';
15
16
  import type { TextlabelT } from './types.js';
@@ -25,30 +26,16 @@ import {
25
26
  } from './make-label.js';
26
27
  import { computeVertices } from './poly-vertices.js';
27
28
  import { agwarningf } from './agerr.js';
29
+ import { imageIsSupplied, imageOpenWarning } from '../gvc/usershape.js';
28
30
 
29
31
  // Default half-dimensions in points (72 dpi, 0.75 in wide, 0.5 in tall)
30
32
  const DEFAULT_LW = 27;
31
33
  const DEFAULT_RW = 27;
32
34
  const DEFAULT_HT = 36;
33
35
 
34
- /**
35
- * Read a node attribute: the node's own value, else the node-attribute defaults
36
- * (`node [...]`) captured from the node's declaring scope up to root *at the
37
- * time the node was created*. The snapshot makes a `node [...]` declared after
38
- * the node — at any scope — correctly not apply, and lets a cluster-scoped
39
- * default reach nodes declared in that cluster, as the oracle does.
40
- *
41
- * Programmatically created nodes have no snapshot; they fall back to the live
42
- * defaults of the passed graph `g` and root.
43
- * @see lib/cgraph/attr.c:agxget; src/parser/builder.ts:effectiveNodeDefaults
44
- */
45
- export function nodeAttr(n: Node, g: Graph, key: string): string | undefined {
46
- const own = n.attrs.get(key);
47
- if (own !== undefined) return own;
48
- if (n.nodeDefaultsSnapshot !== undefined) return n.nodeDefaultsSnapshot.get(key);
49
- return g.nodeDefaults.get(key)
50
- ?? (g !== g.root ? g.root.nodeDefaults.get(key) : undefined);
51
- }
36
+ // nodeAttr moved to attr-get.ts (leaf module, shared with make-label.ts);
37
+ // re-exported so existing importers are unchanged.
38
+ export { nodeAttr };
52
39
 
53
40
  /** Read label-font attributes from node, falling back to defaults. */
54
41
  export function readFontAttrs(n: Node, g: Graph): {
@@ -111,6 +98,22 @@ export function buildNodeLabel(n: Node, g: Graph, measurer: TextMeasurer): void
111
98
  n.info.label = makeAnyLabel(content, isHtml, font, measurer, n);
112
99
  }
113
100
 
101
+ /**
102
+ * poly_init's `image` branch (a non-custom shape): when the caller supplies no
103
+ * image, gvusershape_size fails and C warns after gvusershape_open's own
104
+ * warning. The port never sizes node images.
105
+ * @see lib/common/shapes.c:2043-2046
106
+ */
107
+ function warnMissingImage(n: Node, g: Graph, shapefile: string | undefined): void {
108
+ if (shapefile !== undefined && shapefile !== '' && nodeAttr(n, g, 'shape') !== 'epsf') return;
109
+ const image = nodeAttr(n, g, 'image');
110
+ if (image === undefined || image === '') return;
111
+ const gvc = g.root.info.gvc;
112
+ if (imageIsSupplied(image, gvc?.imageSizer, gvc?.imageResolver)) return;
113
+ agwarningf(imageOpenWarning(image));
114
+ agwarningf(`No or improper image="${image}" for node "${n.name}"`);
115
+ }
116
+
114
117
  export function polyInit(n: Node, g: Graph, measurer: TextMeasurer): void {
115
118
  const shapeName = nodeAttr(n, g, 'shape') ?? 'ellipse';
116
119
  const shapefile = nodeAttr(n, g, 'shapefile');
@@ -118,12 +121,13 @@ export function polyInit(n: Node, g: Graph, measurer: TextMeasurer): void {
118
121
  // C warns when the custom shape's image cannot be sized (headless: always).
119
122
  // @see lib/common/shapes.c:2027-2033
120
123
  if (shapefile !== undefined && shapefile !== '' && shapeName !== 'epsf') {
121
- agwarningf(`No or improper shapefile="${shapefile}" for node "${n.name}"`, 'error');
124
+ agwarningf(`No or improper shapefile="${shapefile}" for node "${n.name}"`);
122
125
  }
123
126
  if ((n.info.shape as ShapeDesc).kind === ShapeKind.SH_RECORD) {
124
127
  if (n.info.shape_info === undefined) recordNodeInit(n, g, measurer);
125
128
  return;
126
129
  }
130
+ warnMissingImage(n, g, shapefile);
127
131
  buildNodeLabel(n, g, measurer);
128
132
 
129
133
  // Layout-time common_init_node already installed the attr-resolved
@@ -27,7 +27,7 @@ import {
27
27
  } from '../layout/dot/init.js';
28
28
  import { addXLabels } from './xlabels-place.js';
29
29
  import { registerRenderState } from './render-state.js';
30
- import { agerrLineOn } from './agerr.js';
30
+ import { agerrorf } from './agerr.js';
31
31
 
32
32
  /** Concentrated multi-edge duplicate — never drawn. @see lib/common/const.h:IGNORED */
33
33
  const IGNORED_EDGE = 6;
@@ -229,7 +229,7 @@ function translateDrawing(g: Graph): void {
229
229
  // @see lib/common/postproc.c:map_edge ("lost %s %s edge")
230
230
  if (e.info.spl === undefined) {
231
231
  if (!(g.info.concentrate ?? false) && (e.info.edge_type ?? 0) !== IGNORED_EDGE) {
232
- agerrLineOn('warn', `lost ${e.tail.name} ${e.head.name} edge`);
232
+ agerrorf(`lost ${e.tail.name} ${e.head.name} edge`);
233
233
  }
234
234
  continue;
235
235
  }
@@ -16,7 +16,7 @@ import type { Edge } from '../model/edge.js';
16
16
  import type { Node } from '../model/node.js';
17
17
  import { approxEqPt, bezierClip, updateBbBz, InsideContext } from './splines-geom.js';
18
18
  import { MILLIPOINT, ARR_NONE } from './splines-constants.js';
19
- import { arrowClipLength } from '../layout/dot/edge-route-clip.js';
19
+ import { arrowClipLength, distFused } from '../layout/dot/edge-route-clip.js';
20
20
  import {
21
21
  arrowDrawOpsForEnd, edgeArrowName, edgeArrowsize,
22
22
  } from '../layout/dot/edge-route-arrow.js';
@@ -25,6 +25,14 @@ import { resolvePenWidth, parseStyleFlags } from './style-resolve.js';
25
25
  /** Normal (filled triangle) arrowhead. @see lib/common/const.h:ARR_TYPE_NORM */
26
26
  export const ARR_NORM = 1;
27
27
 
28
+ /**
29
+ * DIST(p, q) as the oracle compiles it: sqrt(fma(dx, dx, dy * dy)), not
30
+ * Math.hypot (libgvc 16.1 `_arrowEndClip`/`_arrowStartClip`, incl. the inlined
31
+ * sphere_ctx inside(), and `_arrowOrthoClip`).
32
+ * @see lib/common/geom.h:DIST
33
+ */
34
+ export const dist: (p: Point, q: Point) => number = distFused;
35
+
28
36
  // Internal shape accessor type for host objects that may carry a shape.
29
37
  type ShapeHost = { shape?: { fns?: { insidefn?: ((c: InsideContext, p: Point) => boolean) | null } | null } | null };
30
38
 
@@ -123,11 +131,6 @@ class SplineClipHelper {
123
131
  return [sflag, eflag];
124
132
  }
125
133
 
126
- /** DIST(p, q). */
127
- static dist(p: Point, q: Point): number {
128
- return Math.hypot(p.x - q.x, p.y - q.y);
129
- }
130
-
131
134
  /** Sphere inside-fn around `tip` with radius `r` for bezierClip. */
132
135
  static sphereCtx(tip: Point, r: number): {
133
136
  ctx: InsideContext;
@@ -135,7 +138,7 @@ class SplineClipHelper {
135
138
  } {
136
139
  return {
137
140
  ctx: { nodeCoord: { x: 0, y: 0 }, rw: 0, bp: null },
138
- fn: (_c: InsideContext, p: Point) => SplineClipHelper.dist(p, tip) <= r,
141
+ fn: (_c: InsideContext, p: Point) => dist(p, tip) <= r,
139
142
  };
140
143
  }
141
144
 
@@ -170,7 +173,7 @@ class SplineClipHelper {
170
173
  SplineClipHelper.clipArrowsize(e), SplineClipHelper.clipPenwidth(e));
171
174
  spl.eflag = eflag;
172
175
  spl.ep = ps[endp + 3]!;
173
- if (endp > startp && SplineClipHelper.dist(ps[endp]!, ps[endp + 3]!) < elen) {
176
+ if (endp > startp && dist(ps[endp]!, ps[endp + 3]!) < elen) {
174
177
  endp -= 3;
175
178
  }
176
179
  const sp: Point[] = [spl.ep, ps[endp + 2]!, ps[endp + 1]!, ps[endp]!];
@@ -198,7 +201,7 @@ class SplineClipHelper {
198
201
  SplineClipHelper.clipArrowsize(e), SplineClipHelper.clipPenwidth(e));
199
202
  spl.sflag = sflag;
200
203
  spl.sp = ps[startp]!;
201
- if (endp > startp && SplineClipHelper.dist(ps[startp]!, ps[startp + 3]!) < slen) {
204
+ if (endp > startp && dist(ps[startp]!, ps[startp + 3]!) < slen) {
202
205
  startp += 3;
203
206
  }
204
207
  const sp: Point[] = [ps[startp + 3]!, ps[startp + 2]!, ps[startp + 1]!, spl.sp];
@@ -293,7 +296,7 @@ class SplineClipHelper {
293
296
  if (sflag && eflag && endp === startp) {
294
297
  // two arrows on one segment
295
298
  const p = ps[endp]!; const q = ps[endp + 3]!;
296
- const d = SplineClipHelper.dist(p, q);
299
+ const d = dist(p, q);
297
300
  let hlen = hlen0; let tlen = tlen0;
298
301
  if (hlen + tlen >= d) { hlen = tlen = d / 3.0; }
299
302
  const s = { x: p.x, y: p.y }; const t = { x: q.x, y: q.y };
@@ -315,7 +318,7 @@ class SplineClipHelper {
315
318
  if (eflag) {
316
319
  let hlen = hlen0;
317
320
  const p = ps[endp]!; const q = ps[endp + 3]!;
318
- const maxd = 0.9 * SplineClipHelper.dist(p, q);
321
+ const maxd = 0.9 * dist(p, q);
319
322
  if (hlen >= maxd) hlen = maxd;
320
323
  const r = { x: p.x, y: p.y };
321
324
  if (p.y === q.y) { r.y = p.y; r.x = p.x < q.x ? q.x - hlen : q.x + hlen; }
@@ -326,7 +329,7 @@ class SplineClipHelper {
326
329
  if (sflag) {
327
330
  let tlen = tlen0;
328
331
  const p = ps[startp]!; const q = ps[startp + 3]!;
329
- const maxd = 0.9 * SplineClipHelper.dist(p, q);
332
+ const maxd = 0.9 * dist(p, q);
330
333
  if (tlen >= maxd) tlen = maxd;
331
334
  const r = { x: p.x, y: p.y };
332
335
  if (p.y === q.y) { r.y = p.y; r.x = p.x < q.x ? p.x + tlen : p.x - tlen; }
@@ -15,7 +15,7 @@ import { shortestPath, routeSpline, makePolyline } from '../pathplan/index.js';
15
15
  import type { Poly } from '../pathplan/types.js';
16
16
  import { INIT_DELTA, LOOP_TRIES, ROUTESPL_FUDGE } from './splines-constants.js';
17
17
  import { fma } from './fma.js';
18
- import { agerrLineOn } from './agerr.js';
18
+ import { agerrorf } from './agerr.js';
19
19
 
20
20
  // ---------------------------------------------------------------------------
21
21
  // Stage-dump hook (diagnosis only; null in production — same pattern as
@@ -451,7 +451,7 @@ function routeSplinesInternal(pp: Path, polyline: boolean): Point[] | null {
451
451
  if (pl === null) {
452
452
  // C: agerrorf("in routesplines, Pshortestpath failed") — the caller then
453
453
  // loses the edge. @see lib/common/routespl.c (Pshortestpath < 0 branch)
454
- agerrLineOn('warn', 'in routesplines, Pshortestpath failed');
454
+ agerrorf('in routesplines, Pshortestpath failed');
455
455
  return null;
456
456
  }
457
457
  emitDump('S3_POLY', dumpNames, () => ({
@@ -49,7 +49,7 @@ export function getFamilyMetrics(fontName: string): FontFamilyData {
49
49
  if (!warnedFontNames.has(fontName)) {
50
50
  warnedFontNames.add(fontName);
51
51
  agwarningf(
52
- `no hard-coded metrics for '${fontName}'. ` +
52
+ `Warning: no hard-coded metrics for '${fontName}'. ` +
53
53
  `Falling back to 'Times' metrics`,
54
54
  );
55
55
  }
@@ -26,6 +26,7 @@ import type { DebugOptions } from '../debug.js';
26
26
  import type { ImageSizer } from '../common/htmltable-types.js';
27
27
  import type { ImageResolver } from './image-resolver.js';
28
28
  import type { RenderJob } from './job.js'; // scaffold in T25; full class in T26
29
+ import { resetRenderState } from '../common/render-state.js';
29
30
  import { RenderError, invalidArgType, invalidArgValue } from '../errors.js';
30
31
 
31
32
  // ---------------------------------------------------------------------------
@@ -285,6 +286,12 @@ export class GvcContext {
285
286
  * rendering happens in between and must see the layout state
286
287
  * (e.g. cluster arrays).
287
288
  *
289
+ * Contract (AD-4/AD-8): first resets the C process state (RNG streams,
290
+ * sequence counters, once-only warnings), so each `layout` call behaves
291
+ * as one native run — drive one graph per `layout`. Caller-installed hooks
292
+ * (setTextMeasurer, setImageSizer, setImageResolver, the per-context
293
+ * measurer/sizer/resolver) are configuration and are not reset.
294
+ *
288
295
  * @throws TypeError (ERR_INVALID_ARG_TYPE / ERR_INVALID_ARG_VALUE) for a
289
296
  * bad `g`, or an engine argument that is not registered
290
297
  * @throws RenderError (UNKNOWN_LAYOUT) if the `layout` attribute names no
@@ -295,6 +302,7 @@ export class GvcContext {
295
302
  * @see lib/gvc/gvlayout.c:gvLayoutJobs
296
303
  */
297
304
  layout(g: Graph, engineName: EngineName): void {
305
+ resetRenderState(); // AD-8: idempotent under the public entries' own reset
298
306
  const selected = this.checkLayoutArgs(g, engineName);
299
307
  // C gvLayoutJobs: the graph's `layout` ATTRIBUTE unconditionally
300
308
  // overrides the selected engine (-K / API choice); an unrecognized
@@ -6,13 +6,17 @@
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';
19
+ import { gvrenderSetStyle, checkClusterStyleTokens } from './gvrender-set-style.js';
16
20
  import { resolveRenderColor, withColorScheme, reportColorNames } from '../render/color-resolve.js';
17
21
  import { parseSegs } from '../common/multicolor.js';
18
22
 
@@ -100,6 +104,9 @@ export function applyClusterObjState(sg: Graph, job: RenderJob): boolean {
100
104
  const fillRes = resolveClusterFillEx(clusterAttrsOf(sg));
101
105
  if (job.obj === null) return false;
102
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))));
103
110
  // C wraps the cluster color block with setColorScheme. @see emit.c:3800/3943
104
111
  return withColorScheme(clusterAttr(sg, 'colorscheme'), () => {
105
112
  // C resolves the fill (set_fillcolor / gradient vals) before the pen,
@@ -112,23 +119,71 @@ export function applyClusterObjState(sg: Graph, job: RenderJob): boolean {
112
119
  }
113
120
 
114
121
  /**
115
- * The colours stripedBox sets for a `style=striped` cluster (not rounded): each
116
- * segment of the fill spec with a positive width, after the pen.
117
- * @see lib/common/emit.c:3896-3906 emit_clusters (striped branch)
118
- * @see lib/common/emit.c:595 stripedBox
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)
119
125
  */
120
- function stripedClusterColors(sg: Graph): string[] {
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 {
121
136
  const flags = parseStyleFlags(clusterStyle(sg));
122
- if (!flags.striped || flags.rounded) return [];
123
137
  const color = clusterAttr(sg, 'color') ?? '';
124
138
  let fill = clusterAttr(sg, 'fillcolor') || color;
125
139
  const bg = clusterAttr(sg, 'bgcolor') ?? '';
126
140
  if ((!flags.filled || fill === '') && bg !== '') fill = bg;
127
- const { segs, error } = parseSegs(fill === '' ? 'lightgrey' : fill);
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}`);
128
153
  if (error === 1 || error === 2) return [];
129
154
  return segs.flatMap((seg) => (seg.color !== null && seg.t > 0 ? [seg.color] : []));
130
155
  }
131
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
+
132
187
  /** The fill half of {@link applyClusterObjState}; true when the box is filled. */
133
188
  function applyClusterFill(obj: ObjState, fillRes: ResolvedFill): boolean {
134
189
  if (fillRes.kind === 'none') { obj.fill = FillType.None; return false; }
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,
@@ -47,8 +48,9 @@ import {
47
48
  setRendererKnownColors,
48
49
  } from '../render/color-resolve.js';
49
50
  import { emitRoundedBezier } from '../common/poly-shapes.js';
50
- import { applyClusterObjState, clusterStyle, clusterPeripheries } from './device-cluster.js';
51
+ import { applyClusterObjState, clusterStyle, clusterPeripheries, isStripedCluster, renderStripedClusterBox } from './device-cluster.js';
51
52
  import { transformPoint } from './device-transform.js';
53
+ import { gvrenderSetStyle, edgeStyleResets } from './gvrender-set-style.js';
52
54
  import { renderOneLabel, renderNodeXLabel, renderClusterLabel, reportEdgeLabelColors } from './device-labels.js';
53
55
  export { transformPoint, applyRotation, applyScale, buildPoint } from './device-transform.js';
54
56
  export { renderOneLabel, renderNodeXLabel, renderClusterLabel } from './device-labels.js';
@@ -165,6 +167,9 @@ function setEdgePen(e: Edge, job: RenderJob): void {
165
167
  // Raw style tokens for xdot's `S` ops (e.g. explicit style="solid"); SVG
166
168
  // reads only obj.pen, so this is xdot-only. @see gvrender_core_dot.c xdot_style
167
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);
168
173
  }
169
174
 
170
175
  export function renderEdge(e: Edge, renderer: RendererPlugin, job: RenderJob): void {
@@ -195,6 +200,10 @@ export function renderEdge(e: Edge, renderer: RendererPlugin, job: RenderJob): v
195
200
  // emit_edge_graphics, then emit_end_edge's labels: C resolves these names
196
201
  // here whatever the renderer draws (the non-drawing ones included).
197
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);
198
207
  reportEdgeLabelColors(e);
199
208
  renderer.endEdge(e, job);
200
209
  });
@@ -230,6 +239,10 @@ function renderClusterBox(
230
239
  sg: Graph, filled: boolean, renderer: RendererPlugin, job: RenderJob,
231
240
  ): void {
232
241
  const { ll, ur } = sg.info.bb!;
242
+ if (isStripedCluster(sg)) {
243
+ renderStripedClusterBox(sg, renderer, job);
244
+ return;
245
+ }
233
246
  if (parseStyleFlags(clusterStyle(sg)).rounded) {
234
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 }];
235
248
  emitRoundedBezier(af, { x: 0, y: 0 }, filled, { renderer, job });
@@ -280,7 +293,7 @@ function renderOneCluster(sg: Graph, renderer: RendererPlugin, job: RenderJob):
280
293
  // style=invis sets PEN_NONE and gvrender_polygon skips the whole box
281
294
  // (fill included); the group/title still emit.
282
295
  // @see lib/common/emit.c:3907-3917, lib/gvc/gvrender.c:543
283
- if ((clusterPeripheries(sg) !== 0 || filled) && job.obj?.pen !== PenType.None) {
296
+ if ((clusterPeripheries(sg) !== 0 || filled || isStripedCluster(sg)) && job.obj?.pen !== PenType.None) {
284
297
  renderClusterBox(sg, filled, renderer, job);
285
298
  }
286
299
  renderClusterLabel(sg, renderer, job);
@@ -334,6 +347,7 @@ export function renderGraph(g: Graph, job: RenderJob, renderer: RendererPlugin):
334
347
  job.numLayers = layered ? info.numLayers : 1;
335
348
  job.layerIDs = info.layerIDs;
336
349
  renderer.beginGraph(g, job);
350
+ warnLayersUnsupported(job.format, info.numLayers, renderer.beginLayer !== undefined);
337
351
  for (let ln = 1; ln <= job.numLayers; ln++) {
338
352
  job.layerNum = ln;
339
353
  if (layered) renderer.beginLayer!(info.layerIDs[ln]!, job);