@viewscript/renderer 0.1.0-202605140639

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 (89) hide show
  1. package/dist/ast/types.d.ts +403 -0
  2. package/dist/ast/types.js +33 -0
  3. package/dist/compiler/chunk-splitter.d.ts +98 -0
  4. package/dist/compiler/chunk-splitter.js +361 -0
  5. package/dist/index.d.ts +55 -0
  6. package/dist/index.js +17 -0
  7. package/dist/rasterizer/__tests__/error-distribution.test.d.ts +7 -0
  8. package/dist/rasterizer/__tests__/error-distribution.test.js +322 -0
  9. package/dist/rasterizer/canvas-mapper.d.ts +280 -0
  10. package/dist/rasterizer/canvas-mapper.js +414 -0
  11. package/dist/rasterizer/error-distribution.d.ts +143 -0
  12. package/dist/rasterizer/error-distribution.js +231 -0
  13. package/dist/rasterizer/gradient-mapper.d.ts +223 -0
  14. package/dist/rasterizer/gradient-mapper.js +352 -0
  15. package/dist/rasterizer/topology-rounding.d.ts +151 -0
  16. package/dist/rasterizer/topology-rounding.js +347 -0
  17. package/dist/runtime/__tests__/event-backpressure.test.d.ts +10 -0
  18. package/dist/runtime/__tests__/event-backpressure.test.js +190 -0
  19. package/dist/runtime/event-backpressure.d.ts +393 -0
  20. package/dist/runtime/event-backpressure.js +458 -0
  21. package/dist/runtime/render-loop.d.ts +277 -0
  22. package/dist/runtime/render-loop.js +435 -0
  23. package/dist/runtime/wasm-resource-manager.d.ts +122 -0
  24. package/dist/runtime/wasm-resource-manager.js +253 -0
  25. package/dist/runtime/wgpu-renderer-adapter.d.ts +168 -0
  26. package/dist/runtime/wgpu-renderer-adapter.js +230 -0
  27. package/dist/semantic/__tests__/semantic-translator.test.d.ts +4 -0
  28. package/dist/semantic/__tests__/semantic-translator.test.js +203 -0
  29. package/dist/semantic/semantic-translator.d.ts +229 -0
  30. package/dist/semantic/semantic-translator.js +398 -0
  31. package/package.json +28 -0
  32. package/playwright-report/data/0bafe4e0863f0e244bba68a838f73241f8f2efaa.md +226 -0
  33. package/playwright-report/data/9281aca8abfb06c6cecb35d5ddd13d61f8c752d8.md +226 -0
  34. package/playwright-report/index.html +90 -0
  35. package/playwright.config.ts +160 -0
  36. package/screenshot-chrome.png +0 -0
  37. package/screenshots/visual-demo-verification.png +0 -0
  38. package/screenshots/visual-demo.png +0 -0
  39. package/src/ast/types.ts +473 -0
  40. package/src/compiler/chunk-splitter.ts +534 -0
  41. package/src/index.ts +62 -0
  42. package/src/rasterizer/__tests__/error-distribution.test.ts +382 -0
  43. package/src/rasterizer/canvas-mapper.ts +677 -0
  44. package/src/rasterizer/error-distribution.ts +344 -0
  45. package/src/rasterizer/gradient-mapper.ts +563 -0
  46. package/src/rasterizer/topology-rounding.ts +499 -0
  47. package/src/runtime/__tests__/event-backpressure.test.ts +254 -0
  48. package/src/runtime/event-backpressure.ts +622 -0
  49. package/src/runtime/render-loop.ts +660 -0
  50. package/src/runtime/wasm-resource-manager.ts +349 -0
  51. package/src/runtime/wgpu-renderer-adapter.ts +318 -0
  52. package/src/semantic/__tests__/semantic-translator.test.ts +263 -0
  53. package/src/semantic/semantic-translator.ts +637 -0
  54. package/test-results/.last-run.json +4 -0
  55. package/tests/e2e/async-race.spec.ts +612 -0
  56. package/tests/e2e/bilayer-sync.spec.ts +405 -0
  57. package/tests/e2e/failures/.gitkeep +0 -0
  58. package/tests/e2e/fullstack.spec.ts +681 -0
  59. package/tests/e2e/g1-continuity.spec.ts +703 -0
  60. package/tests/e2e/golden/.gitkeep +0 -0
  61. package/tests/e2e/golden/conic-color-wheel.raw +0 -0
  62. package/tests/e2e/golden/conic-color-wheel.sha256 +1 -0
  63. package/tests/e2e/golden/conic-rotated.raw +0 -0
  64. package/tests/e2e/golden/conic-rotated.sha256 +1 -0
  65. package/tests/e2e/golden/linear-45deg.raw +0 -0
  66. package/tests/e2e/golden/linear-45deg.sha256 +1 -0
  67. package/tests/e2e/golden/linear-horizontal.raw +0 -0
  68. package/tests/e2e/golden/linear-horizontal.sha256 +1 -0
  69. package/tests/e2e/golden/linear-multi-stop.raw +0 -0
  70. package/tests/e2e/golden/linear-multi-stop.sha256 +1 -0
  71. package/tests/e2e/golden/radial-circle-center.raw +0 -0
  72. package/tests/e2e/golden/radial-circle-center.sha256 +1 -0
  73. package/tests/e2e/golden/radial-offset.raw +0 -0
  74. package/tests/e2e/golden/radial-offset.sha256 +1 -0
  75. package/tests/e2e/golden/tile-mirror.raw +0 -0
  76. package/tests/e2e/golden/tile-mirror.sha256 +1 -0
  77. package/tests/e2e/golden/tile-repeat.raw +0 -0
  78. package/tests/e2e/golden/tile-repeat.sha256 +1 -0
  79. package/tests/e2e/gradient-animation.spec.ts +606 -0
  80. package/tests/e2e/memory-stability.spec.ts +396 -0
  81. package/tests/e2e/path-topology.spec.ts +674 -0
  82. package/tests/e2e/performance-profile.spec.ts +501 -0
  83. package/tests/e2e/screenshot.spec.ts +60 -0
  84. package/tests/e2e/test-harness.html +1005 -0
  85. package/tests/e2e/text-layout.spec.ts +451 -0
  86. package/tests/e2e/visual-demo.html +340 -0
  87. package/tests/e2e/visual-regression.spec.ts +335 -0
  88. package/tsconfig.json +12 -0
  89. package/vitest.config.ts +8 -0
