@knowvah/dot-engine 2.6.2 → 2.6.4

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 (46) hide show
  1. package/dist/common/htmltable-lex.d.ts +38 -0
  2. package/dist/common/htmltable-lex.d.ts.map +1 -1
  3. package/dist/common/htmltable-parse-attrs.d.ts +41 -0
  4. package/dist/common/htmltable-parse-attrs.d.ts.map +1 -0
  5. package/dist/common/htmltable-parse.d.ts +3 -0
  6. package/dist/common/htmltable-parse.d.ts.map +1 -1
  7. package/dist/common/htmltable-pos.d.ts +17 -0
  8. package/dist/common/htmltable-pos.d.ts.map +1 -1
  9. package/dist/common/htmltable.d.ts +13 -1
  10. package/dist/common/htmltable.d.ts.map +1 -1
  11. package/dist/common/make-label.d.ts.map +1 -1
  12. package/dist/common/spline-midpoint.d.ts +8 -0
  13. package/dist/common/spline-midpoint.d.ts.map +1 -1
  14. package/dist/index.js +621 -323
  15. package/dist/index.js.map +4 -4
  16. package/dist/label/node.d.ts +7 -1
  17. package/dist/label/node.d.ts.map +1 -1
  18. package/dist/layout/circo/position-helpers.d.ts +15 -3
  19. package/dist/layout/circo/position-helpers.d.ts.map +1 -1
  20. package/dist/layout/circo/position.d.ts +17 -1
  21. package/dist/layout/circo/position.d.ts.map +1 -1
  22. package/dist/layout/dot/ortho-adapter.d.ts.map +1 -1
  23. package/dist/layout/dot/splines-flat-labeled.d.ts.map +1 -1
  24. package/dist/layout/neato/multispline.d.ts.map +1 -1
  25. package/dist/layout/pack/poly-pack.d.ts +2 -18
  26. package/dist/layout/pack/poly-pack.d.ts.map +1 -1
  27. package/dist/pathplan/shortest.d.ts.map +1 -1
  28. package/dist/render.js +774 -476
  29. package/dist/render.js.map +4 -4
  30. package/package.json +1 -1
  31. package/src/common/htmltable-lex.ts +265 -5
  32. package/src/common/htmltable-parse-attrs.ts +166 -0
  33. package/src/common/htmltable-parse.ts +262 -242
  34. package/src/common/htmltable-pos.ts +37 -38
  35. package/src/common/htmltable.ts +139 -45
  36. package/src/common/make-label.ts +19 -3
  37. package/src/common/spline-midpoint.ts +30 -9
  38. package/src/label/node.ts +8 -2
  39. package/src/layout/circo/position-helpers.ts +28 -17
  40. package/src/layout/circo/position.ts +28 -7
  41. package/src/layout/dot/__fixtures__/2796-flat-labeled-lost.dot +161 -0
  42. package/src/layout/dot/ortho-adapter.ts +20 -9
  43. package/src/layout/dot/splines-flat-labeled.ts +3 -1
  44. package/src/layout/neato/multispline.ts +17 -5
  45. package/src/layout/pack/poly-pack.ts +5 -31
  46. package/src/pathplan/shortest.ts +15 -2
@@ -19,8 +19,11 @@ import type { Box } from '../model/geom.js';
19
19
  import type { TextlabelT } from './types.js';
20
20
  import type { TextMeasurer } from './textmeasure.js';
21
21
  import type { HtmlCell, HtmlImage, HtmlLabel, HtmlTable, HtmlText, ImageSizer } from './htmltable-types.js';
22
- import { layoutHtmlTable, cellPad, cellBorder, sizeHtmlLabel, parseHtmlLabel } from './htmltable.js';
22
+ import {
23
+ layoutHtmlTable, cellPad, cellBorder, sizeHtmlLabel, parseHtmlLabel, inheritTablePenColor,
24
+ } from './htmltable.js';
23
25
  import { makeLabel } from './make-label.js';
26
+ import { HtmlLexError } from './htmltable-lex.js';
24
27
  import { TOP, BOTTOM, LEFT, RIGHT } from './splines-constants.js';
25
28
  import type { PlacedLine, HtmlFontInfo } from './htmltable-pos-runs.js';
