@knowvah/dot-engine 1.2.1 → 1.2.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@knowvah/dot-engine",
3
- "version": "1.2.1",
3
+ "version": "1.2.2",
4
4
  "description": "Faithful TypeScript port of Graphviz (dot, neato, fdp, sfdp, circo, twopi, osage, patchwork) — no C: no native binary, no WASM. Oracle-verified against the C implementation, runs in the browser.",
5
5
  "keywords": [
6
6
  "graphviz",
@@ -201,6 +201,7 @@ function pointSizeResult(
201
201
  return {
202
202
  ...gvNodesize(sz, sz, false),
203
203
  outlineW: szOutline, outlineH: szOutline, baseW: sz0, baseH: sz0,
204
+ fixedshape: false, // C point_init never sets option.fixedshape
204
205
  };
205
206
  }
206
207
 
@@ -262,17 +263,20 @@ function initNodeFromLabel(n: Node, g: Graph, measurer: TextMeasurer): boolean {
262
263
  // C poly_init installs the ATTR-RESOLVED polygon into ND_shape_info
263
264
  // (sides/orientation/skew/distortion/peripheries/regular); edge
264
265
  // clipping and rendering read it.
265
- assignShapeInfo(n, effectivePolygon(poly, params));
266
+ assignShapeInfo(n, effectivePolygon(poly, params, sz.fixedshape));
266
267
  return true;
267
268
  }
268
269
 
269
270
  /** The attr-resolved polygon poly_init stores. @see shapes.c:poly_init (poly->... assignments) */
270
- function effectivePolygon(poly: PolygonT, p: PolySizeParams): PolygonT {
271
+ function effectivePolygon(poly: PolygonT, p: PolySizeParams, fixedshape: boolean): PolygonT {
271
272
  let sides = p.sides;
272
273
  // ellipses with distortion/skew become 120-gons
273
274
  if (sides <= 2 && (p.distortion !== 0 || p.skew !== 0)) sides = 120;
274
275
  return {
275
276
  ...poly,
277
+ // C: poly->option.fixedshape = true inside the fixedsize="shape" branch;
278
+ // poly-inside and computeVertices key the shape-box/width split on it.
279
+ option: { ...poly.option, fixedshape },
276
280
  regular: p.regular,
277
281
  peripheries: p.peripheries,
278
282
  sides,
@@ -32,7 +32,7 @@ export function sameSide(p0: Point, p1: Point, L0: Point, L1: Point): boolean {
32
32
  }
33
33
 
34
34
  /** Vertex-extent box of a fixedshape polygon. @see lib/common/shapes.c:polyBB */
35
- function fixedShapeSize(poly: PolygonT): { w: number; h: number } {
35
+ export function fixedShapeSize(poly: PolygonT): { w: number; h: number } {
36
36
  let xmax = 0;
37
37
  let ymax = 0;
38
38
  for (const v of poly.vertices ?? []) {
@@ -50,13 +50,10 @@ interface InsideScale {
50
50
  boxURy: number;
51
51
  }
52
52
 
53
- /** Node size + outline extents (points) for the inside scale. */
54
- function insideDims(n: Node, poly: PolygonT, xsize: number, ysize: number):
53
+ /** Node size + outline extents (points) for the inside scale (non-fixedshape;
54
+ * the fixedshape branch short-circuits in insideScale). */
55
+ function insideDims(n: Node, xsize: number, ysize: number):
55
56
  { w: number; h: number; ow: number; oh: number } {
56
- if (poly.option.fixedshape) {
57
- const { w, h } = fixedShapeSize(poly);
58
- return { w, h, ow: w, oh: h };
59
- }
60
57
  const w = 72 * (n.info.width || xsize / 72);
61
58
  const h = 72 * (n.info.height || ysize / 72);
62
59
  // Nodes that skipped poly_init (no measurer) have no outline dims.
@@ -67,10 +64,26 @@ function insideDims(n: Node, poly: PolygonT, xsize: number, ysize: number):
67
64
  function insideScale(n: Node, poly: PolygonT): InsideScale {
68
65
  // C swaps the node extents for flipped (LR/RL) layouts. @see poly_inside (GD_flip)
69
66
  const flip = n.root.info.flip === true;
67
+ // C's fixedshape branch derives xsize/ysize from the polyBB shape box
68
+ // itself (NOT ND_lw/rw/ht, which are label-grown there), so the scale
69
+ // collapses to 1 (swapped under flip) and the boundary is the pure vertex
70
+ // extent with no penwidth outline growth.
71
+ // @see shapes.c:poly_inside (option.fixedshape branch)
72
+ if (poly.option.fixedshape) {
73
+ const { w, h } = fixedShapeSize(poly);
74
+ const xsize = flip ? h : w;
75
+ const ysize = flip ? w : h;
76
+ return {
77
+ scalex: xsize !== 0 ? w / xsize : w,
78
+ scaley: ysize !== 0 ? h / ysize : h,
79
+ boxURx: w / 2,
80
+ boxURy: h / 2,
81
+ };
82
+ }
70
83
  const lwrw = n.info.lw + n.info.rw;
71
84
  const xsize = flip ? n.info.ht : lwrw;
72
85
  const ysize = flip ? lwrw : n.info.ht;
73
- const { w, h, ow, oh } = insideDims(n, poly, xsize, ysize);
86
+ const { w, h, ow, oh } = insideDims(n, xsize, ysize);
74
87
  return {
75
88
  scalex: xsize !== 0 ? w / xsize : w,
76
89
  scaley: ysize !== 0 ? h / ysize : h,
@@ -110,6 +110,14 @@ export interface PolySizeResult extends NodeSize {
110
110
  */
111
111
  baseW: number;
112
112
  baseH: number;
113
+ /**
114
+ * fixedsize="shape": the drawn shape stays at the attr box while
115
+ * width/height grow to max(label, shape). Installed on the polygon
116
+ * descriptor as `option.fixedshape`; vertex generation and insideness
117
+ * must read the base box, not the label-grown width.
118
+ * @see lib/common/shapes.c:poly_init (poly->option.fixedshape)
119
+ */
120
+ fixedshape: boolean;
113
121
  /**
114
122
  * Justification borders — the space available for the label within the node
115
123
  * box. `emit_label` reads this (not `dimen`) for `labelloc` top/bottom and
@@ -628,6 +636,7 @@ function assembleResult(
628
636
  outlineW: Math.max(dimen.x, grown.outline.x),
629
637
  outlineH: Math.max(dimen.y, grown.outline.y),
630
638
  baseW: grown.base.x, baseH: grown.base.y,
639
+ fixedshape,
631
640
  };
632
641
  }
633
642
  return {
@@ -635,5 +644,6 @@ function assembleResult(
635
644
  outlineW: grown.outline.x,
636
645
  outlineH: grown.outline.y,
637
646
  baseW: grown.base.x, baseH: grown.base.y,
647
+ fixedshape,
638
648
  };
639
649
  }
@@ -100,11 +100,32 @@ export function generalPolyVertices(poly: PolygonT, w: number, h: number): Point
100
100
  * which grows bb to the outermost ring after placing rings outward.
101
101
  * @see lib/common/shapes.c:poly_init (ellipse peripheries, :2173-2184)
102
102
  */
103
- function ellipseRings(w: number, h: number, peripheries: number): Point[] {
103
+ function ellipseRings(
104
+ w: number,
105
+ h: number,
106
+ peripheries: number,
107
+ base?: { w: number; h: number },
108
+ ): Point[] {
109
+ if (base === undefined) {
110
+ const out: Point[] = [];
111
+ for (let j = 0; j < peripheries; j++) {
112
+ const inset = (peripheries - 1 - j) * GAP;
113
+ out.push(...ellipseVertices(w - 2 * inset, h - 2 * inset));
114
+ }
115
+ return out;
116
+ }
117
+ // C's forward order: the base ring at the shape box, each further ring
118
+ // stepping the half-extent OUTWARD by GAP. Needed under fixedshape, where
119
+ // the stored width is label-grown and the inset-from-width form above
120
+ // cannot recover the shape box. @see lib/common/shapes.c:poly_init
121
+ // (sides < 3: vertices[0/1] = ±bb/2, then P.x += GAP per periphery)
104
122
  const out: Point[] = [];
123
+ let px = base.w / 2;
124
+ let py = base.h / 2;
105
125
  for (let j = 0; j < peripheries; j++) {
106
- const inset = (peripheries - 1 - j) * GAP;
107
- out.push(...ellipseVertices(w - 2 * inset, h - 2 * inset));
126
+ out.push({ x: -px, y: -py }, { x: px, y: py });
127
+ px += GAP;
128
+ py += GAP;
108
129
  }
109
130
  return out;
110
131
  }
@@ -221,13 +242,25 @@ export function computeVertices(
221
242
  base?: { w: number; h: number },
222
243
  ): Point[] {
223
244
  const sides = poly.sides;
245
+ // C builds vertices from the constrained shape box (bb) BEFORE the
246
+ // fixedshape branch sets ND_width/ND_height to max(label, bb)
247
+ // (shapes.c:poly_init tail), so under fixedsize="shape" the stored width
248
+ // is label-grown while the drawn shape stays at the attr box. Substitute
249
+ // the stored base box (= that bb) as the vertex source; every other case
250
+ // has width == shape box and is untouched.
251
+ if (poly.option.fixedshape && base !== undefined) {
252
+ w = base.w;
253
+ h = base.h;
254
+ }
224
255
  // Cylinder is a 19-point bezier control polygon, not a regular 19-gon.
225
256
  if (poly.option.shape === CYLINDER) return cylinderVertices(w, h);
226
257
  // C generates one ring even for peripheries=0 (outp >= 1); the draw
227
258
  // loop is what skips it. @see lib/common/shapes.c:poly_init (outp)
228
259
  const peripheries = Math.max(poly.peripheries, 1);
229
260
  if (poly.option.shape === STAR) return starRings({ w, h, base }, peripheries);
230
- if (sides <= 2) return ellipseRings(w, h, peripheries);
261
+ if (sides <= 2) {
262
+ return ellipseRings(w, h, peripheries, poly.option.fixedshape ? base : undefined);
263
+ }
231
264
  // C's isBox test: right angles only (diamond = orientation 45).
232
265
  if (sides === 4 && Math.abs(poly.orientation % 90) < 0.5
233
266
  && poly.distortion === 0 && poly.skew === 0) {
@@ -15,6 +15,8 @@ import type { Graph } from '../../model/graph.js';
15
15
  import type { Node } from '../../model/node.js';
16
16
  import type { Edge as GraphEdge } from '../../model/edge.js';
17
17
  import { nodeAttr } from '../../common/poly-init.js';
18
+ import { fixedShapeSize } from '../../common/poly-inside.js';
19
+ import type { PolygonT } from '../../common/types.js';
18
20
  import type { Point, Bezier, Spline } from '../../model/geom.js';
19
21
  import { routeSpline } from '../../pathplan/route.js';
20
22
  import type { Edge as BarrierEdge } from '../../pathplan/types.js';
@@ -69,6 +71,23 @@ export function nodeBoxOf(n: Node, g: Graph): NodeBox {
69
71
  const rb = recordClipBox(n);
70
72
  if (rb) return { center: n.info.coord, ...rb, isEllipse: false, penwidth };
71
73
  }
74
+ // C poly_inside sizes the boundary from polyBB (the vertex extent) under
75
+ // fixedshape — ND_lw/rw/ht are label-grown there and must not clip the
76
+ // edge, and the boundary carries NO penwidth outline growth.
77
+ // @see lib/common/shapes.c:poly_inside (option.fixedshape branch)
78
+ const si = n.info.shape_info as PolygonT | undefined;
79
+ if (si?.option?.fixedshape === true && si.vertices) {
80
+ const ext = fixedShapeSize(si);
81
+ const flip = g.root.info.flip === true;
82
+ const w = flip ? ext.h : ext.w;
83
+ const h = flip ? ext.w : ext.h;
84
+ return {
85
+ center: n.info.coord,
86
+ lw: w / 2, rw: w / 2, ht: h,
87
+ isEllipse: ELLIPSE_SHAPES.has(shapeName),
88
+ penwidth: 0,
89
+ };
90
+ }
72
91
  return {
73
92
  center: n.info.coord,
74
93
  lw: n.info.lw > 0 ? n.info.lw : 27,