@@ -0,0 +1,414 @@
1
+ /**
2
+ * Path Projection (Phase 6)
3
+ *
4
+ * This module transforms P-dimension ControlPoint entities (with exact rational
5
+ * coordinates) into rasterized PathEntity objects for rendering.
6
+ *
7
+ * ## Architecture
8
+ *
9
+ * ```
10
+ * P-Dimension Rasterization Boundary Canvas
11
+ * ───────────────────────────────────────────────────────────────────────────
12
+ *
13
+ * ControlPoint ┌──────────────────┐
14
+ * entities with ─────────────▶ │ canvas-mapper │ ─────────────▶ PathEntity
15
+ * Rational coords │ (this module) │ objects
16
+ * └──────────────────┘
17
+ * │
18
+ * ▼
19
+ * topology-rounding.ts
20
+ * (pixel-perfect adjacency)
21
+ * ```
22
+ *
23
+ * ## Critical Invariants
24
+ *
25
+ * 1. **Float Decontamination**: All f64 values are produced ONLY by
26
+ * `Rational.to_f64_for_rasterization()` at this boundary
27
+ * 2. **Topology Preservation**: Shared ControlPoints produce bit-identical
28
+ * coordinates, ensuring seamless curve connections
29
+ * 3. **Fill Rule Semantics**: SVG fill-rule (nonzero/evenodd) is preserved
30
+ */
31
+ // =============================================================================
32
+ // Core Mapping Logic
33
+ // =============================================================================
34
+ /**
35
+ * Maps P-dimension path definitions to rasterized PathEntity objects.
36
+ *
37
+ * @param paths - Path definitions with EntityId references
38
+ * @param controlPoints - Map of resolved control point positions
39
+ * @param devicePixelRatio - DPR for coordinate scaling
40
+ * @returns Rasterized paths ready for GPU renderer
41
+ */
42
+ export function mapPathsToCanvas(paths, controlPoints, devicePixelRatio = 1) {
43
+ return paths.map(path => mapSinglePath(path, controlPoints, devicePixelRatio));
44
+ }
45
+ /**
46
+ * Map a single path definition to rasterized commands.
47
+ */
48
+ function mapSinglePath(path, controlPoints, dpr) {
49
+ const commands = [];
50
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
51
+ const toFloat = (r) => {
52
+ // RASTERIZATION BOUNDARY: Convert Rational to f64
53
+ return (Number(r.numerator) / Number(r.denominator)) * dpr;
54
+ };
55
+ const getPoint = (id) => {
56
+ const cp = controlPoints.get(id);
57
+ if (!cp) {
58
+ throw new Error(`ControlPoint ${id} not found in resolved set`);
59
+ }
60
+ return {
61
+ x: toFloat(cp.x),
62
+ y: toFloat(cp.y),
63
+ };
64
+ };
65
+ const updateBounds = (x, y) => {
66
+ minX = Math.min(minX, x);
67
+ minY = Math.min(minY, y);
68
+ maxX = Math.max(maxX, x);
69
+ maxY = Math.max(maxY, y);
70
+ };
71
+ const toRational = (n) => ({
72
+ // Convert float back to rational for PathCommand type
73
+ // This is a simplification - in production, we'd keep exact rationals
74
+ numerator: BigInt(Math.round(n * 1000000)),
75
+ denominator: BigInt(1000000),
76
+ });
77
+ for (const segment of path.segments) {
78
+ switch (segment.type) {
79
+ case 'moveTo': {
80
+ const p = getPoint(segment.point);
81
+ commands.push({ type: 'M', x: toRational(p.x), y: toRational(p.y) });
82
+ updateBounds(p.x, p.y);
83
+ break;
84
+ }
85
+ case 'lineTo': {
86
+ const p = getPoint(segment.point);
87
+ commands.push({ type: 'L', x: toRational(p.x), y: toRational(p.y) });
88
+ updateBounds(p.x, p.y);
89
+ break;
90
+ }
91
+ case 'quadTo': {
92
+ const ctrl = getPoint(segment.control);
93
+ const end = getPoint(segment.point);
94
+ commands.push({
95
+ type: 'Q',
96
+ x1: toRational(ctrl.x),
97
+ y1: toRational(ctrl.y),
98
+ x: toRational(end.x),
99
+ y: toRational(end.y),
100
+ });
101
+ updateBounds(ctrl.x, ctrl.y);
102
+ updateBounds(end.x, end.y);
103
+ break;
104
+ }
105
+ case 'cubicTo': {
106
+ const ctrl1 = getPoint(segment.control1);
107
+ const ctrl2 = getPoint(segment.control2);
108
+ const end = getPoint(segment.point);
109
+ commands.push({
110
+ type: 'C',
111
+ x1: toRational(ctrl1.x),
112
+ y1: toRational(ctrl1.y),
113
+ x2: toRational(ctrl2.x),
114
+ y2: toRational(ctrl2.y),
115
+ x: toRational(end.x),
116
+ y: toRational(end.y),
117
+ });
118
+ updateBounds(ctrl1.x, ctrl1.y);
119
+ updateBounds(ctrl2.x, ctrl2.y);
120
+ updateBounds(end.x, end.y);
121
+ break;
122
+ }
123
+ case 'arcTo': {
124
+ const end = getPoint(segment.point);
125
+ commands.push({
126
+ type: 'A',
127
+ rx: segment.radiusX,
128
+ ry: segment.radiusY,
129
+ rotation: toFloat(segment.xRotation),
130
+ largeArc: segment.largeArc,
131
+ sweep: segment.sweep,
132
+ x: toRational(end.x),
133
+ y: toRational(end.y),
134
+ });
135
+ updateBounds(end.x, end.y);
136
+ // Note: Arc bounds are approximate without full geometric calculation
137
+ break;
138
+ }
139
+ case 'close': {
140
+ commands.push({ type: 'Z' });
141
+ break;
142
+ }
143
+ }
144
+ }
145
+ return {
146
+ id: path.id,
147
+ commands,
148
+ fillRule: path.fillRule,
149
+ closed: path.closed,
150
+ bounds: {
151
+ minX: minX === Infinity ? 0 : minX,
152
+ minY: minY === Infinity ? 0 : minY,
153
+ maxX: maxX === -Infinity ? 0 : maxX,
154
+ maxY: maxY === -Infinity ? 0 : maxY,
155
+ },
156
+ };
157
+ }
158
+ /**
159
+ * Fill type constants.
160
+ */
161
+ export const FillType = {
162
+ Winding: 0, // nonzero
163
+ EvenOdd: 1, // evenodd
164
+ };
165
+ /**
166
+ * Build a PathEntity from rasterized path data.
167
+ *
168
+ * @param path - Rasterized path with float coordinates
169
+ * @param pathBuilder - Path builder object to populate
170
+ */
171
+ export function buildPathEntity(path, pathBuilder) {
172
+ // Set fill rule
173
+ pathBuilder.setFillType(path.fillRule === 'evenodd' ? FillType.EvenOdd : FillType.Winding);
174
+ const toFloat = (r) => Number(r.numerator) / Number(r.denominator);
175
+ for (const cmd of path.commands) {
176
+ switch (cmd.type) {
177
+ case 'M':
178
+ pathBuilder.moveTo(toFloat(cmd.x), toFloat(cmd.y));
179
+ break;
180
+ case 'L':
181
+ pathBuilder.lineTo(toFloat(cmd.x), toFloat(cmd.y));
182
+ break;
183
+ case 'Q':
184
+ pathBuilder.quadTo(toFloat(cmd.x1), toFloat(cmd.y1), toFloat(cmd.x), toFloat(cmd.y));
185
+ break;
186
+ case 'C':
187
+ pathBuilder.cubicTo(toFloat(cmd.x1), toFloat(cmd.y1), toFloat(cmd.x2), toFloat(cmd.y2), toFloat(cmd.x), toFloat(cmd.y));
188
+ break;
189
+ case 'A':
190
+ // Uses arcToRotated for SVG-style arcs
191
+ pathBuilder.arcToRotated(toFloat(cmd.rx), toFloat(cmd.ry), cmd.rotation, // already a number
192
+ !cmd.largeArc, // useSmallArc is inverse of largeArc
193
+ !cmd.sweep, // isCCW may differ from sweep
194
+ toFloat(cmd.x), toFloat(cmd.y));
195
+ break;
196
+ case 'Z':
197
+ pathBuilder.close();
198
+ break;
199
+ }
200
+ }
201
+ }
202
+ // =============================================================================
203
+ // Topology-Preserving Shared Control Points
204
+ // =============================================================================
205
+ /**
206
+ * Ensures that paths sharing control points produce bit-identical coordinates.
207
+ *
208
+ * This is critical for seamless curve connections: if two Bezier curves share
209
+ * an endpoint, the rasterized coordinates must be exactly the same to prevent
210
+ * visual gaps or overlaps.
211
+ *
212
+ * ## Algorithm
213
+ *
214
+ * 1. Identify all paths that share control points
215
+ * 2. For shared points, use a single coordinate resolution
216
+ * 3. Both paths reference the same float value
217
+ *
218
+ * @param paths - Paths that may share control points
219
+ * @param controlPoints - Control point definitions
220
+ * @returns Normalized control point map with consistent coordinates
221
+ */
222
+ export function normalizeSharedControlPoints(paths, controlPoints) {
223
+ // Collect all control point references
224
+ const refCounts = new Map();
225
+ for (const path of paths) {
226
+ for (const segment of path.segments) {
227
+ const ids = getSegmentPointIds(segment);
228
+ for (const id of ids) {
229
+ refCounts.set(id, (refCounts.get(id) ?? 0) + 1);
230
+ }
231
+ }
232
+ }
233
+ // Create normalized map (same as input, but this is the guarantee point)
234
+ // In a real implementation, we'd ensure caching of float conversions
235
+ const normalized = new Map();
236
+ for (const [id, cp] of controlPoints) {
237
+ // If this point is shared (referenced by multiple paths),
238
+ // it will produce the same float coordinates for all references
239
+ normalized.set(id, {
240
+ ...cp,
241
+ // The Rational values are preserved exactly; float conversion
242
+ // happens once at rasterization boundary
243
+ });
244
+ }
245
+ return normalized;
246
+ }
247
+ function getSegmentPointIds(segment) {
248
+ switch (segment.type) {
249
+ case 'moveTo':
250
+ case 'lineTo':
251
+ case 'arcTo':
252
+ return [segment.point];
253
+ case 'quadTo':
254
+ return [segment.control, segment.point];
255
+ case 'cubicTo':
256
+ return [segment.control1, segment.control2, segment.point];
257
+ case 'close':
258
+ return [];
259
+ }
260
+ }
261
+ // =============================================================================
262
+ // Validation
263
+ // =============================================================================
264
+ /**
265
+ * Validate that all control point references in paths are resolvable.
266
+ */
267
+ export function validatePathReferences(paths, controlPoints) {
268
+ const errors = [];
269
+ for (const path of paths) {
270
+ for (let i = 0; i < path.segments.length; i++) {
271
+ const segment = path.segments[i];
272
+ const ids = getSegmentPointIds(segment);
273
+ for (const id of ids) {
274
+ if (!controlPoints.has(id)) {
275
+ errors.push(`Path ${path.id} segment ${i}: references undefined ControlPoint ${id}`);
276
+ }
277
+ }
278
+ }
279
+ // Validate path starts with moveTo
280
+ if (path.segments.length > 0 && path.segments[0].type !== 'moveTo') {
281
+ errors.push(`Path ${path.id}: first segment must be 'moveTo', got '${path.segments[0].type}'`);
282
+ }
283
+ }
284
+ return { valid: errors.length === 0, errors };
285
+ }
286
+ /**
287
+ * Convert rational to float at RASTERIZATION BOUNDARY ONLY.
288
+ */
289
+ function toFloat(r) {
290
+ return Number(r.numerator) / Number(r.denominator);
291
+ }
292
+ /**
293
+ * Draw an arc to a GPU draw surface.
294
+ *
295
+ * ## Deferred Evaluation (Phase 7)
296
+ *
297
+ * The arc's circumference points are NOT computed in P-dimension.
298
+ * Only the center, radius, and angles are constrained linearly.
299
+ * The actual arc rendering is delegated to the GPU renderer which evaluates
300
+ * the parametric curve (cos/sin) in its native floating-point space.
301
+ *
302
+ * @param canvas - GPU draw surface
303
+ * @param arc - Resolved arc with rational center/radius/angles
304
+ * @param paint - Paint style for the arc
305
+ */
306
+ export function drawArc(canvas, arc, paint) {
307
+ // Convert rational values to float at rasterization boundary
308
+ const cx = toFloat(arc.center.x);
309
+ const cy = toFloat(arc.center.y);
310
+ const r = toFloat(arc.radius.value);
311
+ const startAngle = toFloat(arc.startAngle);
312
+ const endAngle = toFloat(arc.endAngle);
313
+ // Compute sweep angle
314
+ let sweepAngle = endAngle - startAngle;
315
+ if (arc.clockwise && sweepAngle > 0) {
316
+ sweepAngle -= 360;
317
+ }
318
+ else if (!arc.clockwise && sweepAngle < 0) {
319
+ sweepAngle += 360;
320
+ }
321
+ // drawArc uses an oval bounds
322
+ const oval = {
323
+ x: cx - r,
324
+ y: cy - r,
325
+ width: r * 2,
326
+ height: r * 2,
327
+ };
328
+ canvas.drawArc(oval, startAngle, sweepAngle, false, paint);
329
+ }
330
+ /**
331
+ * Draw a rounded rectangle to a GPU draw surface.
332
+ *
333
+ * ## Deferred Evaluation (Phase 7)
334
+ *
335
+ * Corner curves are NOT evaluated in P-dimension. Only the bounds and
336
+ * radius scalars are constrained. The GPU renderer evaluates the corner arcs
337
+ * internally using native floating-point math.
338
+ *
339
+ * @param canvas - GPU draw surface
340
+ * @param rect - Resolved rounded rect with rational bounds/radii
341
+ * @param paint - Paint style
342
+ */
343
+ export function drawRoundedRect(canvas, rect, paint) {
344
+ // Convert rational values to float at rasterization boundary
345
+ const x = toFloat(rect.bounds.x);
346
+ const y = toFloat(rect.bounds.y);
347
+ const width = toFloat(rect.bounds.width);
348
+ const height = toFloat(rect.bounds.height);
349
+ const rTL = toFloat(rect.radii.topLeft.value);
350
+ const rTR = toFloat(rect.radii.topRight.value);
351
+ const rBR = toFloat(rect.radii.bottomRight.value);
352
+ const rBL = toFloat(rect.radii.bottomLeft.value);
353
+ // Check if all radii are equal (use simpler API)
354
+ if (rTL === rTR && rTR === rBR && rBR === rBL) {
355
+ canvas.drawRoundRect({ x, y, width, height }, rTL, rTL, paint);
356
+ }
357
+ else {
358
+ // Different radii per corner - would need GPU renderer's RRect support
359
+ // For now, use uniform radius (max of all)
360
+ const maxR = Math.max(rTL, rTR, rBR, rBL);
361
+ canvas.drawRoundRect({ x, y, width, height }, maxR, maxR, paint);
362
+ }
363
+ }
364
+ /**
365
+ * Draw a full circle (special case of arc: 0° to 360°).
366
+ *
367
+ * @param canvas - GPU draw surface
368
+ * @param center - Resolved center control point
369
+ * @param radius - Resolved radius entity
370
+ * @param paint - Paint style
371
+ */
372
+ export function drawCircle(canvas, center, radius, paint) {
373
+ const cx = toFloat(center.x);
374
+ const cy = toFloat(center.y);
375
+ const r = toFloat(radius.value);
376
+ const oval = {
377
+ x: cx - r,
378
+ y: cy - r,
379
+ width: r * 2,
380
+ height: r * 2,
381
+ };
382
+ canvas.drawArc(oval, 0, 360, false, paint);
383
+ }
384
+ /**
385
+ * Validate arc/radius entity references.
386
+ */
387
+ export function validateArcReferences(arcs, centers, radii) {
388
+ const errors = [];
389
+ for (const arc of arcs) {
390
+ // Center must exist
391
+ if (!centers.has(arc.center.id)) {
392
+ errors.push(`Arc ${arc.id}: center ControlPoint ${arc.center.id} not found`);
393
+ }
394
+ // Radius must exist
395
+ if (!radii.has(arc.radius.id)) {
396
+ errors.push(`Arc ${arc.id}: Radius entity ${arc.radius.id} not found`);
397
+ }
398
+ // Angles must be valid
399
+ const start = toFloat(arc.startAngle);
400
+ const end = toFloat(arc.endAngle);
401
+ if (isNaN(start) || isNaN(end)) {
402
+ errors.push(`Arc ${arc.id}: invalid angle values`);
403
+ }
404
+ }
405
+ return { valid: errors.length === 0, errors };
406
+ }
407
+ // =============================================================================
408
+ // Exports for Testing
409
+ // =============================================================================
410
+ export const _internals = {
411
+ mapSinglePath,
412
+ toFloat,
413
+ getSegmentPointIds,
414
+ };
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Subpixel Error Distribution Algorithm
3
+ *
4
+ * This module implements the Largest Remainder Method (LRM) for distributing
5
+ * subpixel rounding errors across child elements within a parent container.
6
+ *
7
+ * ## The Problem (Architect's Decision #2: Spatial Closure)
8
+ *
9
+ * Given a 100px container with 3 children of equal width (33.333...px each):
10
+ *
11
+ * ```
12
+ * Naive rounding:
13
+ * floor(33.333) = 33
14
+ * 33 + 33 + 33 = 99px ← 1px HOLE!
15
+ * ```
16
+ *
17
+ * This violates VS axiom: "Constraint holes cannot be hidden in theoretical blind spots"
18
+ *
19
+ * ## Solution: Largest Remainder Method
20
+ *
21
+ * 1. Compute integer quotients: floor(33.333) = 33 for each
22
+ * 2. Compute remainders: 0.333... for each
23
+ * 3. Total shortfall: 100 - 99 = 1px
24
+ * 4. Distribute 1px to elements with largest remainders
25
+ *
26
+ * Result: [34, 33, 33] or [33, 34, 33] or [33, 33, 34]
27
+ * Sum: 100px exactly ✓
28
+ *
29
+ * ## Mathematical Guarantee
30
+ *
31
+ * For any set of positive rationals r₁, r₂, ..., rₙ where Σrᵢ = T (integer):
32
+ *
33
+ * Σ⌊rᵢ⌋ ≤ T ≤ Σ⌈rᵢ⌉
34
+ *
35
+ * LRM distributes exactly (T - Σ⌊rᵢ⌋) extra pixels to achieve Σ = T.
36
+ */
37
+ import type { EntityId } from '../ast/types';
38
+ /**
39
+ * A sibling group: children that must sum to parent's dimension.
40
+ */
41
+ export interface SiblingGroup {
42
+ /** Parent container entity */
43
+ parentId: EntityId;
44
+ /** Child entities in layout order */
45
+ childIds: EntityId[];
46
+ /** Axis being distributed */
47
+ axis: 'horizontal' | 'vertical';
48
+ /** Parent's exact pixel dimension (must be integer) */
49
+ parentDimension: number;
50
+ /** Each child's rational dimension (pre-rounding) */
51
+ childDimensions: Map<EntityId, number>;
52
+ }
53
+ /**
54
+ * Distribution result for a single child.
55
+ */
56
+ export interface DistributedDimension {
57
+ entityId: EntityId;
58
+ /** Integer pixel dimension after distribution */
59
+ pixels: number;
60
+ /** Original fractional value */
61
+ original: number;
62
+ /** Error introduced by rounding (-0.5 to +0.5) */
63
+ error: number;
64
+ }
65
+ /**
66
+ * Complete distribution result.
67
+ */
68
+ export interface DistributionResult {
69
+ /** Distributed dimensions for each child */
70
+ dimensions: DistributedDimension[];
71
+ /** Verification: sum of all pixels */
72
+ totalPixels: number;
73
+ /** Should equal parentDimension exactly */
74
+ isExact: boolean;
75
+ /** Distribution method used */
76
+ method: 'largest-remainder' | 'first-fit';
77
+ }
78
+ /**
79
+ * Distribute subpixel errors using the Largest Remainder Method.
80
+ *
81
+ * ## Algorithm
82
+ *
83
+ * ```
84
+ * INPUT: childDimensions = [33.333, 33.333, 33.333], parentDimension = 100
85
+ *
86
+ * Step 1: Compute floors
87
+ * floors = [33, 33, 33]
88
+ * sum(floors) = 99
89
+ *
90
+ * Step 2: Compute remainders
91
+ * remainders = [0.333, 0.333, 0.333]
92
+ *
93
+ * Step 3: Compute shortfall
94
+ * shortfall = 100 - 99 = 1
95
+ *
96
+ * Step 4: Sort by remainder (descending), distribute shortfall
97
+ * Give 1px to first element (arbitrary tie-break: leftmost)
98
+ *
99
+ * OUTPUT: [34, 33, 33]
100
+ * ```
101
+ *
102
+ * ## Tie-Breaking Strategy
103
+ *
104
+ * When remainders are equal (common for equal-width elements):
105
+ * - Distribute extra pixels left-to-right (reading order)
106
+ * - This is visually predictable and matches user expectation
107
+ */
108
+ export declare function distributeWithLargestRemainder(group: SiblingGroup): DistributionResult;
109
+ /**
110
+ * Fallback: Proportional scaling when LRM fails.
111
+ *
112
+ * Used when child sum exceeds parent (constraint violation).
113
+ */
114
+ declare function distributeProportionally(group: SiblingGroup): DistributionResult;
115
+ /**
116
+ * Parent-child containment constraint for error distribution.
117
+ */
118
+ export interface ContainmentConstraint {
119
+ parentId: EntityId;
120
+ childIds: EntityId[];
121
+ axis: 'horizontal' | 'vertical';
122
+ }
123
+ /**
124
+ * Apply error distribution to a set of sibling groups.
125
+ *
126
+ * This is called AFTER basic topology-preserving rounding,
127
+ * to ensure parent boundaries are exactly satisfied.
128
+ */
129
+ export declare function applyErrorDistribution(roundedBounds: Map<EntityId, {
130
+ x: number;
131
+ y: number;
132
+ width: number;
133
+ height: number;
134
+ }>, containments: ContainmentConstraint[]): Map<EntityId, {
135
+ x: number;
136
+ y: number;
137
+ width: number;
138
+ height: number;
139
+ }>;
140
+ export declare const _internals: {
141
+ distributeProportionally: typeof distributeProportionally;
142
+ };
143
+ export {};