26
29
  import {
@@ -517,57 +520,44 @@ export function posHtmlLabel(
517
520
  return posTextLabel(label, finfo, measurer);
518
521
  }
519
522
 
523
+ /** parseHTML's rv for a label that failed to parse (`ctx->error`). */
524
+ const HTML_PARSE_FATAL = 3;
525
+
526
+ /** A built HTML label plus make_html_label's "problem" return value. */
527
+ export interface HtmlLabelResult {
528
+ label: TextlabelT;
529
+ rv: number;
530
+ }
531
+
520
532
  /**
521
- * Build a TextlabelT for an HTML label string: parse, size, position.
522
- * Falls back to a plain-text label on parse failure (C reverts to the
523
- * object name; we keep the markup text).
524
- *
525
- * The optional `imageSizer` on `finfo` resolves `<IMG SRC="..."/>` dims.
526
- * Absent → C missing-image behavior (zero size + warning).
527
- *
533
+ * make_html_label: parse, size and position an HTML-like label. Returns C's
534
+ * rv too (nonzero: the caller appends `in label of ...`, labels.c:make_label).
535
+ * Parse diagnostics print here, once per label, as parseHTML does. The
536
+ * optional `imageSizer` resolves `<IMG SRC>`; absent means C's missing image.
528
537
  * @see lib/common/htmltable.c:make_html_label
529
538
  */
530
- /**
531
- * Propagate pen (border) color down the table tree. A table with no explicit
532
- * COLOR inherits `parentColor`; each cell inherits the (possibly inherited)
533
- * table color; each nested table inherits its enclosing cell's color. Mirrors
534
- * C, which defaults the top table's pencolor to getPenColor(obj) then copies
535
- * parent->child during processTbl.
536
- * @see lib/common/htmltable.c:1911 (top table), :1406-1407 (cell), :1556-1557
537
- */
538
- function inheritTablePenColor(tbl: HtmlTable, parentColor: string | undefined): void {
539
- if (tbl.color === undefined) tbl.color = parentColor;
540
- const tblColor = tbl.color;
541
- for (const row of tbl.rows) {
542
- for (const cell of row.cells) {
543
- if (cell.color === undefined) cell.color = tblColor;
544
- for (const item of cell.content) {
545
- if (item.kind === 'table') inheritTablePenColor(item, cell.color);
546
- }
547
- }
548
- }
549
- }
550
-
551
- export function makeHtmlLabel(
539
+ export function makeHtmlLabelRv(
552
540
  content: string,
553
541
  finfo: HtmlFontInfo & { imageSizer?: ImageSizer; pencolor?: string },
554
542
  measurer: TextMeasurer,
555
- ): TextlabelT {
543
+ ): HtmlLabelResult {
556
544
  const { fontname, fontsize, fontcolor, imageSizer, pencolor } = finfo;
557
545
  let lbl: HtmlLabel;
558
546
  try {
559
547
  lbl = parseHtmlLabel(content);
560
- } catch {
561
- // C aborts the HTML parse on a syntax error (YYABORT) and leaves the label
562
- // EMPTY — it does not fall back to rendering the raw markup as plain text.
563
- // @see lib/common/htmlparse.y (cleanup + YYABORT), htmllex.c htmlerror
564
- return makeLabel('', fontname, fontsize, fontcolor, measurer);
548
+ } catch (e) {
549
+ // C aborts the HTML parse on a syntax error (YYABORT): parseHTML returns
550
+ // rv 3 (ctx->error) with no label, and make_html_label returns it with
551
+ // lp->html cleared and no spans, so the label renders empty.
552
+ // @see lib/common/htmltable.c:1880 (rv == 3), htmlparse.y (YYABORT)
553
+ if (e instanceof HtmlLexError) for (const line of e.diagnostics) console.error(line);
554
+ return { label: makeLabel('', fontname, fontsize, fontcolor, measurer), rv: HTML_PARSE_FATAL };
565
555
  }
566
556
  // A top-level table with no explicit COLOR inherits the object's pen color,
567
557
  // which then flows table->cell->nested-table.
568
558
  // @see lib/common/htmltable.c:1911 (getPenColor), :1406/:1556 (inheritance)
569
559
  if (lbl.kind === 'table') inheritTablePenColor(lbl.table, pencolor);
570
- sizeHtmlLabel(lbl, measurer, {
560
+ const rv = sizeHtmlLabel(lbl, measurer, {
571
561
  fontsize, fontname, imageSizer,
572
562
  });
573
563
  const placed = posHtmlLabel(lbl, { fontname, fontsize, fontcolor }, measurer);
@@ -576,7 +566,16 @@ export function makeHtmlLabel(
576
566
  // alt fields in image maps" — anchors inherit it as default tooltip.
577
567
  // @see lib/common/htmltable.c:make_html_label (line 1935)
578
568
  const text = lbl.kind === 'table' ? '<TABLE>' : content;
579
- return htmlTextLabel(text, { fontname, fontsize, fontcolor }, placed, dim);
569
+ return { label: htmlTextLabel(text, { fontname, fontsize, fontcolor }, placed, dim), rv };
570
+ }
571
+
572
+ /** {@link makeHtmlLabelRv} without the problem flag. */
573
+ export function makeHtmlLabel(
574
+ content: string,
575
+ finfo: HtmlFontInfo & { imageSizer?: ImageSizer; pencolor?: string },
576
+ measurer: TextMeasurer,
577
+ ): TextlabelT {
578
+ return makeHtmlLabelRv(content, finfo, measurer).label;
580
579
  }
581
580
 
582
581
  /** Build the TextlabelT wrapper around a positioned HTML label. */
@@ -24,6 +24,7 @@ import type {
24
24
  } from './htmltable-types.js';
25
25
  import type { ImageSizer } from './htmltable-types.js';
26
26
  import { findImageSize } from '../gvc/usershape.js';
27
+ import { PointSet } from './pointset.js';
27
28
  import type { TextMeasurer } from './textmeasure.js';
28
29
 
29
30
  const DEFAULT_BORDER = 1;
@@ -45,25 +46,29 @@ interface TableDimArgs {
45
46
 
46
47
  /** @see lib/common/htmltable.c:findCol */
47
48
  const findCol = (
48
- used: Set<string>,
49
+ ps: PointSet,
49
50
  row: number,
50
51
  startCol: number,
51
52
  colspan: number,
52
53
  rowspan: number,
53
54
  ): number => {
54
55
  let col = startCol;
55
- for (;;) {
56
- let conflict = false;
57
- for (let c = col; c < col + colspan && !conflict; c++) {
58
- for (let r = row; r < row + rowspan && !conflict; r++) {
59
- if (used.has(`${c},${r}`)) { conflict = true; col = c + 1; }
60
- }
56
+ let notFound = true;
57
+ const end = colspan - 1;
58
+ while (notFound) {
59
+ const lastc = col + end;
60
+ let c: number;
61
+ for (c = lastc; c >= col; c--) {
62
+ if (ps.isInPS(c, row)) break;
63
+ }
64
+ if (c >= col) col = c + 1; // conflict: try column after
65
+ else notFound = false;
66
+ }
67
+ for (let j = col; j < col + colspan; j++) {
68
+ for (let i = row; i < row + rowspan; i++) {
69
+ ps.addPS(j, i);
61
70
  }
62
- if (!conflict) break;
63
71
  }
64
- for (let c = col; c < col + colspan; c++)
65
- for (let r = row; r < row + rowspan; r++)
66
- used.add(`${c},${r}`);
67
72
  return col;
68
73
  };
69
74
 
@@ -219,15 +224,19 @@ const sizeTextContent = (
219
224
  *
220
225
  * @see lib/common/htmltable.c:size_html_img (line 1080)
221
226
  */
222
- const sizeHtmlImg = (img: HtmlImage, imageSizer?: ImageSizer): void => {
227
+ /** Images whose sizing failed: size_html_img's rv = 1 on every later call. */
228
+ const missingImages = new WeakSet<HtmlImage>();
229
+
230
+ const sizeHtmlImg = (img: HtmlImage, imageSizer?: ImageSizer): number => {
223
231
  // C sizes each image once (processTbl); the port's size and pos passes
224
- // share layoutHtmlTable, so guard against re-sizing (and re-warning).
225
- if (img.width !== undefined && img.height !== undefined) return;
232
+ // share layoutHtmlTable, so guard against re-sizing (and re-warning). The
233
+ // returned rv still follows C on every call.
234
+ if (img.width !== undefined && img.height !== undefined) return missingImages.has(img) ? 1 : 0;
226
235
  const sz = imageSizer !== undefined ? imageSizer(img.src) : findImageSize(img.src);
227
236
  if (sz !== null) {
228
237
  img.width = sz.w;
229
238
  img.height = sz.h;
230
- return;
239
+ return 0;
231
240
  }
232
241
  // Missing-image: zero size + warning.
233
242
  // C: agerrorf("No or improper image file=\"%s\"\n", img->src) — agerr
@@ -235,23 +244,25 @@ const sizeHtmlImg = (img: HtmlImage, imageSizer?: ImageSizer): void => {
235
244
  console.error(`Error: No or improper image file="${img.src}"`);
236
245
  img.width = 0;
237
246
  img.height = 0;
247
+ missingImages.add(img);
248
+ return 1;
238
249
  };
239
250
 
240
251
  const sizeContentItem = (
241
252
  item: HtmlCellContent,
242
253
  measurer: TextMeasurer,
243
254
  env: HtmlSizeEnv,
244
- ): { w: number; h: number } => {
245
- if (item.kind === 'text') return sizeTextContent([item], measurer, env);
255
+ ): { w: number; h: number; rv: number } => {
256
+ if (item.kind === 'text') return { ...sizeTextContent([item], measurer, env), rv: 0 };
246
257
  if (item.kind === 'table') {
247
- sizeTableInner(item, measurer, env);
248
- return item.dimen !== undefined ? item.dimen : { w: 0, h: 0 };
258
+ const rv = sizeTableInner(item, measurer, env);
259
+ return { ...(item.dimen !== undefined ? item.dimen : { w: 0, h: 0 }), rv };
249
260
  }
250
261
  if (item.kind === 'image') {
251
- sizeHtmlImg(item, env.imageSizer);
252
- return { w: item.width ?? 0, h: item.height ?? 0 };
262
+ const rv = sizeHtmlImg(item, env.imageSizer);
263
+ return { w: item.width ?? 0, h: item.height ?? 0, rv };
253
264
  }
254
- return { w: 0, h: 0 };
265
+ return { w: 0, h: 0, rv: 0 };
255
266
  };
256
267
 
257
268
  export const cellPad = (cell: HtmlCell, tbl: HtmlTable): number =>
@@ -273,32 +284,85 @@ export const cellBorder = (cell: HtmlCell, tbl: HtmlTable): number => {
273
284
  return DEFAULT_BORDER;
274
285
  };
275
286
 
287
+ /**
288
+ * Propagate pen (border) color down the table tree. A table with no explicit
289
+ * COLOR inherits `parentColor`; each cell inherits the (possibly inherited)
290
+ * table color; each nested table inherits its enclosing cell's color. Mirrors
291
+ * C, which defaults the top table's pencolor to getPenColor(obj) then copies
292
+ * parent->child during processTbl.
293
+ * @see lib/common/htmltable.c:1911 (top table), :1406-1407 (cell), :1556-1557
294
+ */
295
+ export function inheritTablePenColor(tbl: HtmlTable, parentColor: string | undefined): void {
296
+ if (tbl.color === undefined) tbl.color = parentColor;
297
+ const tblColor = tbl.color;
298
+ for (const row of tbl.rows) {
299
+ for (const cell of row.cells) {
300
+ if (cell.color === undefined) cell.color = tblColor;
301
+ for (const item of cell.content) {
302
+ if (item.kind === 'table') inheritTablePenColor(item, cell.color);
303
+ }
304
+ }
305
+ }
306
+ }
307
+
308
+ /** C `agwarningf` text as rendered on stderr ("Warning: " prefix, no newline). */
309
+ const WARN_CELL_TOO_SMALL = 'Warning: cell size too small for content';
310
+ const WARN_CELL_UNSPECIFIED = 'Warning: fixed cell size with unspecified width or height';
311
+ const WARN_TABLE_TOO_SMALL = 'Warning: table size too small for content';
312
+ const WARN_TABLE_UNSPECIFIED = 'Warning: fixed table size with unspecified width or height';
313
+
314
+ /**
315
+ * C sizes each cell/table exactly once; the port's size and pos passes both
316
+ * run layoutHtmlTable, so the size warnings are emitted on first sight only.
317
+ */
318
+ const sizeWarned = new WeakSet<object>();
319
+
320
+ /**
321
+ * FIXED_FLAG branch of size_html_cell: C's rv (set at the warning sites; the
322
+ * warning itself is printed once per cell) and whether the content size is
323
+ * zeroed.
324
+ * @see lib/common/htmltable.c:1141 (size_html_cell FIXED_FLAG)
325
+ */
326
+ const fixedCellCheck = (
327
+ cell: HtmlCell, sz: { w: number; h: number }, warn: boolean,
328
+ ): { rv: number; zero: boolean } => {
329
+ if (!cell.fixedsize) return { rv: 0, zero: false };
330
+ if (cell.width && cell.height) {
331
+ const small = (cell.width < sz.w || cell.height < sz.h) && cell.content[0]?.kind !== 'image';
332
+ if (small && warn) console.warn(WARN_CELL_TOO_SMALL);
333
+ return { rv: small ? 1 : 0, zero: true };
334
+ }
335
+ if (warn) console.warn(WARN_CELL_UNSPECIFIED);
336
+ return { rv: 1, zero: false };
337
+ };
338
+
276
339
  /** @see lib/common/htmltable.c:size_html_cell */
277
340
  const getCellSize = (
278
341
  cell: HtmlCell,
279
342
  tbl: HtmlTable,
280
343
  measurer: TextMeasurer,
281
344
  env: HtmlSizeEnv,
282
- ): { w: number; h: number } => {
345
+ ): { w: number; h: number; rv: number } => {
283
346
  const margin = 2 * (cellPad(cell, tbl) + cellBorder(cell, tbl));
284
- let cw = 0, ch = 0;
347
+ let cw = 0, ch = 0, rv = 0;
285
348
  for (const item of cell.content) {
286
349
  const s = sizeContentItem(item, measurer, env);
287
- cw = Math.max(cw, s.w); ch += s.h;
350
+ cw = Math.max(cw, s.w); ch += s.h; rv |= s.rv;
288
351
  }
289
352
  // C: sz = content + margin; FIXED_FLAG with both width and height set zeroes
290
353
  // sz (content size is ignored), then the box clamps UP to width/height via
291
354
  // MAX below — so a fixed cell ends up exactly width x height, NOT 0 x 0.
292
- // (C also warns "cell size too small for content" / "unspecified"; non-visual.)
293
355
  // @see lib/common/htmltable.c:size_html_cell (FIXED_FLAG branch + box.UR MAX)
294
356
  let szx = cw + margin;
295
357
  let szy = ch + margin;
296
- if (cell.fixedsize && cell.width && cell.height) {
297
- szx = 0; szy = 0;
298
- }
358
+ const warn = !sizeWarned.has(cell);
359
+ sizeWarned.add(cell);
360
+ const fixed = fixedCellCheck(cell, { w: szx, h: szy }, warn);
361
+ rv |= fixed.rv;
362
+ if (fixed.zero) { szx = 0; szy = 0; }
299
363
  const bw = cell.width !== undefined ? cell.width : 0;
300
364
  const bh = cell.height !== undefined ? cell.height : 0;
301
- return { w: Math.max(szx, bw), h: Math.max(szy, bh) };
365
+ return { w: Math.max(szx, bw), h: Math.max(szy, bh), rv };
302
366
  };
303
367
 
304
368
  /** @see lib/common/htmltable.c:processTbl */
@@ -306,9 +370,10 @@ const buildLayouts = (
306
370
  tbl: HtmlTable,
307
371
  measurer: TextMeasurer,
308
372
  env: HtmlSizeEnv,
309
- ): CellEntry[] => {
373
+ ): { entries: CellEntry[]; rv: number } => {
310
374
  const entries: CellEntry[] = [];
311
- const used = new Set<string>();
375
+ let rv = 0;
376
+ const ps = new PointSet();
312
377
  let rowIdx = 0;
313
378
  for (const row of tbl.rows) {
314
379
  let col = 0;
@@ -316,14 +381,15 @@ const buildLayouts = (
316
381
  if (c.kind !== 'cell') { continue; }
317
382
  const colspan = c.colspan !== undefined ? c.colspan : 1;
318
383
  const rowspan = c.rowspan !== undefined ? c.rowspan : 1;
319
- const { w, h } = getCellSize(c, tbl, measurer, env);
320
- col = findCol(used, rowIdx, col, colspan, rowspan);
384
+ const { w, h, rv: cellRv } = getCellSize(c, tbl, measurer, env);
385
+ rv |= cellRv;
386
+ col = findCol(ps, rowIdx, col, colspan, rowspan);
321
387
  entries.push({ cell: c, row: rowIdx, col, colspan, rowspan, w, h });
322
388
  col += colspan;
323
389
  }
324
390
  rowIdx++;
325
391
  }
326
- return entries;
392
+ return { entries, rv };
327
393
  };
328
394
 
329
395
  /** @see lib/common/htmltable.c:set_cell_widths */
@@ -403,6 +469,8 @@ export interface HtmlTableLayout {
403
469
  heights: number[];
404
470
  spacing: number;
405
471
  border: number;
472
+ /** processTbl's return: nonzero when sizing any cell had a problem. */
473
+ rv: number;
406
474
  }
407
475
 
408
476
  /**
@@ -417,15 +485,33 @@ export const layoutHtmlTable = (
417
485
  ): HtmlTableLayout => {
418
486
  const spacing = getSpacing(tbl);
419
487
  const border = getBorder(tbl);
420
- const entries = buildLayouts(tbl, measurer, env);
488
+ const { entries, rv } = buildLayouts(tbl, measurer, env);
421
489
  if (entries.length === 0) {
422
- return { entries, widths: [], heights: [], spacing, border };
490
+ return { entries, widths: [], heights: [], spacing, border, rv };
423
491
  }
424
492
  const ncols = Math.max(...entries.map(e => e.col + e.colspan));
425
493
  const nrows = Math.max(...entries.map(e => e.row + e.rowspan));
426
494
  const widths = setColWidths(entries, ncols, spacing);
427
495
  const heights = setRowHeights(entries, nrows, spacing);
428
- return { entries, widths, heights, spacing, border };
496
+ return { entries, widths, heights, spacing, border, rv };
497
+ };
498
+
499
+ /**
500
+ * FIXED_FLAG diagnostics of size_html_tbl: the warning is emitted once per
501
+ * table, the returned `rv = 1` follows C on every call.
502
+ * @see lib/common/htmltable.c:size_html_tbl (:1683, :1689)
503
+ */
504
+ const warnFixedTable = (tbl: HtmlTable, dims: { wd: number; ht: number }): number => {
505
+ const warn = !sizeWarned.has(tbl);
506
+ sizeWarned.add(tbl);
507
+ if (tbl.fixedsize !== true) return 0;
508
+ if (tbl.width && tbl.height) {
509
+ if (tbl.width >= dims.wd && tbl.height >= dims.ht) return 0;
510
+ if (warn) console.warn(WARN_TABLE_TOO_SMALL);
511
+ return 1;
512
+ }
513
+ if (warn) console.warn(WARN_TABLE_UNSPECIFIED);
514
+ return 1;
429
515
  };
430
516
 
431
517
  /** @see lib/common/htmltable.c:size_html_tbl */
@@ -433,15 +519,20 @@ const sizeTableInner = (
433
519
  tbl: HtmlTable,
434
520
  measurer: TextMeasurer,
435
521
  env: HtmlSizeEnv,
436
- ): void => {
437
- const { entries, widths, heights, spacing, border } = layoutHtmlTable(tbl, measurer, env);
438
- if (entries.length === 0) { tbl.dimen = { w: 0, h: 0 }; return; }
522
+ ): number => {
523
+ const { entries, widths, heights, spacing, border, rv } = layoutHtmlTable(tbl, measurer, env);
524
+ if (entries.length === 0) {
525
+ const fixed = warnFixedTable(tbl, sumTableDims({ widths, heights, ncols: 0, nrows: 0, spacing, border }));
526
+ tbl.dimen = { w: 0, h: 0 };
527
+ return rv | fixed;
528
+ }
439
529
  const ncols = Math.max(...entries.map(e => e.col + e.colspan));
440
530
  const nrows = Math.max(...entries.map(e => e.row + e.rowspan));
441
531
  for (const e of entries) e.cell.dimen = { w: e.w, h: e.h };
442
532
  const { wd, ht } = sumTableDims({ widths, heights, ncols, nrows, spacing, border });
443
533
  const baseWd = tbl.width !== undefined ? tbl.width : 0;
444
534
  const baseHt = tbl.height !== undefined ? tbl.height : 0;
535
+ const fixed = warnFixedTable(tbl, { wd, ht });
445
536
  // C zeroes only the CONTENT-derived term for a FIXEDSIZE table that has BOTH
446
537
  // width and height, so the box becomes fmax(0, width)=width / fmax(0,height)=
447
538
  // height (the explicit size). A fixed table missing a dimension warns and
@@ -452,6 +543,7 @@ const sizeTableInner = (
452
543
  w: Math.max(zeroContent ? 0 : wd, baseWd),
453
544
  h: Math.max(zeroContent ? 0 : ht, baseHt),
454
545
  };
546
+ return rv | fixed;
455
547
  };
456
548
 
457
549
  /**
@@ -461,17 +553,19 @@ const sizeTableInner = (
461
553
  * When absent or when the sizer returns null, images get zero size and a
462
554
  * warning is emitted, matching C's missing-image behavior exactly.
463
555
  *
556
+ * @returns make_html_label's `rv |= size_html_*`: nonzero on a sizing problem.
464
557
  * @see lib/common/htmltable.c:make_html_label
465
558
  */
466
559
  export const sizeHtmlLabel = (
467
560
  label: HtmlLabel,
468
561
  measurer: TextMeasurer,
469
562
  env: HtmlSizeEnv = DEFAULT_SIZE_ENV,
470
- ): void => {
563
+ ): number => {
471
564
  if (label.kind === 'table') {
472
- sizeTableInner(label.table, measurer, env);
565
+ const rv = sizeTableInner(label.table, measurer, env);
473
566
  label.dimen = label.table.dimen;
474
- } else {
475
- label.dimen = sizeTextContent(label.texts, measurer, env);
567
+ return rv;
476
568
  }
569
+ label.dimen = sizeTextContent(label.texts, measurer, env);
570
+ return 0; // size_html_txt always returns 0
477
571
  };
@@ -12,10 +12,10 @@
12
12
  import type { TextlabelT } from './types.js';
13
13
  import type { TextMeasurer, TextSize } from './textmeasure.js';
14
14
  import type { TextSpan } from './emit-types.js';
15
- import { makeHtmlLabel } from './htmltable-pos.js';
15
+ import { makeHtmlLabelRv } from './htmltable-pos.js';
16
16
  import { substObj, type GraphObj } from './subst.js';
17
17
  import { Edge } from '../model/edge.js';
18
- import type { Graph } from '../model/graph.js';
18
+ import { Graph } from '../model/graph.js';
19
19
  import { htmlEntityUTF8 } from './html-entities.js';
20
20
 
21
21
  export const DEFAULT_FONTSIZE = 14.0;
@@ -48,6 +48,20 @@ function rootOf(obj: GraphObj): Graph {
48
48
  return obj instanceof Edge ? obj.tail.root : obj.root;
49
49
  }
50
50
 
51
+ /**
52
+ * The `agerr(AGPREV, ...)` continuation make_label prints when
53
+ * make_html_label reports a problem (no `Error:`/`Warning:` prefix).
54
+ * @see lib/common/labels.c:make_label (148-163)
55
+ */
56
+ function inLabelOf(obj: GraphObj): string {
57
+ if (obj instanceof Edge) {
58
+ const sep = obj.head.root.kind.endsWith('undirected') ? '--' : '->';
59
+ return `in label of edge ${obj.tail.name} ${sep} ${obj.head.name}`;
60
+ }
61
+ if (obj instanceof Graph) return `in label of graph ${obj.name}`;
62
+ return `in label of node ${obj.name}`;
63
+ }
64
+
51
65
  /** Font attributes bundle — mirrors C fontinfo_t fields used in label init. */
52
66
  export interface FontInfo {
53
67
  fontname: string;
@@ -191,7 +205,9 @@ export function makeAnyLabel(
191
205
  // it (htmltable.c:1911 getPenColor). @see getPenColor
192
206
  // ADR-2: a per-context sizer (when set) wins over the global one.
193
207
  const imageSizer = obj !== undefined ? rootOf(obj).info.gvc?.imageSizer : undefined;
194
- return makeHtmlLabel(content, { ...font, pencolor: getPenColor(obj), imageSizer }, measurer);
208
+ const { label, rv } = makeHtmlLabelRv(content, { ...font, pencolor: getPenColor(obj), imageSizer }, measurer);
209
+ if (rv !== 0 && obj !== undefined) console.error(inLabelOf(obj));
210
+ return label;
195
211
  }
196
212
  // Plain path: resolve \G \N \E \T \H \L against the owning object
197
213
  // BEFORE measuring, as C does (labels.c:169, escBackslash=0; the
@@ -14,20 +14,41 @@ import { fma } from './fma.js';
14
14
  type BezLike = { list: Point[]; sflag: number; eflag: number; sp: Point; ep: Point; size?: number };
15
15
  export type SplLike = { list: BezLike[] };
16
16
 
17
+ /**
18
+ * DIST2(p, q) as the oracle compiles it. clang -ffp-contract=on turns
19
+ * `dx*dx + dy*dy` into `fmul t, dy, dy; fmadd d, dx, dx, t` at every libgvc
20
+ * site the port reaches through here: dotneato_closest's scan and bisection,
21
+ * edgeMidpoint's APPROXEQPT and polylineMidpoint's DIST (libgvc 16.1
22
+ * disassembly), and resolvePort's inlined closestSide.
23
+ * @see lib/common/geom.h:DIST2
24
+ */
17
25
  export function dist2(p: Point, q: Point): number {
18
26
  const dx = p.x - q.x; const dy = p.y - q.y;
19
- return dx * dx + dy * dy;
27
+ return fma(dx, dx, dy * dy);
20
28
  }
21
29
 
30
+ /** DIST: sqrt of the fused DIST2 (`fmadd` then `fsqrt`). @see lib/common/geom.h:DIST */
22
31
  function distPt(p: Point, q: Point): number { return Math.sqrt(dist2(p, q)); }
23
32
 
24
- function evalBez4(pts: Point[], t: number): Point {
25
- const u = 1 - t;
26
- const [p0, p1, p2, p3] = pts;
27
- return {
28
- x: u*u*u*p0.x + 3*u*u*t*p1.x + 3*u*t*t*p2.x + t*t*t*p3.x,
29
- y: u*u*u*p0.y + 3*u*u*t*p1.y + 3*u*t*t*p2.y + t*t*t*p3.y,
30
- };
33
+ /**
34
+ * Point at t on a cubic: C's de Casteljau triangle. Each
35
+ * `(1.0 - t) * a + t * b` compiles to `fmul v, b, t; fmla v, a, 1-t`, i.e.
36
+ * fma(a, 1 - t, t * b), both in Bezier itself and where dotneato_closest
37
+ * inlines it (libgvc 16.1 disassembly). The Left/Right halves are not needed
38
+ * here, so only the apex is kept.
39
+ * @see lib/common/utils.c:Bezier
40
+ */
41
+ function bezier(v: Point[], t: number): Point {
42
+ const u = 1.0 - t;
43
+ const x = [v[0].x, v[1].x, v[2].x, v[3].x];
44
+ const y = [v[0].y, v[1].y, v[2].y, v[3].y];
45
+ for (let i = 1; i <= 3; i++) {
46
+ for (let j = 0; j <= 3 - i; j++) {
47
+ x[j] = fma(x[j], u, t * x[j + 1]);
48
+ y[j] = fma(y[j], u, t * y[j + 1]);
49
+ }
50
+ }
51
+ return { x: x[0], y: y[0] };
31
52
  }
32
53
 
33
54
  /** @see lib/common/utils.c:dotneato_closest */
@@ -55,7 +76,7 @@ export function dotneatoClosest(spl: SplLike, pt: Point): Point {
55
76
  let pt2: Point;
56
77
  for (;;) {
57
78
  const t = (lo + hi) / 2.0;
58
- pt2 = evalBez4(c, t);
79
+ pt2 = bezier(c, t);
59
80
  if (Math.abs(dlo2 - dhi2) < 1.0 || Math.abs(hi - lo) < 0.00001) break;
60
81
  if (dlo2 < dhi2) { hi = t; dhi2 = dist2(pt2, pt); }
61
82
  else { lo = t; dlo2 = dist2(pt2, pt); }
package/src/label/node.ts CHANGED
@@ -176,14 +176,20 @@ function updatePickBest(best: PickBest, i: number, area: number, increase: bigin
176
176
  }
177
177
  }
178
178
 
179
- /** Smallest rect enclosing all occupied branch rects. @see label/node.c:NodeCover */
179
+ /**
180
+ * Smallest rect enclosing all occupied branch rects.
181
+ * C's `r = n->branch[i].rect` copies the Rect_t; returning the branch's own
182
+ * object let a parent branch share it, and a later InitNode of either node
183
+ * (split) zeroed both, so RTreeSearch pruned a live subtree.
184
+ * @see label/node.c:NodeCover
185
+ */
180
186
  export function nodeCover(n: Node): Rect {
181
187
  let r: Rect = { boundary: [0, 0, 0, 0] };
182
188
  initRect(r);
183
189
  let flag = true;
184
190
  for (let i = 0; i < NODECARD; i++) {
185
191
  if (n.branch[i].child !== null) {
186
- if (flag) { r = n.branch[i].rect; flag = false; }
192
+ if (flag) { r = { boundary: [...n.branch[i].rect.boundary] }; flag = false; }
187
193
  else { r = combineRect(r, n.branch[i].rect); }
188
194
  }
189
195
  }
@@ -12,12 +12,18 @@
12
12
  import type { Node } from '../../model/node.js';
13
13
  import type { Block } from './blocks.js';
14
14
  import { isCoalesced } from './blocks.js';
15
+ import { fma } from '../../common/fma.js';
16
+ import { libmHypot } from '../../common/libm-hypot.js';
15
17
 
16
- /** 2-D rotation of (x,y) by (cosR, sinR). */
18
+ /**
19
+ * 2-D rotation of (x,y) by (cosR, sinR). clang contracts both lines into
20
+ * fmadd: X = x*cosR + (-sinR*y), Y = x*sinR + cosR*y (objdump of
21
+ * `_applyDelta`).
22
+ */
17
23
  export function rotatePoint(
18
24
  x: number, y: number, cosR: number, sinR: number,
19
25
  ): [number, number] {
20
- return [x * cosR - y * sinR, x * sinR + y * cosR];
26
+ return [fma(x, cosR, -sinR * y), fma(x, sinR, cosR * y)];
21
27
  }
22
28
 
23
29
  /** Read psi from a node's alg field (set during Pass 4). */
@@ -27,7 +33,7 @@ export function nodePsi(n: Node): number {
27
33
 
28
34
  /** getRotation: single-node block with preset parent_pos. */
29
35
  export function rotFromParentPos(sn: Block, theta: number): number {
30
- let r = theta + Math.PI - sn.parentPos;
36
+ let r = theta + (Math.PI - sn.parentPos); // C: theta += M_PI - parent_pos
31
37
  if (r < 0) r += 2 * Math.PI;
32
38
  return r;
33
39
  }
@@ -35,16 +41,19 @@ export function rotFromParentPos(sn: Block, theta: number): number {
35
41
  /** Find the node in sn closest to origin after translation by (x,y). */
36
42
  export function closestNode(sn: Block, x: number, y: number, skip: Node): Node {
37
43
  let best = skip;
38
- let bestDist = Math.hypot(skip.info.pos![0]! + x, skip.info.pos![1]! + y);
44
+ let bestDist = libmHypot(skip.info.pos![0]! + x, skip.info.pos![1]! + y);
39
45
  for (const dn of sn.subGraph.nodes) {
40
46
  if (dn.orig === skip) continue;
41
- const d = Math.hypot(dn.orig.info.pos![0]! + x, dn.orig.info.pos![1]! + y);
47
+ const d = libmHypot(dn.orig.info.pos![0]! + x, dn.orig.info.pos![1]! + y);
42
48
  if (d < bestDist) { bestDist = d; best = dn.orig; }
43
49
  }
44
50
  return best;
45
51
  }
46
52
 
47
- /** getRotation: COALESCED block — off-center trigonometric correction. */
53
+ /**
54
+ * getRotation: COALESCED block whose neighbor lies right of -r — off-center
55
+ * trigonometric correction. C: `theta += M_PI / 2.0 - phi - asin(...)`.
56
+ */
48
57
  export function rotCoalesced(
49
58
  sn: Block, x: number, y: number, nbr: Node, theta: number,
50
59
  ): number {
@@ -52,26 +61,28 @@ export function rotCoalesced(
52
61
  const r = sn.radius - rho;
53
62
  const nx = nbr.info.pos![0]!;
54
63
  const ny = nbr.info.pos![1]!;
55
- if (-r >= nx) {
56
- const phi = Math.atan2(ny, nx);
57
- return theta + Math.PI - phi - nodePsi(nbr);
58
- }
59
- const R = Math.hypot(x, y);
64
+ const R = libmHypot(x, y);
60
65
  const phi = Math.atan2(ny, nx + r);
61
66
  const l = r - rho / Math.cos(phi);
62
- return theta + Math.PI / 2 - phi - Math.asin((l / R) * Math.cos(phi));
67
+ return theta + ((Math.PI / 2 - phi) - Math.asin((l / R) * Math.cos(phi)));
63
68
  }
64
69
 
65
- /** getRotation: general multi-node non-coalesced block. */
70
+ /**
71
+ * getRotation when the neighbor is not the node closest to the parent: the
72
+ * coalesced correction when `COALESCED(sn) && -r < n_x`, else the
73
+ * origin-centred rotation `theta += M_PI - phi - PSI`, wrapped past 2*M_PI.
74
+ * @see lib/circogen/circpos.c:getRotation
75
+ */
66
76
  export function rotGeneral(
67
77
  sn: Block, x: number, y: number, nbr: Node, theta: number,
68
78
  ): number {
69
79
  if (closestNode(sn, x, y, nbr) === nbr) return 0;
70
- if (isCoalesced(sn)) return rotCoalesced(sn, x, y, nbr, theta);
80
+ const r = sn.radius - sn.rad0;
81
+ if (isCoalesced(sn) && -r < nbr.info.pos![0]!) return rotCoalesced(sn, x, y, nbr, theta);
71
82
  const phi = Math.atan2(nbr.info.pos![1]!, nbr.info.pos![0]!);
72
- let r = theta + Math.PI - phi - nodePsi(nbr);
73
- if (r > 2 * Math.PI) r -= 2 * Math.PI;
74
- return r;
83
+ let t = theta + ((Math.PI - phi) - nodePsi(nbr));
84
+ if (t > 2 * Math.PI) t -= 2 * Math.PI;
85
+ return t;
75
86
  }
76
87
 
77
88
  /**