@kgdiff/layout 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +46 -2
  3. package/dist/components.d.ts +11 -0
  4. package/dist/components.d.ts.map +1 -0
  5. package/dist/components.js +56 -0
  6. package/dist/components.js.map +1 -0
  7. package/dist/engine.d.ts +20 -0
  8. package/dist/engine.d.ts.map +1 -0
  9. package/dist/engine.js +336 -0
  10. package/dist/engine.js.map +1 -0
  11. package/dist/extend.d.ts +7 -0
  12. package/dist/extend.d.ts.map +1 -0
  13. package/dist/extend.js +68 -0
  14. package/dist/extend.js.map +1 -0
  15. package/dist/forces/collide-rect.d.ts +50 -0
  16. package/dist/forces/collide-rect.d.ts.map +1 -0
  17. package/dist/forces/collide-rect.js +310 -0
  18. package/dist/forces/collide-rect.js.map +1 -0
  19. package/dist/forces/free-spot.d.ts +40 -0
  20. package/dist/forces/free-spot.d.ts.map +1 -0
  21. package/dist/forces/free-spot.js +189 -0
  22. package/dist/forces/free-spot.js.map +1 -0
  23. package/dist/forces/params.d.ts +38 -0
  24. package/dist/forces/params.d.ts.map +1 -0
  25. package/dist/forces/params.js +55 -0
  26. package/dist/forces/params.js.map +1 -0
  27. package/dist/forces/springs.d.ts +20 -0
  28. package/dist/forces/springs.d.ts.map +1 -0
  29. package/dist/forces/springs.js +34 -0
  30. package/dist/forces/springs.js.map +1 -0
  31. package/dist/geometry.d.ts +21 -0
  32. package/dist/geometry.d.ts.map +1 -0
  33. package/dist/geometry.js +39 -0
  34. package/dist/geometry.js.map +1 -0
  35. package/dist/graph.d.ts +38 -0
  36. package/dist/graph.d.ts.map +1 -0
  37. package/dist/graph.js +82 -0
  38. package/dist/graph.js.map +1 -0
  39. package/dist/index.d.ts +162 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +87 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/init.d.ts +28 -0
  44. package/dist/init.d.ts.map +1 -0
  45. package/dist/init.js +196 -0
  46. package/dist/init.js.map +1 -0
  47. package/dist/json.d.ts +4 -0
  48. package/dist/json.d.ts.map +1 -0
  49. package/dist/json.js +112 -0
  50. package/dist/json.js.map +1 -0
  51. package/dist/layout.d.ts +7 -0
  52. package/dist/layout.d.ts.map +1 -0
  53. package/dist/layout.js +70 -0
  54. package/dist/layout.js.map +1 -0
  55. package/dist/math.d.ts +29 -0
  56. package/dist/math.d.ts.map +1 -0
  57. package/dist/math.js +91 -0
  58. package/dist/math.js.map +1 -0
  59. package/dist/options.d.ts +43 -0
  60. package/dist/options.d.ts.map +1 -0
  61. package/dist/options.js +76 -0
  62. package/dist/options.js.map +1 -0
  63. package/dist/pack.d.ts +43 -0
  64. package/dist/pack.d.ts.map +1 -0
  65. package/dist/pack.js +168 -0
  66. package/dist/pack.js.map +1 -0
  67. package/dist/prng.d.ts +20 -0
  68. package/dist/prng.d.ts.map +1 -0
  69. package/dist/prng.js +53 -0
  70. package/dist/prng.js.map +1 -0
  71. package/dist/result.d.ts +20 -0
  72. package/dist/result.d.ts.map +1 -0
  73. package/dist/result.js +40 -0
  74. package/dist/result.js.map +1 -0
  75. package/dist/scope.d.ts +25 -0
  76. package/dist/scope.d.ts.map +1 -0
  77. package/dist/scope.js +71 -0
  78. package/dist/scope.js.map +1 -0
  79. package/dist/simulate.d.ts +64 -0
  80. package/dist/simulate.d.ts.map +1 -0
  81. package/dist/simulate.js +247 -0
  82. package/dist/simulate.js.map +1 -0
  83. package/dist/text-metrics.d.ts +32 -0
  84. package/dist/text-metrics.d.ts.map +1 -0
  85. package/dist/text-metrics.js +153 -0
  86. package/dist/text-metrics.js.map +1 -0
  87. package/dist/union.d.ts +32 -0
  88. package/dist/union.d.ts.map +1 -0
  89. package/dist/union.js +106 -0
  90. package/dist/union.js.map +1 -0
  91. package/package.json +52 -3
  92. package/src/components.ts +56 -0
  93. package/src/engine.ts +444 -0
  94. package/src/extend.ts +75 -0
  95. package/src/forces/collide-rect.ts +385 -0
  96. package/src/forces/free-spot.ts +243 -0
  97. package/src/forces/params.ts +88 -0
  98. package/src/forces/springs.ts +50 -0
  99. package/src/geometry.ts +66 -0
  100. package/src/graph.ts +120 -0
  101. package/src/index.ts +193 -0
  102. package/src/init.ts +246 -0
  103. package/src/json.ts +131 -0
  104. package/src/layout.ts +87 -0
  105. package/src/math.ts +91 -0
  106. package/src/options.ts +153 -0
  107. package/src/pack.ts +228 -0
  108. package/src/prng.ts +59 -0
  109. package/src/result.ts +63 -0
  110. package/src/scope.ts +86 -0
  111. package/src/simulate.ts +328 -0
  112. package/src/text-metrics.ts +157 -0
  113. package/src/union.ts +127 -0
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Force parameters (docs/algorithms.md §12 "Simulation") and their label-aware adjustments.
3
+ *
4
+ * Spec values:
5
+ * - link distance `L·(1 + 0.15·log₂(1 + min(deg_s, deg_t)))`, strength `1 / min(deg_s, deg_t)`;
6
+ * - many-body strength `−(220 + 40·√degree)` (θ 0.9, distanceMax 900, see simulate.ts), scaled
7
+ * by `(linkDistance / 56)²` so the picture scales with the `linkDistance` option.
8
+ *
9
+ * Label-aware adjustments (only when `labelAware`), both of which reduce to the spec values for
10
+ * nodes with ordinary labels and ordinary neighbourhoods:
11
+ * - charge × `clamp(boxArea / 1.5·L², 1, 3)`: a node whose label block needs more room than the
12
+ * ~L² the spec charge buys pushes proportionally harder, so the layout makes room for long
13
+ * labels globally instead of jamming them locally;
14
+ * - link strength × `min(1, 2·distance / crowding)`, where `crowding` is the radius of a disc
15
+ * holding the label blocks of a node's neighbours: the links of a hub with hundreds of
16
+ * labelled leaves relax, so the leaves spread into a disc instead of being squeezed into a
17
+ * ring of radius L that cannot hold them.
18
+ */
19
+ import type { LayoutGraph } from '../graph.js';
20
+ export declare function linkDistance(linkDistanceOption: number, minDegree: number): number;
21
+ export declare function linkStrength(graph: LayoutGraph, a: number, b: number, distance: number, labelAware: boolean): number;
22
+ export declare function chargeStrength(graph: LayoutGraph, i: number, linkDistanceOption: number, labelAware: boolean): number;
23
+ /**
24
+ * Pull towards the component center (not in the spec): keeps components round and compact.
25
+ * A linear pull compresses the core of a large component (its pressure grows with the radius), so
26
+ * it decays with size: `0.3 / √(n/16)` up to 400 nodes, then as `(n/400)^−0.75` beyond.
27
+ * Powers are built from `Math.sqrt` only (deterministic across engines).
28
+ */
29
+ export declare function gravityFor(size: number): number;
30
+ /** Many-body cut-off of the spec. */
31
+ export declare const CHARGE_RANGE = 900;
32
+ /**
33
+ * Many-body cut-off when refining around fixed nodes: when most of the picture is fixed, new
34
+ * nodes should settle into local gaps next to their neighbours instead of being blown out of the
35
+ * graph by the summed repulsion of everything that is already placed.
36
+ */
37
+ export declare function anchoredChargeRange(freeShare: number, linkDistanceOption: number): number;
38
+ //# sourceMappingURL=params.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"params.d.ts","sourceRoot":"","sources":["../../src/forces/params.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAY/C,wBAAgB,YAAY,CAAC,kBAAkB,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAElF;AAED,wBAAgB,YAAY,CAC1B,KAAK,EAAE,WAAW,EAClB,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,EACT,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,OAAO,GAClB,MAAM,CAOR;AAED,wBAAgB,cAAc,CAC5B,KAAK,EAAE,WAAW,EAClB,CAAC,EAAE,MAAM,EACT,kBAAkB,EAAE,MAAM,EAC1B,UAAU,EAAE,OAAO,GAClB,MAAM,CAQR;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAK/C;AAED,qCAAqC;AACrC,eAAO,MAAM,YAAY,MAAM,CAAC;AAEhC;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,EAAE,kBAAkB,EAAE,MAAM,GAAG,MAAM,CAEzF"}
@@ -0,0 +1,55 @@
1
+ import { log2 } from '../math.js';
2
+ const DEFAULT_LINK_DISTANCE = 56;
3
+ const LABEL_CHARGE_MAX = 3;
4
+ const LABEL_AREA_REF = 1.5;
5
+ const CROWDING_RATIO = 2;
6
+ /** Gravity of a component with ≤ 16 nodes; larger components get less (see `gravityFor`). */
7
+ const GRAVITY = 0.3;
8
+ /** 400 nodes, in units of 16. */
9
+ const GRAVITY_KNEE = 25;
10
+ export function linkDistance(linkDistanceOption, minDegree) {
11
+ return linkDistanceOption * (1 + 0.15 * log2(1 + minDegree));
12
+ }
13
+ export function linkStrength(graph, a, b, distance, labelAware) {
14
+ const minDegree = Math.max(1, Math.min(graph.degree[a], graph.degree[b]));
15
+ const strength = 1 / minDegree;
16
+ if (!labelAware)
17
+ return strength;
18
+ const crowding = Math.max(graph.crowding[a], graph.crowding[b]);
19
+ const limit = CROWDING_RATIO * distance;
20
+ return crowding > limit ? (strength * limit) / crowding : strength;
21
+ }
22
+ export function chargeStrength(graph, i, linkDistanceOption, labelAware) {
23
+ // The spec constants are tuned for the default link distance; scaling by (L / 56)² keeps the
24
+ // equilibrium shape, so a larger `linkDistance` gives a proportionally larger picture.
25
+ const scale = linkDistanceOption / DEFAULT_LINK_DISTANCE;
26
+ const base = -(220 + 40 * Math.sqrt(graph.degree[i])) * scale * scale;
27
+ if (!labelAware)
28
+ return base;
29
+ const ratio = graph.boxArea[i] / (LABEL_AREA_REF * linkDistanceOption * linkDistanceOption);
30
+ return base * Math.min(LABEL_CHARGE_MAX, Math.max(1, ratio));
31
+ }
32
+ /**
33
+ * Pull towards the component center (not in the spec): keeps components round and compact.
34
+ * A linear pull compresses the core of a large component (its pressure grows with the radius), so
35
+ * it decays with size: `0.3 / √(n/16)` up to 400 nodes, then as `(n/400)^−0.75` beyond.
36
+ * Powers are built from `Math.sqrt` only (deterministic across engines).
37
+ */
38
+ export function gravityFor(size) {
39
+ const s = Math.max(1, size / 16);
40
+ if (s <= GRAVITY_KNEE)
41
+ return GRAVITY / Math.sqrt(s);
42
+ const r = s / GRAVITY_KNEE;
43
+ return GRAVITY / Math.sqrt(GRAVITY_KNEE) / (Math.sqrt(r) * Math.sqrt(Math.sqrt(r)));
44
+ }
45
+ /** Many-body cut-off of the spec. */
46
+ export const CHARGE_RANGE = 900;
47
+ /**
48
+ * Many-body cut-off when refining around fixed nodes: when most of the picture is fixed, new
49
+ * nodes should settle into local gaps next to their neighbours instead of being blown out of the
50
+ * graph by the summed repulsion of everything that is already placed.
51
+ */
52
+ export function anchoredChargeRange(freeShare, linkDistanceOption) {
53
+ return freeShare >= 0.5 ? CHARGE_RANGE : Math.min(CHARGE_RANGE, 4 * linkDistanceOption);
54
+ }
55
+ //# sourceMappingURL=params.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"params.js","sourceRoot":"","sources":["../../src/forces/params.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Small custom forces that complement d3's link and many-body forces.
3
+ */
4
+ import type { SimNode } from '../simulate.js';
5
+ export interface Spring {
6
+ /** Sim-local index of the node that is pulled (merge source / split target). */
7
+ readonly mover: number;
8
+ /** Sim-local index of its partner (merge target / split source). */
9
+ readonly anchor: number;
10
+ }
11
+ /**
12
+ * Weak one-sided springs (docs/algorithms.md §12: strength 0.08) that keep merge sources near
13
+ * their target and split targets near their source, so replay travel stays short. They only
14
+ * pull (collision keeps the pair apart), and the anchor feels a small share of the reaction so
15
+ * it is not dragged around by its ghosts.
16
+ */
17
+ export declare function applySprings(nodes: readonly SimNode[], springs: readonly Spring[], alpha: number, strength: number, restLength: number): void;
18
+ /** Linear pull towards the origin (d3 forceX / forceY at 0): keeps components round and compact. */
19
+ export declare function applyGravity(nodes: readonly SimNode[], alpha: number, strength: number): void;
20
+ //# sourceMappingURL=springs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"springs.d.ts","sourceRoot":"","sources":["../../src/forces/springs.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAE9C,MAAM,WAAW,MAAM;IACrB,gFAAgF;IAChF,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,oEAAoE;IACpE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAC1B,KAAK,EAAE,SAAS,OAAO,EAAE,EACzB,OAAO,EAAE,SAAS,MAAM,EAAE,EAC1B,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,GACjB,IAAI,CAgBN;AAED,oGAAoG;AACpG,wBAAgB,YAAY,CAAC,KAAK,EAAE,SAAS,OAAO,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAM7F"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Weak one-sided springs (docs/algorithms.md §12: strength 0.08) that keep merge sources near
3
+ * their target and split targets near their source, so replay travel stays short. They only
4
+ * pull (collision keeps the pair apart), and the anchor feels a small share of the reaction so
5
+ * it is not dragged around by its ghosts.
6
+ */
7
+ export function applySprings(nodes, springs, alpha, strength, restLength) {
8
+ const anchorShare = 0.2;
9
+ for (const { mover, anchor } of springs) {
10
+ const m = nodes[mover];
11
+ const a = nodes[anchor];
12
+ if (m === undefined || a === undefined)
13
+ continue;
14
+ const dx = a.x + a.vx - m.x - m.vx;
15
+ const dy = a.y + a.vy - m.y - m.vy;
16
+ const l = Math.sqrt(dx * dx + dy * dy);
17
+ if (l <= restLength)
18
+ continue;
19
+ const k = ((l - restLength) / l) * alpha * strength;
20
+ m.vx += dx * k * (1 - anchorShare);
21
+ m.vy += dy * k * (1 - anchorShare);
22
+ a.vx -= dx * k * anchorShare;
23
+ a.vy -= dy * k * anchorShare;
24
+ }
25
+ }
26
+ /** Linear pull towards the origin (d3 forceX / forceY at 0): keeps components round and compact. */
27
+ export function applyGravity(nodes, alpha, strength) {
28
+ const k = strength * alpha;
29
+ for (const node of nodes) {
30
+ node.vx -= node.x * k;
31
+ node.vy -= node.y * k;
32
+ }
33
+ }
34
+ //# sourceMappingURL=springs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"springs.js","sourceRoot":"","sources":["../../src/forces/springs.ts"],"names":[],"mappings":"AAYA;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAC1B,KAAyB,EACzB,OAA0B,EAC1B,KAAa,EACb,QAAgB,EAChB,UAAkB;IAElB,MAAM,WAAW,GAAG,GAAG,CAAC;IACxB,KAAK,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,OAAO,EAAE,CAAC;QACxC,MAAM,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC;QACvB,MAAM,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC;QACxB,IAAI,CAAC,KAAK,SAAS,IAAI,CAAC,KAAK,SAAS;YAAE,SAAS;QACjD,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;QACnC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;QACnC,MAAM,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC;QACvC,IAAI,CAAC,IAAI,UAAU;YAAE,SAAS;QAC9B,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC,GAAG,KAAK,GAAG,QAAQ,CAAC;QACpD,CAAC,CAAC,EAAE,IAAI,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,WAAW,CAAC,CAAC;QACnC,CAAC,CAAC,EAAE,IAAI,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,WAAW,CAAC,CAAC;QACnC,CAAC,CAAC,EAAE,IAAI,EAAE,GAAG,CAAC,GAAG,WAAW,CAAC;QAC7B,CAAC,CAAC,EAAE,IAAI,EAAE,GAAG,CAAC,GAAG,WAAW,CAAC;IAC/B,CAAC;AACH,CAAC;AAED,oGAAoG;AACpG,MAAM,UAAU,YAAY,CAAC,KAAyB,EAAE,KAAa,EAAE,QAAgB;IACrF,MAAM,CAAC,GAAG,QAAQ,GAAG,KAAK,CAAC;IAC3B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,CAAC,EAAE,IAAI,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;QACtB,IAAI,CAAC,EAAE,IAAI,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;IACxB,CAAC;AACH,CAAC"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Extents and rectangles of node + label boxes.
3
+ */
4
+ import type { Rect } from '@kgdiff/core';
5
+ import type { LayoutGraph } from './graph.js';
6
+ /** Gap kept between node / label boxes (world units). */
7
+ export declare const BOX_PADDING = 4;
8
+ export interface Extent {
9
+ minX: number;
10
+ minY: number;
11
+ maxX: number;
12
+ maxY: number;
13
+ }
14
+ export declare const emptyExtent: () => Extent;
15
+ /** Grow `extent` by node `i`'s visual box (circle + label block) centered at (x, y). */
16
+ export declare function includeNode(extent: Extent, graph: LayoutGraph, i: number, x: number, y: number): void;
17
+ export declare function extentOf(graph: LayoutGraph, members: Iterable<number>, x: ArrayLike<number>, y: ArrayLike<number>): Extent;
18
+ export declare const isEmpty: (e: Extent) => boolean;
19
+ /** The smallest 0.01-aligned rectangle containing `extent` (zero rect when empty). */
20
+ export declare function toRect(extent: Extent): Rect;
21
+ //# sourceMappingURL=geometry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"geometry.d.ts","sourceRoot":"","sources":["../src/geometry.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,yDAAyD;AACzD,eAAO,MAAM,WAAW,IAAI,CAAC;AAE7B,MAAM,WAAW,MAAM;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,eAAO,MAAM,WAAW,QAAO,MAK7B,CAAC;AAEH,wFAAwF;AACxF,wBAAgB,WAAW,CACzB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,WAAW,EAClB,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,GACR,IAAI,CAMN;AAED,wBAAgB,QAAQ,CACtB,KAAK,EAAE,WAAW,EAClB,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,EACzB,CAAC,EAAE,SAAS,CAAC,MAAM,CAAC,EACpB,CAAC,EAAE,SAAS,CAAC,MAAM,CAAC,GACnB,MAAM,CAIR;AAED,eAAO,MAAM,OAAO,MAAO,MAAM,KAAG,OAA8B,CAAC;AAKnE,sFAAsF;AACtF,wBAAgB,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAU3C"}
@@ -0,0 +1,39 @@
1
+ /** Gap kept between node / label boxes (world units). */
2
+ export const BOX_PADDING = 4;
3
+ export const emptyExtent = () => ({
4
+ minX: Number.POSITIVE_INFINITY,
5
+ minY: Number.POSITIVE_INFINITY,
6
+ maxX: Number.NEGATIVE_INFINITY,
7
+ maxY: Number.NEGATIVE_INFINITY,
8
+ });
9
+ /** Grow `extent` by node `i`'s visual box (circle + label block) centered at (x, y). */
10
+ export function includeNode(extent, graph, i, x, y) {
11
+ const hw = graph.boxHalfWidth[i];
12
+ extent.minX = Math.min(extent.minX, x - hw);
13
+ extent.maxX = Math.max(extent.maxX, x + hw);
14
+ extent.minY = Math.min(extent.minY, y + graph.boxTop[i]);
15
+ extent.maxY = Math.max(extent.maxY, y + graph.boxBottom[i]);
16
+ }
17
+ export function extentOf(graph, members, x, y) {
18
+ const extent = emptyExtent();
19
+ for (const i of members)
20
+ includeNode(extent, graph, i, x[i], y[i]);
21
+ return extent;
22
+ }
23
+ export const isEmpty = (e) => !(e.maxX >= e.minX);
24
+ const floor2 = (v) => Math.floor(v * 100) / 100 || 0;
25
+ const ceil2 = (v) => Math.ceil(v * 100) / 100 || 0;
26
+ /** The smallest 0.01-aligned rectangle containing `extent` (zero rect when empty). */
27
+ export function toRect(extent) {
28
+ if (isEmpty(extent))
29
+ return Object.freeze({ x: 0, y: 0, width: 0, height: 0 });
30
+ const x = floor2(extent.minX);
31
+ const y = floor2(extent.minY);
32
+ return Object.freeze({
33
+ x,
34
+ y,
35
+ width: Math.round((ceil2(extent.maxX) - x) * 100) / 100,
36
+ height: Math.round((ceil2(extent.maxY) - y) * 100) / 100,
37
+ });
38
+ }
39
+ //# sourceMappingURL=geometry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"geometry.js","sourceRoot":"","sources":["../src/geometry.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * The graph actually laid out: the union nodes in scope (re-indexed 0..n−1 in id order), their
3
+ * radii and label blocks, and the links / partner springs among them.
4
+ */
5
+ import type { UnionId, UnionNode } from '@kgdiff/core';
6
+ import { type Partner, type UnionGraph } from './union.js';
7
+ export interface LayoutGraph {
8
+ readonly n: number;
9
+ readonly nodes: readonly UnionNode[];
10
+ readonly indexOf: ReadonlyMap<UnionId, number>;
11
+ /** `nodeRadius(max(degree.before, degree.after))`. */
12
+ readonly radius: Float64Array;
13
+ /**
14
+ * Visual box of node + label relative to the node center: half width, top (negative) and
15
+ * bottom offsets. Used for bounds, and for collision when the layout is label-aware.
16
+ */
17
+ readonly boxHalfWidth: Float64Array;
18
+ readonly boxTop: Float64Array;
19
+ readonly boxBottom: Float64Array;
20
+ /** Area of the padded visual box. */
21
+ readonly boxArea: Float64Array;
22
+ /** Radius of a disc holding the padded boxes of all neighbours (label-aware link relaxation). */
23
+ readonly crowding: Float64Array;
24
+ /** Distinct neighbours within the layout (drives link and charge strengths). */
25
+ readonly degree: Int32Array;
26
+ /** Distinct undirected links [a, b], a < b. */
27
+ readonly pairs: Int32Array;
28
+ readonly offsets: Int32Array;
29
+ readonly adjacency: Int32Array;
30
+ readonly partners: readonly Partner[];
31
+ }
32
+ export interface GraphGeometry {
33
+ readonly fontSize: number;
34
+ readonly nodeRadius: (degree: number) => number;
35
+ }
36
+ /** Induced subgraph of `union` over `visible`. */
37
+ export declare function buildLayoutGraph(union: UnionGraph, visible: Uint8Array, geometry: GraphGeometry): LayoutGraph;
38
+ //# sourceMappingURL=graph.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graph.d.ts","sourceRoot":"","sources":["../src/graph.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAGvD,OAAO,EAAkB,KAAK,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAE3E,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,KAAK,EAAE,SAAS,SAAS,EAAE,CAAC;IACrC,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IAC/C,sDAAsD;IACtD,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B;;;OAGG;IACH,QAAQ,CAAC,YAAY,EAAE,YAAY,CAAC;IACpC,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B,QAAQ,CAAC,SAAS,EAAE,YAAY,CAAC;IACjC,qCAAqC;IACrC,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC;IAC/B,iGAAiG;IACjG,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAC;IAChC,gFAAgF;IAChF,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAC5B,+CAA+C;IAC/C,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAC;IAC7B,QAAQ,CAAC,SAAS,EAAE,UAAU,CAAC;IAC/B,QAAQ,CAAC,QAAQ,EAAE,SAAS,OAAO,EAAE,CAAC;CACvC;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,UAAU,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,CAAC;CACjD;AAOD,kDAAkD;AAClD,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,UAAU,EACjB,OAAO,EAAE,UAAU,EACnB,QAAQ,EAAE,aAAa,GACtB,WAAW,CAqEb"}
package/dist/graph.js ADDED
@@ -0,0 +1,82 @@
1
+ import { BOX_PADDING } from './geometry.js';
2
+ import { LABEL_GAP, labelBox } from './text-metrics.js';
3
+ import { buildAdjacency } from './union.js';
4
+ function labelBefore(node) {
5
+ for (const facet of node.facets)
6
+ if (facet.kind === 'rename')
7
+ return facet.from;
8
+ return undefined;
9
+ }
10
+ /** Induced subgraph of `union` over `visible`. */
11
+ export function buildLayoutGraph(union, visible, geometry) {
12
+ const local = new Int32Array(union.nodes.length).fill(-1);
13
+ const nodes = [];
14
+ for (let u = 0; u < union.nodes.length; u++) {
15
+ if (!visible[u])
16
+ continue;
17
+ local[u] = nodes.length;
18
+ nodes.push(union.nodes[u]);
19
+ }
20
+ const n = nodes.length;
21
+ const indexOf = new Map();
22
+ const radius = new Float64Array(n);
23
+ const boxHalfWidth = new Float64Array(n);
24
+ const boxTop = new Float64Array(n);
25
+ const boxBottom = new Float64Array(n);
26
+ for (let i = 0; i < n; i++) {
27
+ const node = nodes[i];
28
+ indexOf.set(node.id, i);
29
+ const r = geometry.nodeRadius(Math.max(node.degree.before, node.degree.after));
30
+ const label = labelBox(node.label, labelBefore(node), geometry.fontSize);
31
+ radius[i] = r;
32
+ boxHalfWidth[i] = Math.max(r, label.width / 2);
33
+ boxTop[i] = -r;
34
+ boxBottom[i] = r + LABEL_GAP + label.height;
35
+ }
36
+ const flat = [];
37
+ for (let k = 0; k < union.pairs.length; k += 2) {
38
+ const a = local[union.pairs[k]];
39
+ const b = local[union.pairs[k + 1]];
40
+ if (a >= 0 && b >= 0)
41
+ flat.push(Math.min(a, b), Math.max(a, b));
42
+ }
43
+ const pairs = Int32Array.from(flat);
44
+ const { offsets, adjacency } = buildAdjacency(n, pairs);
45
+ const degree = new Int32Array(n);
46
+ const boxArea = new Float64Array(n);
47
+ for (let i = 0; i < n; i++) {
48
+ degree[i] = offsets[i + 1] - offsets[i];
49
+ boxArea[i] = (2 * boxHalfWidth[i] + BOX_PADDING) * (boxBottom[i] - boxTop[i] + BOX_PADDING);
50
+ }
51
+ const crowding = new Float64Array(n);
52
+ for (let i = 0; i < n; i++) {
53
+ let area = 0;
54
+ for (let k = offsets[i]; k < offsets[i + 1]; k++)
55
+ area += boxArea[adjacency[k]];
56
+ crowding[i] = Math.sqrt(area / Math.PI);
57
+ }
58
+ const partners = [];
59
+ for (const p of union.partners) {
60
+ const mover = local[p.mover];
61
+ const anchor = local[p.anchor];
62
+ if (mover >= 0 && anchor >= 0)
63
+ partners.push({ mover, anchor });
64
+ }
65
+ return {
66
+ n,
67
+ nodes,
68
+ indexOf,
69
+ radius,
70
+ boxHalfWidth,
71
+ boxTop,
72
+ boxBottom,
73
+ boxArea,
74
+ crowding,
75
+ degree,
76
+ pairs,
77
+ offsets,
78
+ adjacency,
79
+ partners,
80
+ };
81
+ }
82
+ //# sourceMappingURL=graph.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graph.js","sourceRoot":"","sources":["../src/graph.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,162 @@
1
+ /**
2
+ * @kgdiff/layout: stable, deterministic, label-aware layout for graph diffs.
3
+ *
4
+ * Both versions are drawn on ONE set of coordinates (the union layout), so
5
+ * nothing jumps when switching between before / after / overlay / replay.
6
+ * See docs/algorithms.md §Layout for the algorithm and quality bars.
7
+ *
8
+ * Signatures are the package contract; bodies live in the modules imported below.
9
+ */
10
+ import type { DiffResult, Point, Rect, UnionId } from '@kgdiff/core';
11
+ export type LayoutStrategy = 'union' | 'anchored';
12
+ export type LayoutScope = 'all' | 'changes';
13
+ export interface LayoutOptions {
14
+ /**
15
+ * - `union`: lay out the union graph so neither version is favoured (default when no node is pinned).
16
+ * - `anchored`: keep existing positions (pins, or `position` on before-version nodes) fixed and
17
+ * place only the remaining nodes (default when at least one position is known).
18
+ */
19
+ readonly strategy?: LayoutStrategy;
20
+ /**
21
+ * - `all`: every union node gets a position.
22
+ * - `changes`: only changed nodes, the endpoints of changed edges and `contextHops` of unchanged
23
+ * context around changed nodes, not expanded through hubs (see `hubDegree`). Default when the
24
+ * union has more than `largeGraphThreshold` nodes. Hidden neighbours are counted in `hidden`.
25
+ */
26
+ readonly scope?: LayoutScope;
27
+ /** Context hops kept around changes when `scope` is `changes`. Default 1. */
28
+ readonly contextHops?: number;
29
+ /**
30
+ * Scope `changes`: context is never expanded FROM a node whose degree (max over both
31
+ * versions) exceeds this, so one changed hub (a class with thousands of instances) does not pull
32
+ * its whole neighbourhood into the layout. The hub itself stays in scope when it is a seed or
33
+ * within reach; neighbours that could only enter through it are counted in `hidden`. Changed
34
+ * nodes are always in scope. Default 40; `Infinity` disables the rule.
35
+ */
36
+ readonly hubDegree?: number;
37
+ /** Default 3000 nodes. */
38
+ readonly largeGraphThreshold?: number;
39
+ /** PRNG seed. Same seed + same input → identical positions. Default 1. */
40
+ readonly seed?: number;
41
+ /** Simulation ticks. Default: chosen from graph size (docs/algorithms.md). */
42
+ readonly iterations?: number;
43
+ /** Target edge length in world units. Default 56. */
44
+ readonly linkDistance?: number;
45
+ /** Reserve room for labels so they do not overlap. Default true. */
46
+ readonly labelAware?: boolean;
47
+ /** Font size used to estimate label boxes (world units at zoom 1). Default 12. */
48
+ readonly fontSize?: number;
49
+ /** Explicit positions; they win over `position` found on graph nodes. */
50
+ readonly pinned?: ReadonlyMap<UnionId, Point>;
51
+ /** Gap between packed connected components. Default 96. */
52
+ readonly componentGap?: number;
53
+ }
54
+ export interface LayoutComponent {
55
+ readonly nodes: readonly UnionId[];
56
+ readonly bounds: Rect;
57
+ }
58
+ export interface LayoutResult {
59
+ /** Positions of every node in scope (world units, y down). */
60
+ readonly positions: ReadonlyMap<UnionId, Point>;
61
+ /** Radius of every node in scope (`nodeRadius` of its max degree across versions). */
62
+ readonly radii: ReadonlyMap<UnionId, number>;
63
+ /** Bounds of all nodes in scope, including estimated label boxes. */
64
+ readonly bounds: Rect;
65
+ readonly scope: LayoutScope;
66
+ /** For scope `changes`: per visible node, number of neighbours left out of the layout. */
67
+ readonly hidden: ReadonlyMap<UnionId, number>;
68
+ /** Connected components after packing, largest first. */
69
+ readonly components: readonly LayoutComponent[];
70
+ readonly stats: {
71
+ readonly strategy: LayoutStrategy;
72
+ readonly iterations: number;
73
+ readonly nodes: number;
74
+ /** Wall-clock time. The only non-deterministic field; never serialized. */
75
+ readonly ms: number;
76
+ };
77
+ }
78
+ /** Deterministic JSON transport (no timing information). */
79
+ export interface LayoutJSON {
80
+ readonly format: 'kgdiff.layout';
81
+ readonly version: 1;
82
+ readonly strategy: LayoutStrategy;
83
+ readonly scope: LayoutScope;
84
+ readonly iterations: number;
85
+ /** Coordinates rounded to 0.01. */
86
+ readonly positions: Readonly<Record<UnionId, readonly [number, number]>>;
87
+ readonly radii: Readonly<Record<UnionId, number>>;
88
+ readonly hidden: Readonly<Record<UnionId, number>>;
89
+ readonly bounds: Rect;
90
+ readonly components: readonly LayoutComponent[];
91
+ }
92
+ /**
93
+ * Compute the shared layout for a diff. Pure and deterministic: same diff + same options →
94
+ * identical `layoutToJSON` output, in Node and in every browser.
95
+ *
96
+ * Steps (docs/algorithms.md §12): select the scope (`changes` above `largeGraphThreshold`
97
+ * nodes), pick the strategy (`anchored` when any position is known), lay out every connected
98
+ * component with a seeded force simulation (BFS phyllotaxis start, degree-aware links, many-body,
99
+ * label-aware rectangle collision, weak merge / split springs), pack components to ~16:10
100
+ * (isolated nodes in a compact grid last), center at the origin (union only) and round to 0.01.
101
+ *
102
+ * - The graph laid out has one link per node pair connected in either version (retargeted and
103
+ * carried edges contribute their before form too), so neither version is favoured.
104
+ * - Scope `changes` keeps changed nodes, both endpoints of changed edges (either version) and
105
+ * the nodes within `contextHops` of a changed node, never expanding from hubs (degree >
106
+ * `hubDegree`); `hidden` only lists visible nodes with ≥ 1 hidden neighbour.
107
+ * - Known positions (`pinned`, then the before node's `position`, then the after node's) are kept
108
+ * exactly (unrounded) in `anchored` mode; `union` ignores them. Nothing is centered when
109
+ * anchored, and components without any known node are packed beside the anchored region.
110
+ * - Label boxes: centered below the node (`r + 5` below its center), `estimateLabelWidth` wide
111
+ * (capped at 32 em), one line of 1.25 em, plus the struck old name for renamed nodes. Without
112
+ * `labelAware` only node circles collide, but bounds still include the labels.
113
+ * - Label-aware extras beyond the spec values (docs in `forces/params.ts`): many-body strength
114
+ * scaled by `(linkDistance / 56)²` and, for long labels, by their box area; links of hubs
115
+ * crowded with labelled neighbours relax; a weak size-dependent pull keeps components compact;
116
+ * merge sources / split targets left far from their partner are docked next to it.
117
+ *
118
+ * Ticks per simulation (one per connected component, or the window around new nodes): the spec
119
+ * formula up to 2000 nodes, then `max(30, 120 · 2000 / N)` so large components stay within
120
+ * budget (`iterations` overrides both; `stats.iterations` reports the largest simulation's).
121
+ *
122
+ * Complexity: O(I · N log N) for I iterations (Barnes–Hut charge, quadtree collision).
123
+ * Budget: 2k nodes < 800 ms (measured ~0.6–0.7 s on an M-class laptop; 5k ≈ 1 s, 10k ≈ 1.6 s).
124
+ *
125
+ * @throws KgdiffError `invalid-input` for invalid options (negative distances, non-integer
126
+ * iterations, non-finite pins…).
127
+ */
128
+ export declare function layoutDiff(diff: DiffResult, options?: LayoutOptions): LayoutResult;
129
+ /**
130
+ * Add `nodes` (currently outside the layout) next to their placed neighbours without moving any
131
+ * node already placed. Used when the user expands a collapsed group in changes-only views.
132
+ *
133
+ * Already placed ids are ignored (returns `layout` itself when nothing is new). New nodes with no
134
+ * path to a placed node are laid out on their own and packed beside the existing bounds. `hidden`
135
+ * is recounted for scope `changes`; `stats.strategy` / `stats.iterations` are kept from `layout`.
136
+ *
137
+ * @throws KgdiffError `invalid-input` when an id is not a node of `diff`.
138
+ */
139
+ export declare function extendLayout(diff: DiffResult, layout: LayoutResult, nodes: readonly UnionId[], options?: LayoutOptions): LayoutResult;
140
+ /** Node radius in world units from degree: `clamp(4 + 2.2·√degree, 4, 14)`. Shared by every renderer. */
141
+ export declare function nodeRadius(degree: number): number;
142
+ /**
143
+ * Deterministic label width estimate (no DOM): CJK/full-width = 1em, Latin ≈ 0.56em, digits 0.6em.
144
+ *
145
+ * Sum of per-code-point advances × `fontSize`: CJK / full-width / emoji 1.0em, digits 0.6em,
146
+ * uppercase 0.68em, space 0.3em, other characters 0.56em; combining marks, joiners and variation
147
+ * selectors 0em; a regional-indicator pair (flag) 1em. O(length).
148
+ */
149
+ export declare function estimateLabelWidth(text: string, fontSize: number): number;
150
+ /**
151
+ * Deterministic JSON form: keys sorted by id, coordinates and radii rounded to 0.01, no timing.
152
+ * `layoutToJSON(layoutFromJSON(json))` reproduces `json`.
153
+ */
154
+ export declare function layoutToJSON(layout: LayoutResult): LayoutJSON;
155
+ /**
156
+ * Rebuild a `LayoutResult` from `layoutToJSON` output (`stats.ms` is 0).
157
+ *
158
+ * @throws KgdiffError `invalid-input` for malformed input, `unsupported-version` for an unknown
159
+ * version.
160
+ */
161
+ export declare function layoutFromJSON(json: LayoutJSON): LayoutResult;
162
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js ADDED
@@ -0,0 +1,87 @@
1
+ import { extend } from './extend.js';
2
+ import { fromJSON, toJSON } from './json.js';
3
+ import { computeLayout } from './layout.js';
4
+ import { labelWidth } from './text-metrics.js';
5
+ /**
6
+ * Compute the shared layout for a diff. Pure and deterministic: same diff + same options →
7
+ * identical `layoutToJSON` output, in Node and in every browser.
8
+ *
9
+ * Steps (docs/algorithms.md §12): select the scope (`changes` above `largeGraphThreshold`
10
+ * nodes), pick the strategy (`anchored` when any position is known), lay out every connected
11
+ * component with a seeded force simulation (BFS phyllotaxis start, degree-aware links, many-body,
12
+ * label-aware rectangle collision, weak merge / split springs), pack components to ~16:10
13
+ * (isolated nodes in a compact grid last), center at the origin (union only) and round to 0.01.
14
+ *
15
+ * - The graph laid out has one link per node pair connected in either version (retargeted and
16
+ * carried edges contribute their before form too), so neither version is favoured.
17
+ * - Scope `changes` keeps changed nodes, both endpoints of changed edges (either version) and
18
+ * the nodes within `contextHops` of a changed node, never expanding from hubs (degree >
19
+ * `hubDegree`); `hidden` only lists visible nodes with ≥ 1 hidden neighbour.
20
+ * - Known positions (`pinned`, then the before node's `position`, then the after node's) are kept
21
+ * exactly (unrounded) in `anchored` mode; `union` ignores them. Nothing is centered when
22
+ * anchored, and components without any known node are packed beside the anchored region.
23
+ * - Label boxes: centered below the node (`r + 5` below its center), `estimateLabelWidth` wide
24
+ * (capped at 32 em), one line of 1.25 em, plus the struck old name for renamed nodes. Without
25
+ * `labelAware` only node circles collide, but bounds still include the labels.
26
+ * - Label-aware extras beyond the spec values (docs in `forces/params.ts`): many-body strength
27
+ * scaled by `(linkDistance / 56)²` and, for long labels, by their box area; links of hubs
28
+ * crowded with labelled neighbours relax; a weak size-dependent pull keeps components compact;
29
+ * merge sources / split targets left far from their partner are docked next to it.
30
+ *
31
+ * Ticks per simulation (one per connected component, or the window around new nodes): the spec
32
+ * formula up to 2000 nodes, then `max(30, 120 · 2000 / N)` so large components stay within
33
+ * budget (`iterations` overrides both; `stats.iterations` reports the largest simulation's).
34
+ *
35
+ * Complexity: O(I · N log N) for I iterations (Barnes–Hut charge, quadtree collision).
36
+ * Budget: 2k nodes < 800 ms (measured ~0.6–0.7 s on an M-class laptop; 5k ≈ 1 s, 10k ≈ 1.6 s).
37
+ *
38
+ * @throws KgdiffError `invalid-input` for invalid options (negative distances, non-integer
39
+ * iterations, non-finite pins…).
40
+ */
41
+ export function layoutDiff(diff, options) {
42
+ return computeLayout(diff, options, nodeRadius);
43
+ }
44
+ /**
45
+ * Add `nodes` (currently outside the layout) next to their placed neighbours without moving any
46
+ * node already placed. Used when the user expands a collapsed group in changes-only views.
47
+ *
48
+ * Already placed ids are ignored (returns `layout` itself when nothing is new). New nodes with no
49
+ * path to a placed node are laid out on their own and packed beside the existing bounds. `hidden`
50
+ * is recounted for scope `changes`; `stats.strategy` / `stats.iterations` are kept from `layout`.
51
+ *
52
+ * @throws KgdiffError `invalid-input` when an id is not a node of `diff`.
53
+ */
54
+ export function extendLayout(diff, layout, nodes, options) {
55
+ return extend(diff, layout, nodes, options, nodeRadius);
56
+ }
57
+ /** Node radius in world units from degree: `clamp(4 + 2.2·√degree, 4, 14)`. Shared by every renderer. */
58
+ export function nodeRadius(degree) {
59
+ return Math.min(14, Math.max(4, 4 + 2.2 * Math.sqrt(Math.max(0, degree))));
60
+ }
61
+ /**
62
+ * Deterministic label width estimate (no DOM): CJK/full-width = 1em, Latin ≈ 0.56em, digits 0.6em.
63
+ *
64
+ * Sum of per-code-point advances × `fontSize`: CJK / full-width / emoji 1.0em, digits 0.6em,
65
+ * uppercase 0.68em, space 0.3em, other characters 0.56em; combining marks, joiners and variation
66
+ * selectors 0em; a regional-indicator pair (flag) 1em. O(length).
67
+ */
68
+ export function estimateLabelWidth(text, fontSize) {
69
+ return labelWidth(text, fontSize);
70
+ }
71
+ /**
72
+ * Deterministic JSON form: keys sorted by id, coordinates and radii rounded to 0.01, no timing.
73
+ * `layoutToJSON(layoutFromJSON(json))` reproduces `json`.
74
+ */
75
+ export function layoutToJSON(layout) {
76
+ return toJSON(layout);
77
+ }
78
+ /**
79
+ * Rebuild a `LayoutResult` from `layoutToJSON` output (`stats.ms` is 0).
80
+ *
81
+ * @throws KgdiffError `invalid-input` for malformed input, `unsupported-version` for an unknown
82
+ * version.
83
+ */
84
+ export function layoutFromJSON(json) {
85
+ return fromJSON(json);
86
+ }
87
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAwF/C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,MAAM,UAAU,UAAU,CAAC,IAAgB,EAAE,OAAuB;IAClE,OAAO,aAAa,CAAC,IAAI,EAAE,OAAO,EAAE,UAAU,CAAC,CAAC;AAClD,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,YAAY,CAC1B,IAAgB,EAChB,MAAoB,EACpB,KAAyB,EACzB,OAAuB;IAEvB,OAAO,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,UAAU,CAAC,CAAC;AAC1D,CAAC;AAED,yGAAyG;AACzG,MAAM,UAAU,UAAU,CAAC,MAAc;IACvC,OAAO,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,GAAG,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;AAC7E,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAAY,EAAE,QAAgB;IAC/D,OAAO,UAAU,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;AACpC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,MAAoB;IAC/C,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC;AACxB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,IAAgB;IAC7C,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC;AACxB,CAAC"}