@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,382 @@
1
+ /**
2
+ * Unit Tests for Subpixel Error Distribution
3
+ *
4
+ * These tests verify the Largest Remainder Method implementation
5
+ * guarantees spatial closure (no gaps, no overflow).
6
+ */
7
+
8
+ import { describe, it, expect } from 'vitest';
9
+ import {
10
+ distributeWithLargestRemainder,
11
+ applyErrorDistribution,
12
+ type SiblingGroup,
13
+ type ContainmentConstraint,
14
+ } from '../error-distribution';
15
+
16
+ describe('Largest Remainder Method', () => {
17
+ /**
18
+ * CRITICAL TEST: Architect's Decision #2
19
+ *
20
+ * "幅100pxのコンテナ内に、幅33.333...pxの要素が3つ、
21
+ * 隙間なく隣接して配置される"
22
+ *
23
+ * This is the canonical case that motivated error distribution.
24
+ */
25
+ it('distributes 100px among 3 equal children (33.333...px each) without gaps', () => {
26
+ // Arrange
27
+ const group: SiblingGroup = {
28
+ parentId: 1,
29
+ childIds: [10, 11, 12],
30
+ axis: 'horizontal',
31
+ parentDimension: 100,
32
+ childDimensions: new Map([
33
+ [10, 100 / 3], // 33.333...
34
+ [11, 100 / 3], // 33.333...
35
+ [12, 100 / 3], // 33.333...
36
+ ]),
37
+ };
38
+
39
+ // Act
40
+ const result = distributeWithLargestRemainder(group);
41
+
42
+ // Assert: Sum must equal parent exactly
43
+ expect(result.totalPixels).toBe(100);
44
+ expect(result.isExact).toBe(true);
45
+
46
+ // Assert: Each child gets 33 or 34 pixels
47
+ const pixels = result.dimensions.map(d => d.pixels);
48
+ expect(pixels.every(p => p === 33 || p === 34)).toBe(true);
49
+
50
+ // Assert: Exactly one child gets the extra pixel
51
+ const count34 = pixels.filter(p => p === 34).length;
52
+ const count33 = pixels.filter(p => p === 33).length;
53
+ expect(count34).toBe(1);
54
+ expect(count33).toBe(2);
55
+
56
+ // Assert: Sum is exactly 100 (33 + 33 + 34 = 100)
57
+ expect(pixels.reduce((a, b) => a + b, 0)).toBe(100);
58
+
59
+ // Assert: Distribution is [34, 33, 33] (leftmost gets extra due to tie-break)
60
+ expect(pixels).toEqual([34, 33, 33]);
61
+ });
62
+
63
+ it('handles exact division (no remainder)', () => {
64
+ // Arrange: 100px / 4 = 25px exactly
65
+ const group: SiblingGroup = {
66
+ parentId: 1,
67
+ childIds: [10, 11, 12, 13],
68
+ axis: 'horizontal',
69
+ parentDimension: 100,
70
+ childDimensions: new Map([
71
+ [10, 25],
72
+ [11, 25],
73
+ [12, 25],
74
+ [13, 25],
75
+ ]),
76
+ };
77
+
78
+ // Act
79
+ const result = distributeWithLargestRemainder(group);
80
+
81
+ // Assert
82
+ expect(result.totalPixels).toBe(100);
83
+ expect(result.isExact).toBe(true);
84
+ expect(result.dimensions.map(d => d.pixels)).toEqual([25, 25, 25, 25]);
85
+ });
86
+
87
+ it('distributes multiple extra pixels by remainder priority', () => {
88
+ // Arrange: 100px for [40.9, 30.8, 28.3]
89
+ // floors: [40, 30, 28] = 98
90
+ // remainders: [0.9, 0.8, 0.3]
91
+ // shortfall: 2px
92
+ // Extra pixels go to: 40.9 → 41, 30.8 → 31
93
+ const group: SiblingGroup = {
94
+ parentId: 1,
95
+ childIds: [10, 11, 12],
96
+ axis: 'horizontal',
97
+ parentDimension: 100,
98
+ childDimensions: new Map([
99
+ [10, 40.9],
100
+ [11, 30.8],
101
+ [12, 28.3],
102
+ ]),
103
+ };
104
+
105
+ // Act
106
+ const result = distributeWithLargestRemainder(group);
107
+
108
+ // Assert
109
+ expect(result.totalPixels).toBe(100);
110
+ expect(result.isExact).toBe(true);
111
+ expect(result.dimensions.map(d => d.pixels)).toEqual([41, 31, 28]);
112
+ });
113
+
114
+ it('handles single child (trivial case)', () => {
115
+ const group: SiblingGroup = {
116
+ parentId: 1,
117
+ childIds: [10],
118
+ axis: 'horizontal',
119
+ parentDimension: 100,
120
+ childDimensions: new Map([[10, 100]]),
121
+ };
122
+
123
+ const result = distributeWithLargestRemainder(group);
124
+
125
+ expect(result.totalPixels).toBe(100);
126
+ expect(result.dimensions[0].pixels).toBe(100);
127
+ });
128
+
129
+ it('handles empty children', () => {
130
+ const group: SiblingGroup = {
131
+ parentId: 1,
132
+ childIds: [],
133
+ axis: 'horizontal',
134
+ parentDimension: 100,
135
+ childDimensions: new Map(),
136
+ };
137
+
138
+ const result = distributeWithLargestRemainder(group);
139
+
140
+ expect(result.totalPixels).toBe(0);
141
+ expect(result.dimensions).toEqual([]);
142
+ });
143
+
144
+ it('ties are broken by layout order (leftmost first)', () => {
145
+ // Arrange: 10px for [3.5, 3.5] - equal remainders
146
+ // floors: [3, 3] = 6
147
+ // remainders: [0.5, 0.5] - TIE!
148
+ // shortfall: 4px (wait, that's wrong)
149
+ // Actually: 10 - 6 = 4... no, 3.5 + 3.5 = 7, not 10
150
+ // Let me fix: 7px for [3.5, 3.5]
151
+ const group: SiblingGroup = {
152
+ parentId: 1,
153
+ childIds: [10, 11],
154
+ axis: 'horizontal',
155
+ parentDimension: 7,
156
+ childDimensions: new Map([
157
+ [10, 3.5],
158
+ [11, 3.5],
159
+ ]),
160
+ };
161
+
162
+ // Act
163
+ const result = distributeWithLargestRemainder(group);
164
+
165
+ // Assert: shortfall is 1, so first element (leftmost) gets it
166
+ expect(result.totalPixels).toBe(7);
167
+ expect(result.dimensions.map(d => d.pixels)).toEqual([4, 3]);
168
+ });
169
+
170
+ it('handles vertical axis', () => {
171
+ const group: SiblingGroup = {
172
+ parentId: 1,
173
+ childIds: [10, 11, 12],
174
+ axis: 'vertical',
175
+ parentDimension: 100,
176
+ childDimensions: new Map([
177
+ [10, 100 / 3],
178
+ [11, 100 / 3],
179
+ [12, 100 / 3],
180
+ ]),
181
+ };
182
+
183
+ const result = distributeWithLargestRemainder(group);
184
+
185
+ expect(result.totalPixels).toBe(100);
186
+ expect(result.isExact).toBe(true);
187
+ });
188
+
189
+ it('records error for each element', () => {
190
+ const group: SiblingGroup = {
191
+ parentId: 1,
192
+ childIds: [10, 11, 12],
193
+ axis: 'horizontal',
194
+ parentDimension: 100,
195
+ childDimensions: new Map([
196
+ [10, 100 / 3], // ~33.333
197
+ [11, 100 / 3],
198
+ [12, 100 / 3],
199
+ ]),
200
+ };
201
+
202
+ const result = distributeWithLargestRemainder(group);
203
+
204
+ // First element: 34 - 33.333... = +0.666...
205
+ expect(result.dimensions[0].error).toBeCloseTo(0.6667, 3);
206
+
207
+ // Other elements: 33 - 33.333... = -0.333...
208
+ expect(result.dimensions[1].error).toBeCloseTo(-0.3333, 3);
209
+ expect(result.dimensions[2].error).toBeCloseTo(-0.3333, 3);
210
+ });
211
+ });
212
+
213
+ describe('applyErrorDistribution (Integration)', () => {
214
+ it('adjusts child bounds to fit parent exactly', () => {
215
+ // Arrange
216
+ const roundedBounds = new Map([
217
+ [1, { x: 0, y: 0, width: 100, height: 50 }], // Parent
218
+ [10, { x: 0, y: 0, width: 33, height: 50 }], // Child 1 (naive)
219
+ [11, { x: 33, y: 0, width: 33, height: 50 }], // Child 2
220
+ [12, { x: 66, y: 0, width: 33, height: 50 }], // Child 3
221
+ // Sum: 33 + 33 + 33 = 99 ← GAP!
222
+ ]);
223
+
224
+ const containments: ContainmentConstraint[] = [
225
+ {
226
+ parentId: 1,
227
+ childIds: [10, 11, 12],
228
+ axis: 'horizontal',
229
+ },
230
+ ];
231
+
232
+ // Act
233
+ const result = applyErrorDistribution(roundedBounds, containments);
234
+
235
+ // Assert: Children sum to parent width exactly
236
+ const child1 = result.get(10)!;
237
+ const child2 = result.get(11)!;
238
+ const child3 = result.get(12)!;
239
+
240
+ const totalWidth = child1.width + child2.width + child3.width;
241
+ expect(totalWidth).toBe(100);
242
+
243
+ // Assert: Children are contiguous (no gaps)
244
+ expect(child2.x).toBe(child1.x + child1.width);
245
+ expect(child3.x).toBe(child2.x + child2.width);
246
+ });
247
+
248
+ it('handles multiple containment constraints', () => {
249
+ const roundedBounds = new Map([
250
+ // Horizontal container
251
+ [1, { x: 0, y: 0, width: 100, height: 50 }],
252
+ [10, { x: 0, y: 0, width: 33, height: 50 }],
253
+ [11, { x: 33, y: 0, width: 33, height: 50 }],
254
+ [12, { x: 66, y: 0, width: 33, height: 50 }],
255
+ // Vertical container
256
+ [2, { x: 0, y: 50, width: 100, height: 100 }],
257
+ [20, { x: 0, y: 50, width: 100, height: 33 }],
258
+ [21, { x: 0, y: 83, width: 100, height: 33 }],
259
+ [22, { x: 0, y: 116, width: 100, height: 33 }],
260
+ ]);
261
+
262
+ const containments: ContainmentConstraint[] = [
263
+ { parentId: 1, childIds: [10, 11, 12], axis: 'horizontal' },
264
+ { parentId: 2, childIds: [20, 21, 22], axis: 'vertical' },
265
+ ];
266
+
267
+ const result = applyErrorDistribution(roundedBounds, containments);
268
+
269
+ // Horizontal container
270
+ const hTotal = result.get(10)!.width + result.get(11)!.width + result.get(12)!.width;
271
+ expect(hTotal).toBe(100);
272
+
273
+ // Vertical container
274
+ const vTotal = result.get(20)!.height + result.get(21)!.height + result.get(22)!.height;
275
+ expect(vTotal).toBe(100);
276
+ });
277
+ });
278
+
279
+ describe('Edge Cases', () => {
280
+ it('handles very small fractional differences', () => {
281
+ // 100px / 7 = 14.285714...
282
+ const group: SiblingGroup = {
283
+ parentId: 1,
284
+ childIds: [1, 2, 3, 4, 5, 6, 7],
285
+ axis: 'horizontal',
286
+ parentDimension: 100,
287
+ childDimensions: new Map([
288
+ [1, 100 / 7],
289
+ [2, 100 / 7],
290
+ [3, 100 / 7],
291
+ [4, 100 / 7],
292
+ [5, 100 / 7],
293
+ [6, 100 / 7],
294
+ [7, 100 / 7],
295
+ ]),
296
+ };
297
+
298
+ const result = distributeWithLargestRemainder(group);
299
+
300
+ expect(result.totalPixels).toBe(100);
301
+ expect(result.isExact).toBe(true);
302
+
303
+ // 7 * 14 = 98, shortfall = 2
304
+ // Two elements get 15px, five get 14px
305
+ const fifteens = result.dimensions.filter(d => d.pixels === 15).length;
306
+ const fourteens = result.dimensions.filter(d => d.pixels === 14).length;
307
+ expect(fifteens).toBe(2);
308
+ expect(fourteens).toBe(5);
309
+ });
310
+
311
+ it('handles zero-width children', () => {
312
+ const group: SiblingGroup = {
313
+ parentId: 1,
314
+ childIds: [10, 11],
315
+ axis: 'horizontal',
316
+ parentDimension: 100,
317
+ childDimensions: new Map([
318
+ [10, 100],
319
+ [11, 0],
320
+ ]),
321
+ };
322
+
323
+ const result = distributeWithLargestRemainder(group);
324
+
325
+ expect(result.totalPixels).toBe(100);
326
+ expect(result.dimensions[0].pixels).toBe(100);
327
+ expect(result.dimensions[1].pixels).toBe(0);
328
+ });
329
+
330
+ it('uses proportional scaling when children exceed parent', () => {
331
+ // Children sum to 150, but parent is only 100
332
+ const group: SiblingGroup = {
333
+ parentId: 1,
334
+ childIds: [10, 11, 12],
335
+ axis: 'horizontal',
336
+ parentDimension: 100,
337
+ childDimensions: new Map([
338
+ [10, 50],
339
+ [11, 50],
340
+ [12, 50],
341
+ ]),
342
+ };
343
+
344
+ const result = distributeWithLargestRemainder(group);
345
+
346
+ // Falls back to proportional: each gets ~33.33
347
+ expect(result.totalPixels).toBe(100);
348
+ expect(result.method).toBe('first-fit');
349
+ });
350
+ });
351
+
352
+ describe('Proof: No Gaps or Overflow', () => {
353
+ /**
354
+ * Property test: For ANY valid input, sum(children) === parent
355
+ */
356
+ it('maintains invariant for random inputs', () => {
357
+ const testCases = [
358
+ { parent: 100, children: [100 / 3, 100 / 3, 100 / 3] },
359
+ { parent: 1000, children: [1000 / 7, 1000 / 7, 1000 / 7, 1000 / 7, 1000 / 7, 1000 / 7, 1000 / 7] },
360
+ { parent: 50, children: [12.5, 12.5, 12.5, 12.5] },
361
+ { parent: 99, children: [33, 33, 33] },
362
+ { parent: 101, children: [50.5, 50.5] },
363
+ { parent: 1, children: [0.5, 0.5] },
364
+ { parent: 2, children: [0.6, 0.7, 0.7] },
365
+ ];
366
+
367
+ for (const tc of testCases) {
368
+ const group: SiblingGroup = {
369
+ parentId: 1,
370
+ childIds: tc.children.map((_, i) => i + 10),
371
+ axis: 'horizontal',
372
+ parentDimension: tc.parent,
373
+ childDimensions: new Map(tc.children.map((c, i) => [i + 10, c])),
374
+ };
375
+
376
+ const result = distributeWithLargestRemainder(group);
377
+
378
+ expect(result.totalPixels).toBe(tc.parent);
379
+ expect(result.isExact).toBe(true);
380
+ }
381
+ });
382
+ });