@variance-authority/presentation 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 (67) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/LICENSE +21 -0
  3. package/README.md +319 -0
  4. package/dist/alignment.d.ts +9 -0
  5. package/dist/alignment.js +100 -0
  6. package/dist/alignment.js.map +1 -0
  7. package/dist/analyze.d.ts +20 -0
  8. package/dist/analyze.js +132 -0
  9. package/dist/analyze.js.map +1 -0
  10. package/dist/browser-agent-entry.d.ts +2 -0
  11. package/dist/browser-agent-entry.js +8 -0
  12. package/dist/browser-agent-entry.js.map +1 -0
  13. package/dist/browser-agent.bundle.js +3587 -0
  14. package/dist/browser-agent.d.ts +37 -0
  15. package/dist/browser-agent.js +130 -0
  16. package/dist/browser-agent.js.map +1 -0
  17. package/dist/bundle.d.ts +3 -0
  18. package/dist/bundle.js +20 -0
  19. package/dist/bundle.js.map +1 -0
  20. package/dist/compare.d.ts +4 -0
  21. package/dist/compare.js +49 -0
  22. package/dist/compare.js.map +1 -0
  23. package/dist/findings.d.ts +5 -0
  24. package/dist/findings.js +285 -0
  25. package/dist/findings.js.map +1 -0
  26. package/dist/focus.d.ts +17 -0
  27. package/dist/focus.js +84 -0
  28. package/dist/focus.js.map +1 -0
  29. package/dist/graph.d.ts +11 -0
  30. package/dist/graph.js +145 -0
  31. package/dist/graph.js.map +1 -0
  32. package/dist/hierarchy.d.ts +4 -0
  33. package/dist/hierarchy.js +171 -0
  34. package/dist/hierarchy.js.map +1 -0
  35. package/dist/index.d.ts +12 -0
  36. package/dist/index.js +8 -0
  37. package/dist/index.js.map +1 -0
  38. package/dist/math.d.ts +24 -0
  39. package/dist/math.js +124 -0
  40. package/dist/math.js.map +1 -0
  41. package/dist/measure.d.ts +15 -0
  42. package/dist/measure.js +216 -0
  43. package/dist/measure.js.map +1 -0
  44. package/dist/model.d.ts +346 -0
  45. package/dist/model.js +2 -0
  46. package/dist/model.js.map +1 -0
  47. package/dist/paint.d.ts +12 -0
  48. package/dist/paint.js +192 -0
  49. package/dist/paint.js.map +1 -0
  50. package/dist/patterns.d.ts +10 -0
  51. package/dist/patterns.js +148 -0
  52. package/dist/patterns.js.map +1 -0
  53. package/dist/playwright.d.ts +27 -0
  54. package/dist/playwright.js +108 -0
  55. package/dist/playwright.js.map +1 -0
  56. package/dist/report.d.ts +17 -0
  57. package/dist/report.js +126 -0
  58. package/dist/report.js.map +1 -0
  59. package/dist/spacing.d.ts +4 -0
  60. package/dist/spacing.js +112 -0
  61. package/dist/spacing.js.map +1 -0
  62. package/dist/telemetry.d.ts +4 -0
  63. package/dist/telemetry.js +73 -0
  64. package/dist/telemetry.js.map +1 -0
  65. package/mark.svg +30 -0
  66. package/package.json +62 -0
  67. package/skills/variance-presentation/SKILL.md +463 -0
