@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.
- package/dist/ast/types.d.ts +403 -0
- package/dist/ast/types.js +33 -0
- package/dist/compiler/chunk-splitter.d.ts +98 -0
- package/dist/compiler/chunk-splitter.js +361 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.js +17 -0
- package/dist/rasterizer/__tests__/error-distribution.test.d.ts +7 -0
- package/dist/rasterizer/__tests__/error-distribution.test.js +322 -0
- package/dist/rasterizer/canvas-mapper.d.ts +280 -0
- package/dist/rasterizer/canvas-mapper.js +414 -0
- package/dist/rasterizer/error-distribution.d.ts +143 -0
- package/dist/rasterizer/error-distribution.js +231 -0
- package/dist/rasterizer/gradient-mapper.d.ts +223 -0
- package/dist/rasterizer/gradient-mapper.js +352 -0
- package/dist/rasterizer/topology-rounding.d.ts +151 -0
- package/dist/rasterizer/topology-rounding.js +347 -0
- package/dist/runtime/__tests__/event-backpressure.test.d.ts +10 -0
- package/dist/runtime/__tests__/event-backpressure.test.js +190 -0
- package/dist/runtime/event-backpressure.d.ts +393 -0
- package/dist/runtime/event-backpressure.js +458 -0
- package/dist/runtime/render-loop.d.ts +277 -0
- package/dist/runtime/render-loop.js +435 -0
- package/dist/runtime/wasm-resource-manager.d.ts +122 -0
- package/dist/runtime/wasm-resource-manager.js +253 -0
- package/dist/runtime/wgpu-renderer-adapter.d.ts +168 -0
- package/dist/runtime/wgpu-renderer-adapter.js +230 -0
- package/dist/semantic/__tests__/semantic-translator.test.d.ts +4 -0
- package/dist/semantic/__tests__/semantic-translator.test.js +203 -0
- package/dist/semantic/semantic-translator.d.ts +229 -0
- package/dist/semantic/semantic-translator.js +398 -0
- package/package.json +28 -0
- package/playwright-report/data/0bafe4e0863f0e244bba68a838f73241f8f2efaa.md +226 -0
- package/playwright-report/data/9281aca8abfb06c6cecb35d5ddd13d61f8c752d8.md +226 -0
- package/playwright-report/index.html +90 -0
- package/playwright.config.ts +160 -0
- package/screenshot-chrome.png +0 -0
- package/screenshots/visual-demo-verification.png +0 -0
- package/screenshots/visual-demo.png +0 -0
- package/src/ast/types.ts +473 -0
- package/src/compiler/chunk-splitter.ts +534 -0
- package/src/index.ts +62 -0
- package/src/rasterizer/__tests__/error-distribution.test.ts +382 -0
- package/src/rasterizer/canvas-mapper.ts +677 -0
- package/src/rasterizer/error-distribution.ts +344 -0
- package/src/rasterizer/gradient-mapper.ts +563 -0
- package/src/rasterizer/topology-rounding.ts +499 -0
- package/src/runtime/__tests__/event-backpressure.test.ts +254 -0
- package/src/runtime/event-backpressure.ts +622 -0
- package/src/runtime/render-loop.ts +660 -0
- package/src/runtime/wasm-resource-manager.ts +349 -0
- package/src/runtime/wgpu-renderer-adapter.ts +318 -0
- package/src/semantic/__tests__/semantic-translator.test.ts +263 -0
- package/src/semantic/semantic-translator.ts +637 -0
- package/test-results/.last-run.json +4 -0
- package/tests/e2e/async-race.spec.ts +612 -0
- package/tests/e2e/bilayer-sync.spec.ts +405 -0
- package/tests/e2e/failures/.gitkeep +0 -0
- package/tests/e2e/fullstack.spec.ts +681 -0
- package/tests/e2e/g1-continuity.spec.ts +703 -0
- package/tests/e2e/golden/.gitkeep +0 -0
- package/tests/e2e/golden/conic-color-wheel.raw +0 -0
- package/tests/e2e/golden/conic-color-wheel.sha256 +1 -0
- package/tests/e2e/golden/conic-rotated.raw +0 -0
- package/tests/e2e/golden/conic-rotated.sha256 +1 -0
- package/tests/e2e/golden/linear-45deg.raw +0 -0
- package/tests/e2e/golden/linear-45deg.sha256 +1 -0
- package/tests/e2e/golden/linear-horizontal.raw +0 -0
- package/tests/e2e/golden/linear-horizontal.sha256 +1 -0
- package/tests/e2e/golden/linear-multi-stop.raw +0 -0
- package/tests/e2e/golden/linear-multi-stop.sha256 +1 -0
- package/tests/e2e/golden/radial-circle-center.raw +0 -0
- package/tests/e2e/golden/radial-circle-center.sha256 +1 -0
- package/tests/e2e/golden/radial-offset.raw +0 -0
- package/tests/e2e/golden/radial-offset.sha256 +1 -0
- package/tests/e2e/golden/tile-mirror.raw +0 -0
- package/tests/e2e/golden/tile-mirror.sha256 +1 -0
- package/tests/e2e/golden/tile-repeat.raw +0 -0
- package/tests/e2e/golden/tile-repeat.sha256 +1 -0
- package/tests/e2e/gradient-animation.spec.ts +606 -0
- package/tests/e2e/memory-stability.spec.ts +396 -0
- package/tests/e2e/path-topology.spec.ts +674 -0
- package/tests/e2e/performance-profile.spec.ts +501 -0
- package/tests/e2e/screenshot.spec.ts +60 -0
- package/tests/e2e/test-harness.html +1005 -0
- package/tests/e2e/text-layout.spec.ts +451 -0
- package/tests/e2e/visual-demo.html +340 -0
- package/tests/e2e/visual-regression.spec.ts +335 -0
- package/tsconfig.json +12 -0
- 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
|
+
});
|