@knowvah/dot-engine 2.6.2 → 2.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/common/htmltable-lex.d.ts +38 -0
- package/dist/common/htmltable-lex.d.ts.map +1 -1
- package/dist/common/htmltable-parse-attrs.d.ts +41 -0
- package/dist/common/htmltable-parse-attrs.d.ts.map +1 -0
- package/dist/common/htmltable-parse.d.ts +3 -0
- package/dist/common/htmltable-parse.d.ts.map +1 -1
- package/dist/common/htmltable-pos.d.ts +17 -0
- package/dist/common/htmltable-pos.d.ts.map +1 -1
- package/dist/common/htmltable.d.ts +13 -1
- package/dist/common/htmltable.d.ts.map +1 -1
- package/dist/common/make-label.d.ts.map +1 -1
- package/dist/common/spline-midpoint.d.ts +8 -0
- package/dist/common/spline-midpoint.d.ts.map +1 -1
- package/dist/index.js +617 -322
- package/dist/index.js.map +4 -4
- package/dist/label/node.d.ts +7 -1
- package/dist/label/node.d.ts.map +1 -1
- package/dist/layout/circo/position-helpers.d.ts +15 -3
- package/dist/layout/circo/position-helpers.d.ts.map +1 -1
- package/dist/layout/circo/position.d.ts +17 -1
- package/dist/layout/circo/position.d.ts.map +1 -1
- package/dist/layout/dot/ortho-adapter.d.ts.map +1 -1
- package/dist/layout/neato/multispline.d.ts.map +1 -1
- package/dist/layout/pack/poly-pack.d.ts +2 -18
- package/dist/layout/pack/poly-pack.d.ts.map +1 -1
- package/dist/pathplan/shortest.d.ts.map +1 -1
- package/dist/render.js +770 -475
- package/dist/render.js.map +4 -4
- package/package.json +1 -1
- package/src/common/htmltable-lex.ts +265 -5
- package/src/common/htmltable-parse-attrs.ts +166 -0
- package/src/common/htmltable-parse.ts +262 -242
- package/src/common/htmltable-pos.ts +37 -38
- package/src/common/htmltable.ts +139 -45
- package/src/common/make-label.ts +19 -3
- package/src/common/spline-midpoint.ts +30 -9
- package/src/label/node.ts +8 -2
- package/src/layout/circo/position-helpers.ts +28 -17
- package/src/layout/circo/position.ts +28 -7
- package/src/layout/dot/ortho-adapter.ts +20 -9
- package/src/layout/neato/multispline.ts +17 -5
- package/src/layout/pack/poly-pack.ts +5 -31
- 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 {
|
|
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
|
-
*
|
|
522
|
-
*
|
|
523
|
-
*
|
|
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
|
-
):
|
|
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)
|
|
562
|
-
//
|
|
563
|
-
//
|
|
564
|
-
|
|
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. */
|
package/src/common/htmltable.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
297
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
):
|
|
437
|
-
const { entries, widths, heights, spacing, border } = layoutHtmlTable(tbl, measurer, env);
|
|
438
|
-
if (entries.length === 0) {
|
|
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
|
-
):
|
|
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
|
-
|
|
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
|
};
|
package/src/common/make-label.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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 =
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
|
73
|
-
if (
|
|
74
|
-
return
|
|
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
|
/**
|