@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,499 @@
1
+ /**
2
+ * Topology-Preserving Rounding Algorithm
3
+ *
4
+ * This module implements the rasterization layer that projects P-dimension
5
+ * rational coordinates to discrete pixel coordinates while preserving
6
+ * topological relationships (adjacency, containment, ordering).
7
+ *
8
+ * ## The Problem
9
+ *
10
+ * Given two adjacent surfaces A and B where:
11
+ * A.right = 100.333... (rational)
12
+ * B.left = 100.333... (same rational)
13
+ *
14
+ * Naive rounding may produce:
15
+ * A.right = 100px (floor)
16
+ * B.left = 101px (ceil)
17
+ *
18
+ * This creates a 1px gap that violates the topological constraint
19
+ * that A and B are adjacent (no gap, no overlap).
20
+ *
21
+ * ## Solution: Constraint-Aware Rounding
22
+ *
23
+ * Instead of rounding each coordinate independently, we:
24
+ * 1. Build a graph of topological relationships (adjacency, containment)
25
+ * 2. Partition coordinates into equivalence classes (same rational = same pixel)
26
+ * 3. Round equivalence classes together
27
+ * 4. Propagate rounding decisions through the constraint graph
28
+ *
29
+ * ## Algorithm
30
+ *
31
+ * ```
32
+ * INPUT:
33
+ * - Set of surfaces S with rational bounds
34
+ * - Topological constraints T (adjacency, containment)
35
+ * - Device pixel ratio DPR
36
+ *
37
+ * OUTPUT:
38
+ * - Integer pixel coordinates for all surfaces
39
+ * - Guarantee: topology is preserved
40
+ *
41
+ * ALGORITHM:
42
+ *
43
+ * Phase 1: Build Coordinate Equivalence Classes
44
+ * For each unique rational value r:
45
+ * equiv[r] = { all coordinates that equal r }
46
+ *
47
+ * Phase 2: Compute Rounding Constraints
48
+ * For each adjacency constraint (A.right = B.left):
49
+ * round(A.right) MUST equal round(B.left)
50
+ * For each ordering constraint (A.right < B.left):
51
+ * round(A.right) MUST be < round(B.left)
52
+ *
53
+ * Phase 3: Propagate Rounding Decisions
54
+ * Using constraint propagation:
55
+ * - Start with coordinates that have no constraints (free variables)
56
+ * - Round them to nearest integer
57
+ * - Propagate to constrained coordinates
58
+ * - Resolve conflicts by adjusting adjacent surfaces symmetrically
59
+ *
60
+ * Phase 4: Verify Topology Preservation
61
+ * Assert all topological constraints are satisfied
62
+ * ```
63
+ */
64
+
65
+ import type { EntityId, Rational, RasterBounds, PVectorBounds } from '../ast/types';
66
+
67
+ // =============================================================================
68
+ // Types
69
+ // =============================================================================
70
+
71
+ /**
72
+ * A coordinate in the pre-rasterization space.
73
+ */
74
+ interface RationalCoord {
75
+ entityId: EntityId;
76
+ edge: 'left' | 'right' | 'top' | 'bottom';
77
+ value: Rational;
78
+ }
79
+
80
+ /**
81
+ * Topological constraint between coordinates.
82
+ */
83
+ type TopoConstraint =
84
+ | { type: 'equal'; a: CoordRef; b: CoordRef } // A and B must round to same pixel
85
+ | { type: 'less-than'; a: CoordRef; b: CoordRef } // A must round to less than B
86
+ | { type: 'adjacent'; a: CoordRef; b: CoordRef }; // A.right touches B.left (no gap, no overlap)
87
+
88
+ interface CoordRef {
89
+ entityId: EntityId;
90
+ edge: 'left' | 'right' | 'top' | 'bottom';
91
+ }
92
+
93
+ /**
94
+ * Result of the rounding algorithm.
95
+ */
96
+ export interface RoundingResult {
97
+ /** Rasterized bounds for each entity */
98
+ bounds: Map<EntityId, RasterBounds>;
99
+
100
+ /** Any topology violations detected (should be empty if algorithm is correct) */
101
+ violations: TopologyViolation[];
102
+
103
+ /** Statistics about the rounding process */
104
+ stats: RoundingStats;
105
+ }
106
+
107
+ interface TopologyViolation {
108
+ constraint: TopoConstraint;
109
+ message: string;
110
+ }
111
+
112
+ interface RoundingStats {
113
+ totalCoordinates: number;
114
+ equivalenceClasses: number;
115
+ constraintsPropagated: number;
116
+ conflictsResolved: number;
117
+ }
118
+
119
+ // =============================================================================
120
+ // Core Algorithm
121
+ // =============================================================================
122
+
123
+ /**
124
+ * Topology-preserving rounding entry point.
125
+ */
126
+ export function roundWithTopologyPreservation(
127
+ entities: Map<EntityId, PVectorBounds>,
128
+ constraints: TopoConstraint[],
129
+ devicePixelRatio: number,
130
+ ): RoundingResult {
131
+ const stats: RoundingStats = {
132
+ totalCoordinates: 0,
133
+ equivalenceClasses: 0,
134
+ constraintsPropagated: 0,
135
+ conflictsResolved: 0,
136
+ };
137
+
138
+ // Phase 1: Extract all coordinates and build equivalence classes
139
+ const coords = extractCoordinates(entities);
140
+ stats.totalCoordinates = coords.length;
141
+
142
+ const equivClasses = buildEquivalenceClasses(coords, constraints);
143
+ stats.equivalenceClasses = equivClasses.size;
144
+
145
+ // Phase 2: Compute rounding for each equivalence class
146
+ const roundedClasses = new Map<string, number>();
147
+
148
+ for (const [classId, members] of equivClasses) {
149
+ // All members have the same rational value
150
+ const rationalValue = members[0].value;
151
+ const floatValue = rationalToFloat(rationalValue) * devicePixelRatio;
152
+
153
+ // Default: round to nearest
154
+ roundedClasses.set(classId, Math.round(floatValue));
155
+ }
156
+
157
+ // Phase 3: Propagate constraints and resolve conflicts
158
+ const { adjusted, conflictsResolved } = propagateConstraints(
159
+ roundedClasses,
160
+ equivClasses,
161
+ constraints,
162
+ );
163
+ stats.constraintsPropagated = constraints.length;
164
+ stats.conflictsResolved = conflictsResolved;
165
+
166
+ // Phase 4: Build final bounds
167
+ const bounds = buildFinalBounds(entities, adjusted, equivClasses, devicePixelRatio);
168
+
169
+ // Phase 5: Verify topology
170
+ const violations = verifyTopology(bounds, constraints);
171
+
172
+ return { bounds, violations, stats };
173
+ }
174
+
175
+ // =============================================================================
176
+ // Phase 1: Coordinate Extraction and Equivalence Classes
177
+ // =============================================================================
178
+
179
+ function extractCoordinates(entities: Map<EntityId, PVectorBounds>): RationalCoord[] {
180
+ const coords: RationalCoord[] = [];
181
+
182
+ for (const [entityId, bounds] of entities) {
183
+ coords.push(
184
+ { entityId, edge: 'left', value: bounds.topLeft.x },
185
+ { entityId, edge: 'right', value: bounds.bottomRight.x },
186
+ { entityId, edge: 'top', value: bounds.topLeft.y },
187
+ { entityId, edge: 'bottom', value: bounds.bottomRight.y },
188
+ );
189
+ }
190
+
191
+ return coords;
192
+ }
193
+
194
+ /**
195
+ * Build equivalence classes from coordinates and equality constraints.
196
+ *
197
+ * Two coordinates are in the same class if:
198
+ * 1. They have the same rational value, OR
199
+ * 2. They are connected by an 'equal' or 'adjacent' constraint
200
+ */
201
+ function buildEquivalenceClasses(
202
+ coords: RationalCoord[],
203
+ constraints: TopoConstraint[],
204
+ ): Map<string, RationalCoord[]> {
205
+ // Union-Find data structure
206
+ const parent = new Map<string, string>();
207
+
208
+ const coordKey = (c: CoordRef): string => `${c.entityId}:${c.edge}`;
209
+
210
+ const find = (key: string): string => {
211
+ if (!parent.has(key)) {
212
+ parent.set(key, key);
213
+ return key;
214
+ }
215
+ if (parent.get(key) !== key) {
216
+ parent.set(key, find(parent.get(key)!));
217
+ }
218
+ return parent.get(key)!;
219
+ };
220
+
221
+ const union = (a: string, b: string): void => {
222
+ const rootA = find(a);
223
+ const rootB = find(b);
224
+ if (rootA !== rootB) {
225
+ parent.set(rootA, rootB);
226
+ }
227
+ };
228
+
229
+ // Initialize each coord as its own class
230
+ for (const coord of coords) {
231
+ const key = `${coord.entityId}:${coord.edge}`;
232
+ parent.set(key, key);
233
+ }
234
+
235
+ // Union coordinates with same rational value
236
+ const byValue = new Map<string, RationalCoord[]>();
237
+ for (const coord of coords) {
238
+ const valKey = rationalKey(coord.value);
239
+ if (!byValue.has(valKey)) {
240
+ byValue.set(valKey, []);
241
+ }
242
+ byValue.get(valKey)!.push(coord);
243
+ }
244
+
245
+ for (const [, group] of byValue) {
246
+ if (group.length > 1) {
247
+ const first = coordKey({ entityId: group[0].entityId, edge: group[0].edge });
248
+ for (let i = 1; i < group.length; i++) {
249
+ union(first, coordKey({ entityId: group[i].entityId, edge: group[i].edge }));
250
+ }
251
+ }
252
+ }
253
+
254
+ // Union by equality/adjacency constraints
255
+ for (const c of constraints) {
256
+ if (c.type === 'equal' || c.type === 'adjacent') {
257
+ union(coordKey(c.a), coordKey(c.b));
258
+ }
259
+ }
260
+
261
+ // Build final classes
262
+ const classes = new Map<string, RationalCoord[]>();
263
+ for (const coord of coords) {
264
+ const key = `${coord.entityId}:${coord.edge}`;
265
+ const root = find(key);
266
+ if (!classes.has(root)) {
267
+ classes.set(root, []);
268
+ }
269
+ classes.get(root)!.push(coord);
270
+ }
271
+
272
+ return classes;
273
+ }
274
+
275
+ function rationalKey(r: Rational): string {
276
+ // Normalize to lowest terms for consistent keying
277
+ const gcd = bigIntGcd(r.numerator < 0n ? -r.numerator : r.numerator, r.denominator);
278
+ const num = r.numerator / gcd;
279
+ const den = r.denominator / gcd;
280
+ return `${num}/${den}`;
281
+ }
282
+
283
+ function bigIntGcd(a: bigint, b: bigint): bigint {
284
+ while (b !== 0n) {
285
+ const t = b;
286
+ b = a % b;
287
+ a = t;
288
+ }
289
+ return a;
290
+ }
291
+
292
+ function rationalToFloat(r: Rational): number {
293
+ return Number(r.numerator) / Number(r.denominator);
294
+ }
295
+
296
+ // =============================================================================
297
+ // Phase 3: Constraint Propagation
298
+ // =============================================================================
299
+
300
+ interface PropagationResult {
301
+ adjusted: Map<string, number>;
302
+ conflictsResolved: number;
303
+ }
304
+
305
+ /**
306
+ * Propagate rounding decisions through less-than constraints.
307
+ *
308
+ * If A < B in rational space, we must ensure round(A) < round(B) in pixel space.
309
+ * If rounding would violate this, we adjust by:
310
+ * 1. Decreasing A by 1, OR
311
+ * 2. Increasing B by 1
312
+ *
313
+ * We choose the option that minimizes total visual shift.
314
+ */
315
+ function propagateConstraints(
316
+ initial: Map<string, number>,
317
+ equivClasses: Map<string, RationalCoord[]>,
318
+ constraints: TopoConstraint[],
319
+ ): PropagationResult {
320
+ const adjusted = new Map(initial);
321
+ let conflictsResolved = 0;
322
+
323
+ // Build class lookup
324
+ const coordToClass = new Map<string, string>();
325
+ for (const [classId, members] of equivClasses) {
326
+ for (const m of members) {
327
+ coordToClass.set(`${m.entityId}:${m.edge}`, classId);
328
+ }
329
+ }
330
+
331
+ // Process less-than constraints
332
+ for (const c of constraints) {
333
+ if (c.type !== 'less-than') continue;
334
+
335
+ const classA = coordToClass.get(`${c.a.entityId}:${c.a.edge}`);
336
+ const classB = coordToClass.get(`${c.b.entityId}:${c.b.edge}`);
337
+
338
+ if (!classA || !classB) continue;
339
+
340
+ const valA = adjusted.get(classA) ?? 0;
341
+ const valB = adjusted.get(classB) ?? 0;
342
+
343
+ // Must satisfy: valA < valB
344
+ if (valA >= valB) {
345
+ // Conflict! Need to adjust.
346
+ // Strategy: Create a gap of 1px
347
+
348
+ // Option 1: Decrease A
349
+ const costDecreaseA = computeAdjustmentCost(classA, valA, valA - 1, equivClasses);
350
+
351
+ // Option 2: Increase B
352
+ const costIncreaseB = computeAdjustmentCost(classB, valB, valB + 1, equivClasses);
353
+
354
+ if (costDecreaseA <= costIncreaseB) {
355
+ adjusted.set(classA, valB - 1);
356
+ } else {
357
+ adjusted.set(classB, valA + 1);
358
+ }
359
+
360
+ conflictsResolved++;
361
+ }
362
+ }
363
+
364
+ return { adjusted, conflictsResolved };
365
+ }
366
+
367
+ /**
368
+ * Compute the visual cost of adjusting a coordinate.
369
+ *
370
+ * Cost is proportional to:
371
+ * - Number of entities affected
372
+ * - Distance of adjustment
373
+ */
374
+ function computeAdjustmentCost(
375
+ classId: string,
376
+ from: number,
377
+ to: number,
378
+ equivClasses: Map<string, RationalCoord[]>,
379
+ ): number {
380
+ const members = equivClasses.get(classId) ?? [];
381
+ const distance = Math.abs(to - from);
382
+ return members.length * distance;
383
+ }
384
+
385
+ // =============================================================================
386
+ // Phase 4: Build Final Bounds
387
+ // =============================================================================
388
+
389
+ function buildFinalBounds(
390
+ entities: Map<EntityId, PVectorBounds>,
391
+ roundedClasses: Map<string, number>,
392
+ equivClasses: Map<string, RationalCoord[]>,
393
+ devicePixelRatio: number,
394
+ ): Map<EntityId, RasterBounds> {
395
+ // Build coord to class lookup
396
+ const coordToClass = new Map<string, string>();
397
+ for (const [classId, members] of equivClasses) {
398
+ for (const m of members) {
399
+ coordToClass.set(`${m.entityId}:${m.edge}`, classId);
400
+ }
401
+ }
402
+
403
+ const bounds = new Map<EntityId, RasterBounds>();
404
+
405
+ for (const [entityId] of entities) {
406
+ const leftClass = coordToClass.get(`${entityId}:left`);
407
+ const rightClass = coordToClass.get(`${entityId}:right`);
408
+ const topClass = coordToClass.get(`${entityId}:top`);
409
+ const bottomClass = coordToClass.get(`${entityId}:bottom`);
410
+
411
+ const left = roundedClasses.get(leftClass!) ?? 0;
412
+ const right = roundedClasses.get(rightClass!) ?? 0;
413
+ const top = roundedClasses.get(topClass!) ?? 0;
414
+ const bottom = roundedClasses.get(bottomClass!) ?? 0;
415
+
416
+ // Convert from device pixels to CSS pixels
417
+ bounds.set(entityId, {
418
+ x: left / devicePixelRatio,
419
+ y: top / devicePixelRatio,
420
+ width: (right - left) / devicePixelRatio,
421
+ height: (bottom - top) / devicePixelRatio,
422
+ });
423
+ }
424
+
425
+ return bounds;
426
+ }
427
+
428
+ // =============================================================================
429
+ // Phase 5: Topology Verification
430
+ // =============================================================================
431
+
432
+ function verifyTopology(
433
+ bounds: Map<EntityId, RasterBounds>,
434
+ constraints: TopoConstraint[],
435
+ ): TopologyViolation[] {
436
+ const violations: TopologyViolation[] = [];
437
+
438
+ for (const c of constraints) {
439
+ const boundsA = bounds.get(c.a.entityId);
440
+ const boundsB = bounds.get(c.b.entityId);
441
+
442
+ if (!boundsA || !boundsB) continue;
443
+
444
+ const valA = getEdgeValue(boundsA, c.a.edge);
445
+ const valB = getEdgeValue(boundsB, c.b.edge);
446
+
447
+ switch (c.type) {
448
+ case 'equal':
449
+ if (valA !== valB) {
450
+ violations.push({
451
+ constraint: c,
452
+ message: `Equal constraint violated: ${valA} !== ${valB}`,
453
+ });
454
+ }
455
+ break;
456
+
457
+ case 'adjacent':
458
+ if (valA !== valB) {
459
+ violations.push({
460
+ constraint: c,
461
+ message: `Adjacent constraint violated: ${valA} !== ${valB} (gap or overlap)`,
462
+ });
463
+ }
464
+ break;
465
+
466
+ case 'less-than':
467
+ if (valA >= valB) {
468
+ violations.push({
469
+ constraint: c,
470
+ message: `Less-than constraint violated: ${valA} >= ${valB}`,
471
+ });
472
+ }
473
+ break;
474
+ }
475
+ }
476
+
477
+ return violations;
478
+ }
479
+
480
+ function getEdgeValue(bounds: RasterBounds, edge: 'left' | 'right' | 'top' | 'bottom'): number {
481
+ switch (edge) {
482
+ case 'left': return bounds.x;
483
+ case 'right': return bounds.x + bounds.width;
484
+ case 'top': return bounds.y;
485
+ case 'bottom': return bounds.y + bounds.height;
486
+ }
487
+ }
488
+
489
+ // =============================================================================
490
+ // Exports for Testing
491
+ // =============================================================================
492
+
493
+ export const _internals = {
494
+ extractCoordinates,
495
+ buildEquivalenceClasses,
496
+ propagateConstraints,
497
+ verifyTopology,
498
+ rationalToFloat,
499
+ };