package/dist/math.d.ts ADDED
@@ -0,0 +1,24 @@
1
+ import type { Rect } from '@variance-authority/core';
2
+ export declare const round: (value: number, places?: number) => number;
3
+ export declare const codeUnitCompare: (left: string, right: string) => number;
4
+ export declare function median(values: readonly number[]): number | undefined;
5
+ export declare function numeric(value: string | undefined): number | undefined;
6
+ interface Color {
7
+ readonly red: number;
8
+ readonly green: number;
9
+ readonly blue: number;
10
+ readonly alpha: number;
11
+ }
12
+ export declare function colorOf(value: string | undefined): Color | undefined;
13
+ export declare function opaque(value: string | undefined): string | undefined;
14
+ /** CIE76 distance after sRGB → Lab. Useful as boundary evidence, not WCAG contrast. */
15
+ export declare function perceptualDifference(left: string, right: string): number | undefined;
16
+ export declare function luminance(value: string | undefined): number | undefined;
17
+ export declare function gapBetween(left: Rect, right: Rect): {
18
+ readonly axis: 'horizontal' | 'vertical' | 'overlap';
19
+ readonly distance: number;
20
+ };
21
+ export declare function unionArea(rects: readonly Rect[]): number;
22
+ export declare function stableEntries(value: Readonly<Record<string, unknown>>): string;
23
+ export {};
24
+ //# sourceMappingURL=math.d.ts.map
package/dist/math.js ADDED
@@ -0,0 +1,124 @@
1
+ export const round = (value, places = 3) => {
2
+ const scale = 10 ** places;
3
+ return Math.round(value * scale) / scale;
4
+ };
5
+ export const codeUnitCompare = (left, right) => left < right ? -1 : left > right ? 1 : 0;
6
+ export function median(values) {
7
+ if (values.length === 0)
8
+ return undefined;
9
+ const sorted = [...values].sort((a, b) => a - b);
10
+ const middle = Math.floor(sorted.length / 2);
11
+ return sorted.length % 2 === 0
12
+ ? (sorted[middle - 1] + sorted[middle]) / 2
13
+ : sorted[middle];
14
+ }
15
+ export function numeric(value) {
16
+ if (value === undefined)
17
+ return undefined;
18
+ const parsed = Number.parseFloat(value);
19
+ return Number.isFinite(parsed) ? parsed : undefined;
20
+ }
21
+ export function colorOf(value) {
22
+ if (value === undefined || value === 'transparent')
23
+ return undefined;
24
+ const components = value.match(/[\d.]+/g)?.map(Number);
25
+ if (components === undefined || components.length < 3)
26
+ return undefined;
27
+ const [red, green, blue, alpha = 1] = components;
28
+ if (red === undefined || green === undefined || blue === undefined)
29
+ return undefined;
30
+ return { red, green, blue, alpha };
31
+ }
32
+ export function opaque(value) {
33
+ const color = colorOf(value);
34
+ return color !== undefined && color.alpha > 0.01 ? value : undefined;
35
+ }
36
+ /** CIE76 distance after sRGB → Lab. Useful as boundary evidence, not WCAG contrast. */
37
+ export function perceptualDifference(left, right) {
38
+ const a = colorOf(left);
39
+ const b = colorOf(right);
40
+ if (a === undefined || b === undefined)
41
+ return undefined;
42
+ const [l1, a1, b1] = lab(a);
43
+ const [l2, a2, b2] = lab(b);
44
+ return round(Math.hypot(l1 - l2, a1 - a2, b1 - b2), 2);
45
+ }
46
+ export function luminance(value) {
47
+ const color = colorOf(value);
48
+ if (color === undefined)
49
+ return undefined;
50
+ const channel = (part) => {
51
+ const scaled = part / 255;
52
+ return scaled <= 0.04045 ? scaled / 12.92 : ((scaled + 0.055) / 1.055) ** 2.4;
53
+ };
54
+ return round(channel(color.red) * 0.2126 + channel(color.green) * 0.7152 + channel(color.blue) * 0.0722);
55
+ }
56
+ function lab(color) {
57
+ const linear = (part) => {
58
+ const scaled = part / 255;
59
+ return scaled <= 0.04045 ? scaled / 12.92 : ((scaled + 0.055) / 1.055) ** 2.4;
60
+ };
61
+ const red = linear(color.red);
62
+ const green = linear(color.green);
63
+ const blue = linear(color.blue);
64
+ const x = (red * 0.4124 + green * 0.3576 + blue * 0.1805) / 0.95047;
65
+ const y = (red * 0.2126 + green * 0.7152 + blue * 0.0722) / 1;
66
+ const z = (red * 0.0193 + green * 0.1192 + blue * 0.9505) / 1.08883;
67
+ const curve = (part) => part > 0.008856 ? Math.cbrt(part) : 7.787 * part + 16 / 116;
68
+ const fx = curve(x);
69
+ const fy = curve(y);
70
+ const fz = curve(z);
71
+ return [116 * fy - 16, 500 * (fx - fy), 200 * (fy - fz)];
72
+ }
73
+ export function gapBetween(left, right) {
74
+ const horizontal = Math.max(left.x, right.x) - Math.min(left.x + left.width, right.x + right.width);
75
+ const vertical = Math.max(left.y, right.y) - Math.min(left.y + left.height, right.y + right.height);
76
+ if (vertical >= 0 && vertical >= horizontal)
77
+ return { axis: 'vertical', distance: round(vertical, 2) };
78
+ if (horizontal >= 0)
79
+ return { axis: 'horizontal', distance: round(horizontal, 2) };
80
+ return { axis: 'overlap', distance: 0 };
81
+ }
82
+ export function unionArea(rects) {
83
+ const usable = rects.filter((rect) => rect.width > 0 && rect.height > 0);
84
+ const xs = [...new Set(usable.flatMap((rect) => [rect.x, rect.x + rect.width]))].sort((a, b) => a - b);
85
+ let area = 0;
86
+ for (let index = 0; index < xs.length - 1; index += 1) {
87
+ const from = xs[index];
88
+ const to = xs[index + 1];
89
+ if (to <= from)
90
+ continue;
91
+ const spans = usable
92
+ .filter((rect) => rect.x < to && rect.x + rect.width > from)
93
+ .map((rect) => [rect.y, rect.y + rect.height])
94
+ .sort((a, b) => a[0] - b[0]);
95
+ let covered = 0;
96
+ let start;
97
+ let end;
98
+ for (const [nextStart, nextEnd] of spans) {
99
+ if (start === undefined || end === undefined) {
100
+ start = nextStart;
101
+ end = nextEnd;
102
+ }
103
+ else if (nextStart > end) {
104
+ covered += end - start;
105
+ start = nextStart;
106
+ end = nextEnd;
107
+ }
108
+ else {
109
+ end = Math.max(end, nextEnd);
110
+ }
111
+ }
112
+ if (start !== undefined && end !== undefined)
113
+ covered += end - start;
114
+ area += (to - from) * covered;
115
+ }
116
+ return round(area, 2);
117
+ }
118
+ export function stableEntries(value) {
119
+ return Object.keys(value)
120
+ .sort()
121
+ .map((key) => `${key}:${String(value[key])}`)
122
+ .join('|');
123
+ }
124
+ //# sourceMappingURL=math.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"math.js","sourceRoot":"","sources":["../src/math.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,KAAK,GAAG,CAAC,KAAa,EAAE,MAAM,GAAG,CAAC,EAAU,EAAE;IACzD,MAAM,KAAK,GAAG,EAAE,IAAI,MAAM,CAAC;IAC3B,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC,GAAG,KAAK,CAAC;AAC3C,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,IAAY,EAAE,KAAa,EAAU,EAAE,CACrE,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAE3C,MAAM,UAAU,MAAM,CAAC,MAAyB;IAC9C,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IAC1C,MAAM,MAAM,GAAG,CAAC,GAAG,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IACjD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAC7C,OAAO,MAAM,CAAC,MAAM,GAAG,CAAC,KAAK,CAAC;QAC5B,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAE,GAAG,MAAM,CAAC,MAAM,CAAE,CAAC,GAAG,CAAC;QAC7C,CAAC,CAAC,MAAM,CAAC,MAAM,CAAE,CAAC;AACtB,CAAC;AAED,MAAM,UAAU,OAAO,CAAC,KAAyB;IAC/C,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IAC1C,MAAM,MAAM,GAAG,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;IACxC,OAAO,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC;AACtD,CAAC;AASD,MAAM,UAAU,OAAO,CAAC,KAAyB;IAC/C,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,aAAa;QAAE,OAAO,SAAS,CAAC;IACrE,MAAM,UAAU,GAAG,KAAK,CAAC,KAAK,CAAC,SAAS,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACvD,IAAI,UAAU,KAAK,SAAS,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,SAAS,CAAC;IACxE,MAAM,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,GAAG,CAAC,CAAC,GAAG,UAAU,CAAC;IACjD,IAAI,GAAG,KAAK,SAAS,IAAI,KAAK,KAAK,SAAS,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IACrF,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;AACrC,CAAC;AAED,MAAM,UAAU,MAAM,CAAC,KAAyB;IAC9C,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC;IAC7B,OAAO,KAAK,KAAK,SAAS,IAAI,KAAK,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;AACvE,CAAC;AAED,uFAAuF;AACvF,MAAM,UAAU,oBAAoB,CAAC,IAAY,EAAE,KAAa;IAC9D,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxB,MAAM,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC;IACzB,IAAI,CAAC,KAAK,SAAS,IAAI,CAAC,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IACzD,MAAM,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC;IAC5B,MAAM,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC;IAC5B,OAAO,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;AACzD,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,KAAyB;IACjD,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC;IAC7B,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IAC1C,MAAM,OAAO,GAAG,CAAC,IAAY,EAAU,EAAE;QACvC,MAAM,MAAM,GAAG,IAAI,GAAG,GAAG,CAAC;QAC1B,OAAO,MAAM,IAAI,OAAO,CAAC,CAAC,CAAC,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,KAAK,CAAC,GAAG,KAAK,CAAC,IAAI,GAAG,CAAC;IAChF,CAAC,CAAC;IACF,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,CAAC;AAC3G,CAAC;AAED,SAAS,GAAG,CAAC,KAAY;IACvB,MAAM,MAAM,GAAG,CAAC,IAAY,EAAU,EAAE;QACtC,MAAM,MAAM,GAAG,IAAI,GAAG,GAAG,CAAC;QAC1B,OAAO,MAAM,IAAI,OAAO,CAAC,CAAC,CAAC,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,KAAK,CAAC,GAAG,KAAK,CAAC,IAAI,GAAG,CAAC;IAChF,CAAC,CAAC;IACF,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC9B,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAClC,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAChC,MAAM,CAAC,GAAG,CAAC,GAAG,GAAG,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,IAAI,GAAG,MAAM,CAAC,GAAG,OAAO,CAAC;IACpE,MAAM,CAAC,GAAG,CAAC,GAAG,GAAG,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;IAC9D,MAAM,CAAC,GAAG,CAAC,GAAG,GAAG,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,IAAI,GAAG,MAAM,CAAC,GAAG,OAAO,CAAC;IACpE,MAAM,KAAK,GAAG,CAAC,IAAY,EAAU,EAAE,CACrC,IAAI,GAAG,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG,IAAI,GAAG,EAAE,GAAG,GAAG,CAAC;IAC9D,MAAM,EAAE,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;IACpB,MAAM,EAAE,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;IACpB,MAAM,EAAE,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;IACpB,OAAO,CAAC,GAAG,GAAG,EAAE,GAAG,EAAE,EAAE,GAAG,GAAG,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,GAAG,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC;AAC3D,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,IAAU,EAAE,KAAW;IAIhD,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC;IACpG,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC;IACpG,IAAI,QAAQ,IAAI,CAAC,IAAI,QAAQ,IAAI,UAAU;QAAE,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC,EAAE,CAAC;IACvG,IAAI,UAAU,IAAI,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,QAAQ,EAAE,KAAK,CAAC,UAAU,EAAE,CAAC,CAAC,EAAE,CAAC;IACnF,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC;AAC1C,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,KAAsB;IAC9C,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IACzE,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IACvG,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,EAAE,CAAC,MAAM,GAAG,CAAC,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC;QACtD,MAAM,IAAI,GAAG,EAAE,CAAC,KAAK,CAAE,CAAC;QACxB,MAAM,EAAE,GAAG,EAAE,CAAC,KAAK,GAAG,CAAC,CAAE,CAAC;QAC1B,IAAI,EAAE,IAAI,IAAI;YAAE,SAAS;QACzB,MAAM,KAAK,GAAG,MAAM;aACjB,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,IAAI,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;aAC3D,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,MAAM,CAAU,CAAC;aACtD,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC/B,IAAI,OAAO,GAAG,CAAC,CAAC;QAChB,IAAI,KAAyB,CAAC;QAC9B,IAAI,GAAuB,CAAC;QAC5B,KAAK,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,IAAI,KAAK,EAAE,CAAC;YACzC,IAAI,KAAK,KAAK,SAAS,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;gBAC7C,KAAK,GAAG,SAAS,CAAC;gBAClB,GAAG,GAAG,OAAO,CAAC;YAChB,CAAC;iBAAM,IAAI,SAAS,GAAG,GAAG,EAAE,CAAC;gBAC3B,OAAO,IAAI,GAAG,GAAG,KAAK,CAAC;gBACvB,KAAK,GAAG,SAAS,CAAC;gBAClB,GAAG,GAAG,OAAO,CAAC;YAChB,CAAC;iBAAM,CAAC;gBACN,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;YAC/B,CAAC;QACH,CAAC;QACD,IAAI,KAAK,KAAK,SAAS,IAAI,GAAG,KAAK,SAAS;YAAE,OAAO,IAAI,GAAG,GAAG,KAAK,CAAC;QACrE,IAAI,IAAI,CAAC,EAAE,GAAG,IAAI,CAAC,GAAG,OAAO,CAAC;IAChC,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;AACxB,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,KAAwC;IACpE,OAAO,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC;SACtB,IAAI,EAAE;SACN,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,GAAG,IAAI,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;SAC5C,IAAI,CAAC,GAAG,CAAC,CAAC;AACf,CAAC","sourcesContent":["import type { Rect } from '@variance-authority/core';\n\nexport const round = (value: number, places = 3): number => {\n const scale = 10 ** places;\n return Math.round(value * scale) / scale;\n};\n\nexport const codeUnitCompare = (left: string, right: string): number =>\n left < right ? -1 : left > right ? 1 : 0;\n\nexport function median(values: readonly number[]): number | undefined {\n if (values.length === 0) return undefined;\n const sorted = [...values].sort((a, b) => a - b);\n const middle = Math.floor(sorted.length / 2);\n return sorted.length % 2 === 0\n ? (sorted[middle - 1]! + sorted[middle]!) / 2\n : sorted[middle]!;\n}\n\nexport function numeric(value: string | undefined): number | undefined {\n if (value === undefined) return undefined;\n const parsed = Number.parseFloat(value);\n return Number.isFinite(parsed) ? parsed : undefined;\n}\n\ninterface Color {\n readonly red: number;\n readonly green: number;\n readonly blue: number;\n readonly alpha: number;\n}\n\nexport function colorOf(value: string | undefined): Color | undefined {\n if (value === undefined || value === 'transparent') return undefined;\n const components = value.match(/[\\d.]+/g)?.map(Number);\n if (components === undefined || components.length < 3) return undefined;\n const [red, green, blue, alpha = 1] = components;\n if (red === undefined || green === undefined || blue === undefined) return undefined;\n return { red, green, blue, alpha };\n}\n\nexport function opaque(value: string | undefined): string | undefined {\n const color = colorOf(value);\n return color !== undefined && color.alpha > 0.01 ? value : undefined;\n}\n\n/** CIE76 distance after sRGB → Lab. Useful as boundary evidence, not WCAG contrast. */\nexport function perceptualDifference(left: string, right: string): number | undefined {\n const a = colorOf(left);\n const b = colorOf(right);\n if (a === undefined || b === undefined) return undefined;\n const [l1, a1, b1] = lab(a);\n const [l2, a2, b2] = lab(b);\n return round(Math.hypot(l1 - l2, a1 - a2, b1 - b2), 2);\n}\n\nexport function luminance(value: string | undefined): number | undefined {\n const color = colorOf(value);\n if (color === undefined) return undefined;\n const channel = (part: number): number => {\n const scaled = part / 255;\n return scaled <= 0.04045 ? scaled / 12.92 : ((scaled + 0.055) / 1.055) ** 2.4;\n };\n return round(channel(color.red) * 0.2126 + channel(color.green) * 0.7152 + channel(color.blue) * 0.0722);\n}\n\nfunction lab(color: Color): readonly [number, number, number] {\n const linear = (part: number): number => {\n const scaled = part / 255;\n return scaled <= 0.04045 ? scaled / 12.92 : ((scaled + 0.055) / 1.055) ** 2.4;\n };\n const red = linear(color.red);\n const green = linear(color.green);\n const blue = linear(color.blue);\n const x = (red * 0.4124 + green * 0.3576 + blue * 0.1805) / 0.95047;\n const y = (red * 0.2126 + green * 0.7152 + blue * 0.0722) / 1;\n const z = (red * 0.0193 + green * 0.1192 + blue * 0.9505) / 1.08883;\n const curve = (part: number): number =>\n part > 0.008856 ? Math.cbrt(part) : 7.787 * part + 16 / 116;\n const fx = curve(x);\n const fy = curve(y);\n const fz = curve(z);\n return [116 * fy - 16, 500 * (fx - fy), 200 * (fy - fz)];\n}\n\nexport function gapBetween(left: Rect, right: Rect): {\n readonly axis: 'horizontal' | 'vertical' | 'overlap';\n readonly distance: number;\n} {\n const horizontal = Math.max(left.x, right.x) - Math.min(left.x + left.width, right.x + right.width);\n const vertical = Math.max(left.y, right.y) - Math.min(left.y + left.height, right.y + right.height);\n if (vertical >= 0 && vertical >= horizontal) return { axis: 'vertical', distance: round(vertical, 2) };\n if (horizontal >= 0) return { axis: 'horizontal', distance: round(horizontal, 2) };\n return { axis: 'overlap', distance: 0 };\n}\n\nexport function unionArea(rects: readonly Rect[]): number {\n const usable = rects.filter((rect) => rect.width > 0 && rect.height > 0);\n const xs = [...new Set(usable.flatMap((rect) => [rect.x, rect.x + rect.width]))].sort((a, b) => a - b);\n let area = 0;\n for (let index = 0; index < xs.length - 1; index += 1) {\n const from = xs[index]!;\n const to = xs[index + 1]!;\n if (to <= from) continue;\n const spans = usable\n .filter((rect) => rect.x < to && rect.x + rect.width > from)\n .map((rect) => [rect.y, rect.y + rect.height] as const)\n .sort((a, b) => a[0] - b[0]);\n let covered = 0;\n let start: number | undefined;\n let end: number | undefined;\n for (const [nextStart, nextEnd] of spans) {\n if (start === undefined || end === undefined) {\n start = nextStart;\n end = nextEnd;\n } else if (nextStart > end) {\n covered += end - start;\n start = nextStart;\n end = nextEnd;\n } else {\n end = Math.max(end, nextEnd);\n }\n }\n if (start !== undefined && end !== undefined) covered += end - start;\n area += (to - from) * covered;\n }\n return round(area, 2);\n}\n\nexport function stableEntries(value: Readonly<Record<string, unknown>>): string {\n return Object.keys(value)\n .sort()\n .map((key) => `${key}:${String(value[key])}`)\n .join('|');\n}\n"]}
@@ -0,0 +1,15 @@
1
+ import type { AlignmentAxis, BaselineCluster, PresentationNode, PresentationRelation, ProminenceCluster, SpacingCluster, SurfaceGroup } from './model.js';
2
+ import type { BuiltGraph } from './graph.js';
3
+ export interface Measurements {
4
+ readonly nodes: PresentationNode[];
5
+ readonly relations: PresentationRelation[];
6
+ readonly spacing: SpacingCluster[];
7
+ readonly axes: AlignmentAxis[];
8
+ readonly baselines: BaselineCluster[];
9
+ readonly prominence: ProminenceCluster[];
10
+ readonly surfaces: SurfaceGroup[];
11
+ readonly baselineByNode: ReadonlyMap<string, number>;
12
+ readonly axisByMember: ReadonlyMap<string, readonly AlignmentAxis[]>;
13
+ }
14
+ export declare function measureGraph(graph: BuiltGraph): Measurements;
15
+ //# sourceMappingURL=measure.d.ts.map
@@ -0,0 +1,216 @@
1
+ import { codeUnitCompare, gapBetween, median, round } from './math.js';
2
+ export function measureGraph(graph) {
3
+ const separations = siblingSeparations(graph);
4
+ const spacing = numericClusters(separations.filter((relation) => (relation.distancePx ?? 0) > 0).map((relation) => relation.distancePx), 'S', (center) => Math.max(0.75, center * 0.08)).map((cluster) => ({
5
+ id: cluster.id,
6
+ centerPx: round(cluster.center, 2),
7
+ minPx: round(Math.min(...cluster.values), 2),
8
+ maxPx: round(Math.max(...cluster.values), 2),
9
+ samples: cluster.values.length,
10
+ }));
11
+ const withSpacing = separations.map((relation) => {
12
+ const cluster = spacing.find((candidate) => relation.distancePx !== undefined &&
13
+ relation.distancePx >= candidate.minPx - 0.01 &&
14
+ relation.distancePx <= candidate.maxPx + 0.01);
15
+ return cluster === undefined ? relation : { ...relation, spacingCluster: cluster.id };
16
+ });
17
+ const axes = alignmentAxes(graph.nodes);
18
+ const baselines = baselineClusters(graph.nodes);
19
+ const prominence = prominenceClusters(graph.nodes);
20
+ const surfaces = surfaceGroups(graph.nodes);
21
+ const clusterByNode = new Map();
22
+ for (const cluster of prominence)
23
+ for (const member of cluster.members)
24
+ clusterByNode.set(member, cluster.id);
25
+ const nodes = graph.nodes.map((node) => {
26
+ const cluster = clusterByNode.get(node.id);
27
+ return cluster === undefined
28
+ ? node
29
+ : { ...node, prominence: { ...node.prominence, cluster } };
30
+ });
31
+ const relations = [...graph.relations, ...withSpacing];
32
+ for (const axis of axes) {
33
+ const first = axis.members[0];
34
+ if (first === undefined)
35
+ continue;
36
+ for (const member of axis.members.slice(1)) {
37
+ relations.push({
38
+ id: `aligns:${axis.id}:${first}:${member}`,
39
+ kind: 'aligns',
40
+ from: first,
41
+ to: member,
42
+ alignment: axis.kind,
43
+ });
44
+ }
45
+ }
46
+ for (const baseline of baselines) {
47
+ const first = baseline.members[0]?.node;
48
+ if (first === undefined)
49
+ continue;
50
+ for (const member of baseline.members.slice(1)) {
51
+ relations.push({
52
+ id: `baseline:${baseline.id}:${first}:${member.node}`,
53
+ kind: 'shares-baseline',
54
+ from: first,
55
+ to: member.node,
56
+ });
57
+ }
58
+ }
59
+ const baselineByNode = new Map();
60
+ for (const baseline of baselines) {
61
+ for (const member of baseline.members) {
62
+ baselineByNode.set(member.node, baseline.coordinate + member.deviationPx);
63
+ }
64
+ }
65
+ const axisByMember = new Map();
66
+ for (const axis of axes) {
67
+ for (const member of axis.members) {
68
+ const current = axisByMember.get(member) ?? [];
69
+ current.push(axis);
70
+ axisByMember.set(member, current);
71
+ }
72
+ }
73
+ return { nodes, relations, spacing, axes, baselines, prominence, surfaces, baselineByNode, axisByMember };
74
+ }
75
+ function siblingSeparations(graph) {
76
+ const raw = [];
77
+ for (const parent of graph.nodes) {
78
+ for (let index = 0; index < parent.children.length - 1; index += 1) {
79
+ const left = graph.byId.get(parent.children[index]);
80
+ const right = graph.byId.get(parent.children[index + 1]);
81
+ if (left === undefined || right === undefined)
82
+ continue;
83
+ const gap = gapBetween(left.rect, right.rect);
84
+ raw.push({
85
+ id: `separates:${left.id}:${right.id}`,
86
+ kind: 'separates',
87
+ from: left.id,
88
+ to: right.id,
89
+ axis: gap.axis,
90
+ distancePx: gap.distance,
91
+ });
92
+ }
93
+ }
94
+ const scale = median(raw.map((relation) => relation.distancePx).filter((distance) => distance > 0)) ?? 1;
95
+ return raw.map((relation) => {
96
+ const left = graph.byId.get(relation.from);
97
+ const right = graph.byId.get(relation.to);
98
+ const whitespace = relation.distancePx / (relation.distancePx + scale);
99
+ const border = Math.min(1, Math.max(left.surface.borderWidthPx, right.surface.borderWidthPx) / 2);
100
+ const surface = surfaceDistance(left, right) / 100;
101
+ const shadow = left.surface.shadow || right.surface.shadow ? 1 : 0;
102
+ const indent = Math.min(1, Math.abs(left.rect.x - right.rect.x) / Math.max(1, scale * 2));
103
+ return {
104
+ ...relation,
105
+ boundaryStrength: round(Math.min(1, whitespace * 0.5 + border * 0.2 + surface * 0.15 + shadow * 0.1 + indent * 0.05)),
106
+ };
107
+ });
108
+ }
109
+ function surfaceDistance(left, right) {
110
+ if (left.surface.fill === undefined || right.surface.fill === undefined)
111
+ return 0;
112
+ if (left.surface.fill === right.surface.fill)
113
+ return 0;
114
+ return Math.min(100, Math.max(left.surface.perceptualDifference ?? 0, right.surface.perceptualDifference ?? 0));
115
+ }
116
+ function alignmentAxes(nodes) {
117
+ const definitions = [
118
+ ['left', (node) => node.rect.x],
119
+ ['right', (node) => node.rect.x + node.rect.width],
120
+ ['horizontal-center', (node) => node.rect.x + node.rect.width / 2],
121
+ ['top', (node) => node.rect.y],
122
+ ['bottom', (node) => node.rect.y + node.rect.height],
123
+ ['vertical-center', (node) => node.rect.y + node.rect.height / 2],
124
+ ];
125
+ const axes = [];
126
+ for (const [kind, coordinateOf] of definitions) {
127
+ const clusters = numericMembershipClusters(nodes.map((node) => ({ id: node.id, value: coordinateOf(node) })), 1).filter((cluster) => cluster.members.length >= 2);
128
+ clusters.forEach((cluster, index) => {
129
+ axes.push({
130
+ id: `A:${kind}:${index + 1}`,
131
+ kind,
132
+ coordinate: round(cluster.center, 2),
133
+ members: cluster.members.map((member) => member.id).sort(),
134
+ });
135
+ });
136
+ }
137
+ return axes;
138
+ }
139
+ function baselineClusters(nodes) {
140
+ const candidates = nodes.flatMap((node) => {
141
+ const size = node.typography.sizePx;
142
+ const lineHeight = node.typography.lineHeightPx;
143
+ if ((node.text === undefined && node.name === undefined) || size === undefined || lineHeight === undefined)
144
+ return [];
145
+ const coordinate = node.rect.y + Math.max(0, (node.rect.height - lineHeight) / 2) + size * 0.8;
146
+ return [{ id: node.id, value: coordinate }];
147
+ });
148
+ return numericMembershipClusters(candidates, 1)
149
+ .filter((cluster) => cluster.members.length >= 2)
150
+ .map((cluster, index) => ({
151
+ id: `B${index + 1}`,
152
+ coordinate: round(cluster.center, 2),
153
+ confidence: 'inferred',
154
+ members: cluster.members
155
+ .map((member) => ({ node: member.id, deviationPx: round(member.value - cluster.center, 2) }))
156
+ .sort((a, b) => codeUnitCompare(a.node, b.node)),
157
+ }));
158
+ }
159
+ function prominenceClusters(nodes) {
160
+ return numericMembershipClusters(nodes.map((node) => ({ id: node.id, value: node.prominence.magnitude })), 0.12).map((cluster, index) => {
161
+ const members = cluster.members.map((member) => member.id).sort();
162
+ const semanticClasses = [...new Set(members.map((id) => nodes.find((node) => node.id === id).semanticClass))].sort();
163
+ return {
164
+ id: `P${index + 1}`,
165
+ magnitude: round(cluster.center),
166
+ members,
167
+ semanticClasses,
168
+ };
169
+ });
170
+ }
171
+ function surfaceGroups(nodes) {
172
+ const groups = new Map();
173
+ for (const node of nodes) {
174
+ if (node.surface.fill === undefined)
175
+ continue;
176
+ const members = groups.get(node.surface.fill) ?? [];
177
+ members.push(node.id);
178
+ groups.set(node.surface.fill, members);
179
+ }
180
+ return [...groups.entries()]
181
+ .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
182
+ .map(([fill, members], index) => ({ id: `F${index + 1}`, fill, members: members.sort() }));
183
+ }
184
+ function numericClusters(values, prefix, tolerance) {
185
+ const clusters = [];
186
+ for (const value of [...values].sort((a, b) => a - b)) {
187
+ const current = clusters.at(-1);
188
+ const center = current === undefined ? undefined : median(current);
189
+ if (current === undefined || center === undefined || Math.abs(value - center) > tolerance(center)) {
190
+ clusters.push([value]);
191
+ }
192
+ else {
193
+ current.push(value);
194
+ }
195
+ }
196
+ return clusters.map((cluster, index) => ({
197
+ id: `${prefix}${index + 1}`,
198
+ center: median(cluster),
199
+ values: cluster,
200
+ }));
201
+ }
202
+ function numericMembershipClusters(values, tolerance) {
203
+ const clusters = [];
204
+ for (const value of [...values].sort((a, b) => a.value - b.value || codeUnitCompare(a.id, b.id))) {
205
+ const current = clusters.at(-1);
206
+ const center = current === undefined ? undefined : median(current.map((member) => member.value));
207
+ if (current === undefined || center === undefined || Math.abs(value.value - center) > tolerance) {
208
+ clusters.push([value]);
209
+ }
210
+ else {
211
+ current.push(value);
212
+ }
213
+ }
214
+ return clusters.map((cluster) => ({ center: median(cluster.map((member) => member.value)), members: cluster }));
215
+ }
216
+ //# sourceMappingURL=measure.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"measure.js","sourceRoot":"","sources":["../src/measure.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,WAAW,CAAC;AAcvE,MAAM,UAAU,YAAY,CAAC,KAAiB;IAC5C,MAAM,WAAW,GAAG,kBAAkB,CAAC,KAAK,CAAC,CAAC;IAC9C,MAAM,OAAO,GAAG,eAAe,CAC7B,WAAW,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,QAAQ,CAAC,UAAU,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,UAAW,CAAC,EACxG,GAAG,EACH,CAAC,MAAM,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC,CAC1C,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;QAClB,EAAE,EAAE,OAAO,CAAC,EAAE;QACd,QAAQ,EAAE,KAAK,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;QAClC,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QAC5C,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QAC5C,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM;KAC/B,CAAC,CAAC,CAAC;IACJ,MAAM,WAAW,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE;QAC/C,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,CAC1B,CAAC,SAAS,EAAE,EAAE,CACZ,QAAQ,CAAC,UAAU,KAAK,SAAS;YACjC,QAAQ,CAAC,UAAU,IAAI,SAAS,CAAC,KAAK,GAAG,IAAI;YAC7C,QAAQ,CAAC,UAAU,IAAI,SAAS,CAAC,KAAK,GAAG,IAAI,CAChD,CAAC;QACF,OAAO,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,GAAG,QAAQ,EAAE,cAAc,EAAE,OAAO,CAAC,EAAE,EAAE,CAAC;IACxF,CAAC,CAAC,CAAC;IAEH,MAAM,IAAI,GAAG,aAAa,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IACxC,MAAM,SAAS,GAAG,gBAAgB,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAChD,MAAM,UAAU,GAAG,kBAAkB,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IACnD,MAAM,QAAQ,GAAG,aAAa,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAC5C,MAAM,aAAa,GAAG,IAAI,GAAG,EAAkB,CAAC;IAChD,KAAK,MAAM,OAAO,IAAI,UAAU;QAAE,KAAK,MAAM,MAAM,IAAI,OAAO,CAAC,OAAO;YAAE,aAAa,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC;IAC9G,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;QACrC,MAAM,OAAO,GAAG,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAC3C,OAAO,OAAO,KAAK,SAAS;YAC1B,CAAC,CAAC,IAAI;YACN,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,UAAU,EAAE,EAAE,GAAG,IAAI,CAAC,UAAU,EAAE,OAAO,EAAE,EAAE,CAAC;IAC/D,CAAC,CAAC,CAAC;IAEH,MAAM,SAAS,GAA2B,CAAC,GAAG,KAAK,CAAC,SAAS,EAAE,GAAG,WAAW,CAAC,CAAC;IAC/E,KAAK,MAAM,IAAI,IAAI,IAAI,EAAE,CAAC;QACxB,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;QAC9B,IAAI,KAAK,KAAK,SAAS;YAAE,SAAS;QAClC,KAAK,MAAM,MAAM,IAAI,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;YAC3C,SAAS,CAAC,IAAI,CAAC;gBACb,EAAE,EAAE,UAAU,IAAI,CAAC,EAAE,IAAI,KAAK,IAAI,MAAM,EAAE;gBAC1C,IAAI,EAAE,QAAQ;gBACd,IAAI,EAAE,KAAK;gBACX,EAAE,EAAE,MAAM;gBACV,SAAS,EAAE,IAAI,CAAC,IAAI;aACrB,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IACD,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;QACjC,MAAM,KAAK,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC;QACxC,IAAI,KAAK,KAAK,SAAS;YAAE,SAAS;QAClC,KAAK,MAAM,MAAM,IAAI,QAAQ,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;YAC/C,SAAS,CAAC,IAAI,CAAC;gBACb,EAAE,EAAE,YAAY,QAAQ,CAAC,EAAE,IAAI,KAAK,IAAI,MAAM,CAAC,IAAI,EAAE;gBACrD,IAAI,EAAE,iBAAiB;gBACvB,IAAI,EAAE,KAAK;gBACX,EAAE,EAAE,MAAM,CAAC,IAAI;aAChB,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IACD,MAAM,cAAc,GAAG,IAAI,GAAG,EAAkB,CAAC;IACjD,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;QACjC,KAAK,MAAM,MAAM,IAAI,QAAQ,CAAC,OAAO,EAAE,CAAC;YACtC,cAAc,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,QAAQ,CAAC,UAAU,GAAG,MAAM,CAAC,WAAW,CAAC,CAAC;QAC5E,CAAC;IACH,CAAC;IACD,MAAM,YAAY,GAAG,IAAI,GAAG,EAA2B,CAAC;IACxD,KAAK,MAAM,IAAI,IAAI,IAAI,EAAE,CAAC;QACxB,KAAK,MAAM,MAAM,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YAClC,MAAM,OAAO,GAAG,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;YAC/C,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACnB,YAAY,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACpC,CAAC;IACH,CAAC;IAED,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,UAAU,EAAE,QAAQ,EAAE,cAAc,EAAE,YAAY,EAAE,CAAC;AAC5G,CAAC;AAED,SAAS,kBAAkB,CAAC,KAAiB;IAC3C,MAAM,GAAG,GAAkE,EAAE,CAAC;IAC9E,KAAK,MAAM,MAAM,IAAI,KAAK,CAAC,KAAK,EAAE,CAAC;QACjC,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC;YACnE,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAE,CAAC,CAAC;YACrD,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,GAAG,CAAC,CAAE,CAAC,CAAC;YAC1D,IAAI,IAAI,KAAK,SAAS,IAAI,KAAK,KAAK,SAAS;gBAAE,SAAS;YACxD,MAAM,GAAG,GAAG,UAAU,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;YAC9C,GAAG,CAAC,IAAI,CAAC;gBACP,EAAE,EAAE,aAAa,IAAI,CAAC,EAAE,IAAI,KAAK,CAAC,EAAE,EAAE;gBACtC,IAAI,EAAE,WAAW;gBACjB,IAAI,EAAE,IAAI,CAAC,EAAE;gBACb,EAAE,EAAE,KAAK,CAAC,EAAE;gBACZ,IAAI,EAAE,GAAG,CAAC,IAAI;gBACd,UAAU,EAAE,GAAG,CAAC,QAAQ;aACzB,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IACD,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IACzG,OAAO,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE;QAC1B,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAE,CAAC;QAC5C,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAE,CAAC;QAC3C,MAAM,UAAU,GAAG,QAAQ,CAAC,UAAU,GAAG,CAAC,QAAQ,CAAC,UAAU,GAAG,KAAK,CAAC,CAAC;QACvE,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,aAAa,EAAE,KAAK,CAAC,OAAO,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC;QAClG,MAAM,OAAO,GAAG,eAAe,CAAC,IAAI,EAAE,KAAK,CAAC,GAAG,GAAG,CAAC;QACnD,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QACnE,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC;QAC1F,OAAO;YACL,GAAG,QAAQ;YACX,gBAAgB,EAAE,KAAK,CACrB,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,UAAU,GAAG,GAAG,GAAG,MAAM,GAAG,GAAG,GAAG,OAAO,GAAG,IAAI,GAAG,MAAM,GAAG,GAAG,GAAG,MAAM,GAAG,IAAI,CAAC,CAC7F;SACF,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC;AAED,SAAS,eAAe,CAAC,IAAsB,EAAE,KAAuB;IACtE,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,KAAK,SAAS,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,KAAK,SAAS;QAAE,OAAO,CAAC,CAAC;IAClF,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,KAAK,KAAK,CAAC,OAAO,CAAC,IAAI;QAAE,OAAO,CAAC,CAAC;IACvD,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,oBAAoB,IAAI,CAAC,EAAE,KAAK,CAAC,OAAO,CAAC,oBAAoB,IAAI,CAAC,CAAC,CAAC,CAAC;AAClH,CAAC;AAED,SAAS,aAAa,CAAC,KAAkC;IACvD,MAAM,WAAW,GAAG;QAClB,CAAC,MAAM,EAAE,CAAC,IAAsB,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;QACjD,CAAC,OAAO,EAAE,CAAC,IAAsB,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC;QACpE,CAAC,mBAAmB,EAAE,CAAC,IAAsB,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC;QACpF,CAAC,KAAK,EAAE,CAAC,IAAsB,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;QAChD,CAAC,QAAQ,EAAE,CAAC,IAAsB,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC;QACtE,CAAC,iBAAiB,EAAE,CAAC,IAAsB,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC;KAC3E,CAAC;IACX,MAAM,IAAI,GAAoB,EAAE,CAAC;IACjC,KAAK,MAAM,CAAC,IAAI,EAAE,YAAY,CAAC,IAAI,WAAW,EAAE,CAAC;QAC/C,MAAM,QAAQ,GAAG,yBAAyB,CACxC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,KAAK,EAAE,YAAY,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,EACjE,CAAC,CACF,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,IAAI,CAAC,CAAC,CAAC;QACnD,QAAQ,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE,KAAK,EAAE,EAAE;YAClC,IAAI,CAAC,IAAI,CAAC;gBACR,EAAE,EAAE,KAAK,IAAI,IAAI,KAAK,GAAG,CAAC,EAAE;gBAC5B,IAAI;gBACJ,UAAU,EAAE,KAAK,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;gBACpC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE;aAC3D,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;IACL,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,gBAAgB,CAAC,KAAkC;IAC1D,MAAM,UAAU,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE;QACxC,MAAM,IAAI,GAAG,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC;QACpC,MAAM,UAAU,GAAG,IAAI,CAAC,UAAU,CAAC,YAAY,CAAC;QAChD,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,SAAS,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS,CAAC,IAAI,IAAI,KAAK,SAAS,IAAI,UAAU,KAAK,SAAS;YAAE,OAAO,EAAE,CAAC;QACtH,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC,GAAG,IAAI,GAAG,GAAG,CAAC;QAC/F,OAAO,CAAC,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC,CAAC;IAC9C,CAAC,CAAC,CAAC;IACH,OAAO,yBAAyB,CAAC,UAAU,EAAE,CAAC,CAAC;SAC5C,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,IAAI,CAAC,CAAC;SAChD,GAAG,CAAC,CAAC,OAAO,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC;QACxB,EAAE,EAAE,IAAI,KAAK,GAAG,CAAC,EAAE;QACnB,UAAU,EAAE,KAAK,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;QACpC,UAAU,EAAE,UAAmB;QAC/B,OAAO,EAAE,OAAO,CAAC,OAAO;aACrB,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,EAAE,EAAE,WAAW,EAAE,KAAK,CAAC,MAAM,CAAC,KAAK,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC;aAC5F,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,eAAe,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;KACnD,CAAC,CAAC,CAAC;AACR,CAAC;AAED,SAAS,kBAAkB,CAAC,KAAkC;IAC5D,OAAO,yBAAyB,CAC9B,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,UAAU,CAAC,SAAS,EAAE,CAAC,CAAC,EACxE,IAAI,CACL,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,KAAK,EAAE,EAAE;QACvB,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QAClE,MAAM,eAAe,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,CAAE,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QACtH,OAAO;YACL,EAAE,EAAE,IAAI,KAAK,GAAG,CAAC,EAAE;YACnB,SAAS,EAAE,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC;YAChC,OAAO;YACP,eAAe;SAChB,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC;AAED,SAAS,aAAa,CAAC,KAAkC;IACvD,MAAM,MAAM,GAAG,IAAI,GAAG,EAAoB,CAAC;IAC3C,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,KAAK,SAAS;YAAE,SAAS;QAC9C,MAAM,OAAO,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;QACpD,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACtB,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IACzC,CAAC;IACD,OAAO,CAAC,GAAG,MAAM,CAAC,OAAO,EAAE,CAAC;SACzB,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;SACrE,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,OAAO,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,IAAI,KAAK,GAAG,CAAC,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC;AAC/F,CAAC;AAQD,SAAS,eAAe,CACtB,MAAyB,EACzB,MAAc,EACd,SAAqC;IAErC,MAAM,QAAQ,GAAe,EAAE,CAAC;IAChC,KAAK,MAAM,KAAK,IAAI,CAAC,GAAG,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;QACtD,MAAM,OAAO,GAAG,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;QAChC,MAAM,MAAM,GAAG,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QACnE,IAAI,OAAO,KAAK,SAAS,IAAI,MAAM,KAAK,SAAS,IAAI,IAAI,CAAC,GAAG,CAAC,KAAK,GAAG,MAAM,CAAC,GAAG,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC;YAClG,QAAQ,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;QACzB,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACtB,CAAC;IACH,CAAC;IACD,OAAO,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC;QACvC,EAAE,EAAE,GAAG,MAAM,GAAG,KAAK,GAAG,CAAC,EAAE;QAC3B,MAAM,EAAE,MAAM,CAAC,OAAO,CAAE;QACxB,MAAM,EAAE,OAAO;KAChB,CAAC,CAAC,CAAC;AACN,CAAC;AAED,SAAS,yBAAyB,CAChC,MAAkE,EAClE,SAAiB;IAKjB,MAAM,QAAQ,GAAkE,EAAE,CAAC;IACnF,KAAK,MAAM,KAAK,IAAI,CAAC,GAAG,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,IAAI,eAAe,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;QACjG,MAAM,OAAO,GAAG,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;QAChC,MAAM,MAAM,GAAG,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;QACjG,IAAI,OAAO,KAAK,SAAS,IAAI,MAAM,KAAK,SAAS,IAAI,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,GAAG,MAAM,CAAC,GAAG,SAAS,EAAE,CAAC;YAChG,QAAQ,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;QACzB,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACtB,CAAC;IACH,CAAC;IACD,OAAO,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAE,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC;AACnH,CAAC","sourcesContent":["import type {\n AlignmentAxis,\n BaselineCluster,\n PresentationNode,\n PresentationRelation,\n ProminenceCluster,\n SpacingCluster,\n SurfaceGroup,\n} from './model.js';\nimport type { BuiltGraph } from './graph.js';\nimport { codeUnitCompare, gapBetween, median, round } from './math.js';\n\nexport interface Measurements {\n readonly nodes: PresentationNode[];\n readonly relations: PresentationRelation[];\n readonly spacing: SpacingCluster[];\n readonly axes: AlignmentAxis[];\n readonly baselines: BaselineCluster[];\n readonly prominence: ProminenceCluster[];\n readonly surfaces: SurfaceGroup[];\n readonly baselineByNode: ReadonlyMap<string, number>;\n readonly axisByMember: ReadonlyMap<string, readonly AlignmentAxis[]>;\n}\n\nexport function measureGraph(graph: BuiltGraph): Measurements {\n const separations = siblingSeparations(graph);\n const spacing = numericClusters(\n separations.filter((relation) => (relation.distancePx ?? 0) > 0).map((relation) => relation.distancePx!),\n 'S',\n (center) => Math.max(0.75, center * 0.08),\n ).map((cluster) => ({\n id: cluster.id,\n centerPx: round(cluster.center, 2),\n minPx: round(Math.min(...cluster.values), 2),\n maxPx: round(Math.max(...cluster.values), 2),\n samples: cluster.values.length,\n }));\n const withSpacing = separations.map((relation) => {\n const cluster = spacing.find(\n (candidate) =>\n relation.distancePx !== undefined &&\n relation.distancePx >= candidate.minPx - 0.01 &&\n relation.distancePx <= candidate.maxPx + 0.01,\n );\n return cluster === undefined ? relation : { ...relation, spacingCluster: cluster.id };\n });\n\n const axes = alignmentAxes(graph.nodes);\n const baselines = baselineClusters(graph.nodes);\n const prominence = prominenceClusters(graph.nodes);\n const surfaces = surfaceGroups(graph.nodes);\n const clusterByNode = new Map<string, string>();\n for (const cluster of prominence) for (const member of cluster.members) clusterByNode.set(member, cluster.id);\n const nodes = graph.nodes.map((node) => {\n const cluster = clusterByNode.get(node.id);\n return cluster === undefined\n ? node\n : { ...node, prominence: { ...node.prominence, cluster } };\n });\n\n const relations: PresentationRelation[] = [...graph.relations, ...withSpacing];\n for (const axis of axes) {\n const first = axis.members[0];\n if (first === undefined) continue;\n for (const member of axis.members.slice(1)) {\n relations.push({\n id: `aligns:${axis.id}:${first}:${member}`,\n kind: 'aligns',\n from: first,\n to: member,\n alignment: axis.kind,\n });\n }\n }\n for (const baseline of baselines) {\n const first = baseline.members[0]?.node;\n if (first === undefined) continue;\n for (const member of baseline.members.slice(1)) {\n relations.push({\n id: `baseline:${baseline.id}:${first}:${member.node}`,\n kind: 'shares-baseline',\n from: first,\n to: member.node,\n });\n }\n }\n const baselineByNode = new Map<string, number>();\n for (const baseline of baselines) {\n for (const member of baseline.members) {\n baselineByNode.set(member.node, baseline.coordinate + member.deviationPx);\n }\n }\n const axisByMember = new Map<string, AlignmentAxis[]>();\n for (const axis of axes) {\n for (const member of axis.members) {\n const current = axisByMember.get(member) ?? [];\n current.push(axis);\n axisByMember.set(member, current);\n }\n }\n\n return { nodes, relations, spacing, axes, baselines, prominence, surfaces, baselineByNode, axisByMember };\n}\n\nfunction siblingSeparations(graph: BuiltGraph): PresentationRelation[] {\n const raw: Array<PresentationRelation & { readonly distancePx: number }> = [];\n for (const parent of graph.nodes) {\n for (let index = 0; index < parent.children.length - 1; index += 1) {\n const left = graph.byId.get(parent.children[index]!);\n const right = graph.byId.get(parent.children[index + 1]!);\n if (left === undefined || right === undefined) continue;\n const gap = gapBetween(left.rect, right.rect);\n raw.push({\n id: `separates:${left.id}:${right.id}`,\n kind: 'separates',\n from: left.id,\n to: right.id,\n axis: gap.axis,\n distancePx: gap.distance,\n });\n }\n }\n const scale = median(raw.map((relation) => relation.distancePx).filter((distance) => distance > 0)) ?? 1;\n return raw.map((relation) => {\n const left = graph.byId.get(relation.from)!;\n const right = graph.byId.get(relation.to)!;\n const whitespace = relation.distancePx / (relation.distancePx + scale);\n const border = Math.min(1, Math.max(left.surface.borderWidthPx, right.surface.borderWidthPx) / 2);\n const surface = surfaceDistance(left, right) / 100;\n const shadow = left.surface.shadow || right.surface.shadow ? 1 : 0;\n const indent = Math.min(1, Math.abs(left.rect.x - right.rect.x) / Math.max(1, scale * 2));\n return {\n ...relation,\n boundaryStrength: round(\n Math.min(1, whitespace * 0.5 + border * 0.2 + surface * 0.15 + shadow * 0.1 + indent * 0.05),\n ),\n };\n });\n}\n\nfunction surfaceDistance(left: PresentationNode, right: PresentationNode): number {\n if (left.surface.fill === undefined || right.surface.fill === undefined) return 0;\n if (left.surface.fill === right.surface.fill) return 0;\n return Math.min(100, Math.max(left.surface.perceptualDifference ?? 0, right.surface.perceptualDifference ?? 0));\n}\n\nfunction alignmentAxes(nodes: readonly PresentationNode[]): AlignmentAxis[] {\n const definitions = [\n ['left', (node: PresentationNode) => node.rect.x],\n ['right', (node: PresentationNode) => node.rect.x + node.rect.width],\n ['horizontal-center', (node: PresentationNode) => node.rect.x + node.rect.width / 2],\n ['top', (node: PresentationNode) => node.rect.y],\n ['bottom', (node: PresentationNode) => node.rect.y + node.rect.height],\n ['vertical-center', (node: PresentationNode) => node.rect.y + node.rect.height / 2],\n ] as const;\n const axes: AlignmentAxis[] = [];\n for (const [kind, coordinateOf] of definitions) {\n const clusters = numericMembershipClusters(\n nodes.map((node) => ({ id: node.id, value: coordinateOf(node) })),\n 1,\n ).filter((cluster) => cluster.members.length >= 2);\n clusters.forEach((cluster, index) => {\n axes.push({\n id: `A:${kind}:${index + 1}`,\n kind,\n coordinate: round(cluster.center, 2),\n members: cluster.members.map((member) => member.id).sort(),\n });\n });\n }\n return axes;\n}\n\nfunction baselineClusters(nodes: readonly PresentationNode[]): BaselineCluster[] {\n const candidates = nodes.flatMap((node) => {\n const size = node.typography.sizePx;\n const lineHeight = node.typography.lineHeightPx;\n if ((node.text === undefined && node.name === undefined) || size === undefined || lineHeight === undefined) return [];\n const coordinate = node.rect.y + Math.max(0, (node.rect.height - lineHeight) / 2) + size * 0.8;\n return [{ id: node.id, value: coordinate }];\n });\n return numericMembershipClusters(candidates, 1)\n .filter((cluster) => cluster.members.length >= 2)\n .map((cluster, index) => ({\n id: `B${index + 1}`,\n coordinate: round(cluster.center, 2),\n confidence: 'inferred' as const,\n members: cluster.members\n .map((member) => ({ node: member.id, deviationPx: round(member.value - cluster.center, 2) }))\n .sort((a, b) => codeUnitCompare(a.node, b.node)),\n }));\n}\n\nfunction prominenceClusters(nodes: readonly PresentationNode[]): ProminenceCluster[] {\n return numericMembershipClusters(\n nodes.map((node) => ({ id: node.id, value: node.prominence.magnitude })),\n 0.12,\n ).map((cluster, index) => {\n const members = cluster.members.map((member) => member.id).sort();\n const semanticClasses = [...new Set(members.map((id) => nodes.find((node) => node.id === id)!.semanticClass))].sort();\n return {\n id: `P${index + 1}`,\n magnitude: round(cluster.center),\n members,\n semanticClasses,\n };\n });\n}\n\nfunction surfaceGroups(nodes: readonly PresentationNode[]): SurfaceGroup[] {\n const groups = new Map<string, string[]>();\n for (const node of nodes) {\n if (node.surface.fill === undefined) continue;\n const members = groups.get(node.surface.fill) ?? [];\n members.push(node.id);\n groups.set(node.surface.fill, members);\n }\n return [...groups.entries()]\n .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))\n .map(([fill, members], index) => ({ id: `F${index + 1}`, fill, members: members.sort() }));\n}\n\ninterface NumericCluster {\n readonly id: string;\n readonly center: number;\n readonly values: readonly number[];\n}\n\nfunction numericClusters(\n values: readonly number[],\n prefix: string,\n tolerance: (center: number) => number,\n): NumericCluster[] {\n const clusters: number[][] = [];\n for (const value of [...values].sort((a, b) => a - b)) {\n const current = clusters.at(-1);\n const center = current === undefined ? undefined : median(current);\n if (current === undefined || center === undefined || Math.abs(value - center) > tolerance(center)) {\n clusters.push([value]);\n } else {\n current.push(value);\n }\n }\n return clusters.map((cluster, index) => ({\n id: `${prefix}${index + 1}`,\n center: median(cluster)!,\n values: cluster,\n }));\n}\n\nfunction numericMembershipClusters(\n values: readonly { readonly id: string; readonly value: number }[],\n tolerance: number,\n): readonly {\n readonly center: number;\n readonly members: readonly { readonly id: string; readonly value: number }[];\n}[] {\n const clusters: Array<Array<{ readonly id: string; readonly value: number }>> = [];\n for (const value of [...values].sort((a, b) => a.value - b.value || codeUnitCompare(a.id, b.id))) {\n const current = clusters.at(-1);\n const center = current === undefined ? undefined : median(current.map((member) => member.value));\n if (current === undefined || center === undefined || Math.abs(value.value - center) > tolerance) {\n clusters.push([value]);\n } else {\n current.push(value);\n }\n }\n return clusters.map((cluster) => ({ center: median(cluster.map((member) => member.value))!, members: cluster }));\n}\n"]}