@viewscript/renderer 0.1.0-20260515015257 → 0.1.0-20260515042015
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/compiler/chunk-splitter.d.ts +1 -1
- package/dist/rasterizer/canvas-mapper.d.ts +1 -1
- package/dist/rasterizer/error-distribution.d.ts +1 -1
- package/dist/rasterizer/gradient-mapper.d.ts +1 -1
- package/dist/rasterizer/topology-rounding.d.ts +1 -1
- package/dist/runtime/event-backpressure.d.ts +1 -1
- package/dist/runtime/render-loop.d.ts +1 -1
- package/dist/semantic/semantic-translator.d.ts +1 -1
- package/package.json +1 -1
- package/src/compiler/chunk-splitter.ts +1 -1
- package/src/rasterizer/__tests__/error-distribution.test.ts +11 -11
- package/src/rasterizer/canvas-mapper.ts +1 -1
- package/src/rasterizer/error-distribution.ts +1 -1
- package/src/rasterizer/gradient-mapper.ts +1 -1
- package/src/rasterizer/topology-rounding.ts +1 -1
- package/src/runtime/__tests__/event-backpressure.test.ts +3 -3
- package/src/runtime/__tests__/ffi-integration.test.ts +8 -0
- package/src/runtime/event-backpressure.ts +1 -1
- package/src/runtime/render-loop.ts +1 -1
- package/src/semantic/__tests__/semantic-translator.test.ts +2 -2
- package/src/semantic/semantic-translator.ts +1 -1
- package/tsconfig.json +2 -1
- package/dist/rasterizer/__tests__/error-distribution.test.d.ts +0 -7
- package/dist/rasterizer/__tests__/error-distribution.test.js +0 -322
- package/dist/runtime/__tests__/event-backpressure.test.d.ts +0 -10
- package/dist/runtime/__tests__/event-backpressure.test.js +0 -190
- package/dist/runtime/__tests__/ffi-dispatcher.test.d.ts +0 -11
- package/dist/runtime/__tests__/ffi-dispatcher.test.js +0 -408
- package/dist/runtime/__tests__/ffi-integration.test.d.ts +0 -9
- package/dist/runtime/__tests__/ffi-integration.test.js +0 -514
- package/dist/runtime/__tests__/wasm-solver-bridge.test.d.ts +0 -9
- package/dist/runtime/__tests__/wasm-solver-bridge.test.js +0 -214
- package/dist/semantic/__tests__/semantic-translator.test.d.ts +0 -4
- package/dist/semantic/__tests__/semantic-translator.test.js +0 -203
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
* 5. Compute chunk dependency DAG
|
|
36
36
|
* ```
|
|
37
37
|
*/
|
|
38
|
-
import type { EntityId, ConstraintId, ChunkId, Chunk, Rational } from '../ast/types';
|
|
38
|
+
import type { EntityId, ConstraintId, ChunkId, Chunk, Rational } from '../ast/types.js';
|
|
39
39
|
interface IRConstraint {
|
|
40
40
|
id: ConstraintId;
|
|
41
41
|
target: EntityId;
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
* coordinates, ensuring seamless curve connections
|
|
29
29
|
* 3. **Fill Rule Semantics**: SVG fill-rule (nonzero/evenodd) is preserved
|
|
30
30
|
*/
|
|
31
|
-
import type { EntityId, Rational, PathCommand } from '../ast/types';
|
|
31
|
+
import type { EntityId, Rational, PathCommand } from '../ast/types.js';
|
|
32
32
|
/**
|
|
33
33
|
* Control point with resolved rational coordinates.
|
|
34
34
|
*/
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
*
|
|
35
35
|
* LRM distributes exactly (T - Σ⌊rᵢ⌋) extra pixels to achieve Σ = T.
|
|
36
36
|
*/
|
|
37
|
-
import type { EntityId } from '../ast/types';
|
|
37
|
+
import type { EntityId } from '../ast/types.js';
|
|
38
38
|
/**
|
|
39
39
|
* A sibling group: children that must sum to parent's dimension.
|
|
40
40
|
*/
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
* canvas.drawRect(bounds, paint);
|
|
37
37
|
* ```
|
|
38
38
|
*/
|
|
39
|
-
import type { Rational, RasterBounds } from '../ast/types';
|
|
39
|
+
import type { Rational, RasterBounds } from '../ast/types.js';
|
|
40
40
|
/** GPU shader backend interface (minimal subset needed for gradients) */
|
|
41
41
|
export interface GpuShaderBackend {
|
|
42
42
|
Shader: {
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
* Assert all topological constraints are satisfied
|
|
62
62
|
* ```
|
|
63
63
|
*/
|
|
64
|
-
import type { EntityId, Rational, RasterBounds, PVectorBounds } from '../ast/types';
|
|
64
|
+
import type { EntityId, Rational, RasterBounds, PVectorBounds } from '../ast/types.js';
|
|
65
65
|
/**
|
|
66
66
|
* A coordinate in the pre-rasterization space.
|
|
67
67
|
*/
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
*
|
|
35
35
|
* We NEVER touch: width, height, top, left, margin, padding (reflow triggers)
|
|
36
36
|
*/
|
|
37
|
-
import type { EntityId, RenderableEntity, RasterBounds, PVectorBounds } from '../ast/types';
|
|
37
|
+
import type { EntityId, RenderableEntity, RasterBounds, PVectorBounds } from '../ast/types.js';
|
|
38
38
|
import type { FfiDispatcher, PendingFfiCall } from './ffi-dispatcher.js';
|
|
39
39
|
import type { WasmSolverBridge } from './wasm-solver-bridge.js';
|
|
40
40
|
/**
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
* const diff = translator.compareSolutions(solution1, solution2);
|
|
35
35
|
* ```
|
|
36
36
|
*/
|
|
37
|
-
import type { EntityId } from '../ast/types';
|
|
37
|
+
import type { EntityId } from '../ast/types.js';
|
|
38
38
|
/**
|
|
39
39
|
* Raw solution from solver: "entity_id:component" -> "numerator/denominator"
|
|
40
40
|
*/
|
package/package.json
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
applyErrorDistribution,
|
|
12
12
|
type SiblingGroup,
|
|
13
13
|
type ContainmentConstraint,
|
|
14
|
-
} from '../error-distribution';
|
|
14
|
+
} from '../error-distribution.js';
|
|
15
15
|
|
|
16
16
|
describe('Largest Remainder Method', () => {
|
|
17
17
|
/**
|
|
@@ -44,17 +44,17 @@ describe('Largest Remainder Method', () => {
|
|
|
44
44
|
expect(result.isExact).toBe(true);
|
|
45
45
|
|
|
46
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);
|
|
47
|
+
const pixels = result.dimensions.map((d: { pixels: number }) => d.pixels);
|
|
48
|
+
expect(pixels.every((p: number) => p === 33 || p === 34)).toBe(true);
|
|
49
49
|
|
|
50
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;
|
|
51
|
+
const count34 = pixels.filter((p: number) => p === 34).length;
|
|
52
|
+
const count33 = pixels.filter((p: number) => p === 33).length;
|
|
53
53
|
expect(count34).toBe(1);
|
|
54
54
|
expect(count33).toBe(2);
|
|
55
55
|
|
|
56
56
|
// Assert: Sum is exactly 100 (33 + 33 + 34 = 100)
|
|
57
|
-
expect(pixels.reduce((a, b) => a + b, 0)).toBe(100);
|
|
57
|
+
expect(pixels.reduce((a: number, b: number) => a + b, 0)).toBe(100);
|
|
58
58
|
|
|
59
59
|
// Assert: Distribution is [34, 33, 33] (leftmost gets extra due to tie-break)
|
|
60
60
|
expect(pixels).toEqual([34, 33, 33]);
|
|
@@ -81,7 +81,7 @@ describe('Largest Remainder Method', () => {
|
|
|
81
81
|
// Assert
|
|
82
82
|
expect(result.totalPixels).toBe(100);
|
|
83
83
|
expect(result.isExact).toBe(true);
|
|
84
|
-
expect(result.dimensions.map(d => d.pixels)).toEqual([25, 25, 25, 25]);
|
|
84
|
+
expect(result.dimensions.map((d: { pixels: number }) => d.pixels)).toEqual([25, 25, 25, 25]);
|
|
85
85
|
});
|
|
86
86
|
|
|
87
87
|
it('distributes multiple extra pixels by remainder priority', () => {
|
|
@@ -108,7 +108,7 @@ describe('Largest Remainder Method', () => {
|
|
|
108
108
|
// Assert
|
|
109
109
|
expect(result.totalPixels).toBe(100);
|
|
110
110
|
expect(result.isExact).toBe(true);
|
|
111
|
-
expect(result.dimensions.map(d => d.pixels)).toEqual([41, 31, 28]);
|
|
111
|
+
expect(result.dimensions.map((d: { pixels: number }) => d.pixels)).toEqual([41, 31, 28]);
|
|
112
112
|
});
|
|
113
113
|
|
|
114
114
|
it('handles single child (trivial case)', () => {
|
|
@@ -164,7 +164,7 @@ describe('Largest Remainder Method', () => {
|
|
|
164
164
|
|
|
165
165
|
// Assert: shortfall is 1, so first element (leftmost) gets it
|
|
166
166
|
expect(result.totalPixels).toBe(7);
|
|
167
|
-
expect(result.dimensions.map(d => d.pixels)).toEqual([4, 3]);
|
|
167
|
+
expect(result.dimensions.map((d: { pixels: number }) => d.pixels)).toEqual([4, 3]);
|
|
168
168
|
});
|
|
169
169
|
|
|
170
170
|
it('handles vertical axis', () => {
|
|
@@ -302,8 +302,8 @@ describe('Edge Cases', () => {
|
|
|
302
302
|
|
|
303
303
|
// 7 * 14 = 98, shortfall = 2
|
|
304
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;
|
|
305
|
+
const fifteens = result.dimensions.filter((d: { pixels: number }) => d.pixels === 15).length;
|
|
306
|
+
const fourteens = result.dimensions.filter((d: { pixels: number }) => d.pixels === 14).length;
|
|
307
307
|
expect(fifteens).toBe(2);
|
|
308
308
|
expect(fourteens).toBe(5);
|
|
309
309
|
});
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
* 3. **Fill Rule Semantics**: SVG fill-rule (nonzero/evenodd) is preserved
|
|
30
30
|
*/
|
|
31
31
|
|
|
32
|
-
import type { EntityId, Rational, PathCommand, FillStyle, StrokeStyle } from '../ast/types';
|
|
32
|
+
import type { EntityId, Rational, PathCommand, FillStyle, StrokeStyle } from '../ast/types.js';
|
|
33
33
|
|
|
34
34
|
// =============================================================================
|
|
35
35
|
// Input Types (from P-Dimension Solver)
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
* LRM distributes exactly (T - Σ⌊rᵢ⌋) extra pixels to achieve Σ = T.
|
|
36
36
|
*/
|
|
37
37
|
|
|
38
|
-
import type { EntityId, Rational } from '../ast/types';
|
|
38
|
+
import type { EntityId, Rational } from '../ast/types.js';
|
|
39
39
|
|
|
40
40
|
// =============================================================================
|
|
41
41
|
// Types
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
* ```
|
|
63
63
|
*/
|
|
64
64
|
|
|
65
|
-
import type { EntityId, Rational, RasterBounds, PVectorBounds } from '../ast/types';
|
|
65
|
+
import type { EntityId, Rational, RasterBounds, PVectorBounds } from '../ast/types.js';
|
|
66
66
|
|
|
67
67
|
// =============================================================================
|
|
68
68
|
// Types
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
EventPriority,
|
|
15
15
|
type QDimensionEvent,
|
|
16
16
|
type TStateKey,
|
|
17
|
-
} from '../event-backpressure';
|
|
17
|
+
} from '../event-backpressure.js';
|
|
18
18
|
|
|
19
19
|
describe('EventBuffer', () => {
|
|
20
20
|
let buffer: EventBuffer;
|
|
@@ -187,8 +187,8 @@ describe('EventBuffer', () => {
|
|
|
187
187
|
|
|
188
188
|
expect(flushed).toHaveLength(2);
|
|
189
189
|
// Both events should be present
|
|
190
|
-
expect(flushed.some(e => e.entityId === 1)).toBe(true);
|
|
191
|
-
expect(flushed.some(e => e.entityId === 2)).toBe(true);
|
|
190
|
+
expect(flushed.some((e: QDimensionEvent) => e.entityId === 1)).toBe(true);
|
|
191
|
+
expect(flushed.some((e: QDimensionEvent) => e.entityId === 2)).toBe(true);
|
|
192
192
|
});
|
|
193
193
|
});
|
|
194
194
|
|
|
@@ -127,6 +127,10 @@ function createBindManifestContext(bindings: {
|
|
|
127
127
|
line: i + 2,
|
|
128
128
|
})),
|
|
129
129
|
triggers: [],
|
|
130
|
+
componentDecls: [],
|
|
131
|
+
components: [],
|
|
132
|
+
scene: null,
|
|
133
|
+
consts: [],
|
|
130
134
|
errors: [],
|
|
131
135
|
};
|
|
132
136
|
|
|
@@ -168,6 +172,10 @@ function createTriggerManifestContext(triggers: {
|
|
|
168
172
|
functionArgs: t.functionArgs,
|
|
169
173
|
line: i + 2,
|
|
170
174
|
})),
|
|
175
|
+
componentDecls: [],
|
|
176
|
+
components: [],
|
|
177
|
+
scene: null,
|
|
178
|
+
consts: [],
|
|
171
179
|
errors: [],
|
|
172
180
|
};
|
|
173
181
|
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* 4. Priority queues for critical events (click > mousemove)
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
-
import type { EntityId } from '../ast/types';
|
|
27
|
+
import type { EntityId } from '../ast/types.js';
|
|
28
28
|
|
|
29
29
|
// =============================================================================
|
|
30
30
|
// Types
|
|
@@ -41,7 +41,7 @@ import type {
|
|
|
41
41
|
RasterBounds,
|
|
42
42
|
PVectorBounds,
|
|
43
43
|
ChunkId,
|
|
44
|
-
} from '../ast/types';
|
|
44
|
+
} from '../ast/types.js';
|
|
45
45
|
import type { FfiDispatcher, PendingFfiCall } from './ffi-dispatcher.js';
|
|
46
46
|
import type { WasmSolverBridge } from './wasm-solver-bridge.js';
|
|
47
47
|
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
createEmptyRegistry,
|
|
10
10
|
type EntityMetadata,
|
|
11
11
|
type RawSolution,
|
|
12
|
-
} from '../semantic-translator';
|
|
12
|
+
} from '../semantic-translator.js';
|
|
13
13
|
|
|
14
14
|
describe('SemanticTranslator', () => {
|
|
15
15
|
describe('parseVarId', () => {
|
|
@@ -162,7 +162,7 @@ describe('SemanticTranslator', () => {
|
|
|
162
162
|
const result = translator.translateSolution(rawSolution, 0);
|
|
163
163
|
|
|
164
164
|
expect(result.relationships.length).toBeGreaterThan(0);
|
|
165
|
-
const alignment = result.relationships.find(r => r.type === 'alignment');
|
|
165
|
+
const alignment = result.relationships.find((r: { type: string }) => r.type === 'alignment');
|
|
166
166
|
expect(alignment).toBeDefined();
|
|
167
167
|
expect(alignment?.description).toContain('horizontally aligned');
|
|
168
168
|
});
|
package/tsconfig.json
CHANGED
|
@@ -1,322 +0,0 @@
|
|
|
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
|
-
import { describe, it, expect } from 'vitest';
|
|
8
|
-
import { distributeWithLargestRemainder, applyErrorDistribution, } from '../error-distribution';
|
|
9
|
-
describe('Largest Remainder Method', () => {
|
|
10
|
-
/**
|
|
11
|
-
* CRITICAL TEST: Architect's Decision #2
|
|
12
|
-
*
|
|
13
|
-
* "幅100pxのコンテナ内に、幅33.333...pxの要素が3つ、
|
|
14
|
-
* 隙間なく隣接して配置される"
|
|
15
|
-
*
|
|
16
|
-
* This is the canonical case that motivated error distribution.
|
|
17
|
-
*/
|
|
18
|
-
it('distributes 100px among 3 equal children (33.333...px each) without gaps', () => {
|
|
19
|
-
// Arrange
|
|
20
|
-
const group = {
|
|
21
|
-
parentId: 1,
|
|
22
|
-
childIds: [10, 11, 12],
|
|
23
|
-
axis: 'horizontal',
|
|
24
|
-
parentDimension: 100,
|
|
25
|
-
childDimensions: new Map([
|
|
26
|
-
[10, 100 / 3], // 33.333...
|
|
27
|
-
[11, 100 / 3], // 33.333...
|
|
28
|
-
[12, 100 / 3], // 33.333...
|
|
29
|
-
]),
|
|
30
|
-
};
|
|
31
|
-
// Act
|
|
32
|
-
const result = distributeWithLargestRemainder(group);
|
|
33
|
-
// Assert: Sum must equal parent exactly
|
|
34
|
-
expect(result.totalPixels).toBe(100);
|
|
35
|
-
expect(result.isExact).toBe(true);
|
|
36
|
-
// Assert: Each child gets 33 or 34 pixels
|
|
37
|
-
const pixels = result.dimensions.map(d => d.pixels);
|
|
38
|
-
expect(pixels.every(p => p === 33 || p === 34)).toBe(true);
|
|
39
|
-
// Assert: Exactly one child gets the extra pixel
|
|
40
|
-
const count34 = pixels.filter(p => p === 34).length;
|
|
41
|
-
const count33 = pixels.filter(p => p === 33).length;
|
|
42
|
-
expect(count34).toBe(1);
|
|
43
|
-
expect(count33).toBe(2);
|
|
44
|
-
// Assert: Sum is exactly 100 (33 + 33 + 34 = 100)
|
|
45
|
-
expect(pixels.reduce((a, b) => a + b, 0)).toBe(100);
|
|
46
|
-
// Assert: Distribution is [34, 33, 33] (leftmost gets extra due to tie-break)
|
|
47
|
-
expect(pixels).toEqual([34, 33, 33]);
|
|
48
|
-
});
|
|
49
|
-
it('handles exact division (no remainder)', () => {
|
|
50
|
-
// Arrange: 100px / 4 = 25px exactly
|
|
51
|
-
const group = {
|
|
52
|
-
parentId: 1,
|
|
53
|
-
childIds: [10, 11, 12, 13],
|
|
54
|
-
axis: 'horizontal',
|
|
55
|
-
parentDimension: 100,
|
|
56
|
-
childDimensions: new Map([
|
|
57
|
-
[10, 25],
|
|
58
|
-
[11, 25],
|
|
59
|
-
[12, 25],
|
|
60
|
-
[13, 25],
|
|
61
|
-
]),
|
|
62
|
-
};
|
|
63
|
-
// Act
|
|
64
|
-
const result = distributeWithLargestRemainder(group);
|
|
65
|
-
// Assert
|
|
66
|
-
expect(result.totalPixels).toBe(100);
|
|
67
|
-
expect(result.isExact).toBe(true);
|
|
68
|
-
expect(result.dimensions.map(d => d.pixels)).toEqual([25, 25, 25, 25]);
|
|
69
|
-
});
|
|
70
|
-
it('distributes multiple extra pixels by remainder priority', () => {
|
|
71
|
-
// Arrange: 100px for [40.9, 30.8, 28.3]
|
|
72
|
-
// floors: [40, 30, 28] = 98
|
|
73
|
-
// remainders: [0.9, 0.8, 0.3]
|
|
74
|
-
// shortfall: 2px
|
|
75
|
-
// Extra pixels go to: 40.9 → 41, 30.8 → 31
|
|
76
|
-
const group = {
|
|
77
|
-
parentId: 1,
|
|
78
|
-
childIds: [10, 11, 12],
|
|
79
|
-
axis: 'horizontal',
|
|
80
|
-
parentDimension: 100,
|
|
81
|
-
childDimensions: new Map([
|
|
82
|
-
[10, 40.9],
|
|
83
|
-
[11, 30.8],
|
|
84
|
-
[12, 28.3],
|
|
85
|
-
]),
|
|
86
|
-
};
|
|
87
|
-
// Act
|
|
88
|
-
const result = distributeWithLargestRemainder(group);
|
|
89
|
-
// Assert
|
|
90
|
-
expect(result.totalPixels).toBe(100);
|
|
91
|
-
expect(result.isExact).toBe(true);
|
|
92
|
-
expect(result.dimensions.map(d => d.pixels)).toEqual([41, 31, 28]);
|
|
93
|
-
});
|
|
94
|
-
it('handles single child (trivial case)', () => {
|
|
95
|
-
const group = {
|
|
96
|
-
parentId: 1,
|
|
97
|
-
childIds: [10],
|
|
98
|
-
axis: 'horizontal',
|
|
99
|
-
parentDimension: 100,
|
|
100
|
-
childDimensions: new Map([[10, 100]]),
|
|
101
|
-
};
|
|
102
|
-
const result = distributeWithLargestRemainder(group);
|
|
103
|
-
expect(result.totalPixels).toBe(100);
|
|
104
|
-
expect(result.dimensions[0].pixels).toBe(100);
|
|
105
|
-
});
|
|
106
|
-
it('handles empty children', () => {
|
|
107
|
-
const group = {
|
|
108
|
-
parentId: 1,
|
|
109
|
-
childIds: [],
|
|
110
|
-
axis: 'horizontal',
|
|
111
|
-
parentDimension: 100,
|
|
112
|
-
childDimensions: new Map(),
|
|
113
|
-
};
|
|
114
|
-
const result = distributeWithLargestRemainder(group);
|
|
115
|
-
expect(result.totalPixels).toBe(0);
|
|
116
|
-
expect(result.dimensions).toEqual([]);
|
|
117
|
-
});
|
|
118
|
-
it('ties are broken by layout order (leftmost first)', () => {
|
|
119
|
-
// Arrange: 10px for [3.5, 3.5] - equal remainders
|
|
120
|
-
// floors: [3, 3] = 6
|
|
121
|
-
// remainders: [0.5, 0.5] - TIE!
|
|
122
|
-
// shortfall: 4px (wait, that's wrong)
|
|
123
|
-
// Actually: 10 - 6 = 4... no, 3.5 + 3.5 = 7, not 10
|
|
124
|
-
// Let me fix: 7px for [3.5, 3.5]
|
|
125
|
-
const group = {
|
|
126
|
-
parentId: 1,
|
|
127
|
-
childIds: [10, 11],
|
|
128
|
-
axis: 'horizontal',
|
|
129
|
-
parentDimension: 7,
|
|
130
|
-
childDimensions: new Map([
|
|
131
|
-
[10, 3.5],
|
|
132
|
-
[11, 3.5],
|
|
133
|
-
]),
|
|
134
|
-
};
|
|
135
|
-
// Act
|
|
136
|
-
const result = distributeWithLargestRemainder(group);
|
|
137
|
-
// Assert: shortfall is 1, so first element (leftmost) gets it
|
|
138
|
-
expect(result.totalPixels).toBe(7);
|
|
139
|
-
expect(result.dimensions.map(d => d.pixels)).toEqual([4, 3]);
|
|
140
|
-
});
|
|
141
|
-
it('handles vertical axis', () => {
|
|
142
|
-
const group = {
|
|
143
|
-
parentId: 1,
|
|
144
|
-
childIds: [10, 11, 12],
|
|
145
|
-
axis: 'vertical',
|
|
146
|
-
parentDimension: 100,
|
|
147
|
-
childDimensions: new Map([
|
|
148
|
-
[10, 100 / 3],
|
|
149
|
-
[11, 100 / 3],
|
|
150
|
-
[12, 100 / 3],
|
|
151
|
-
]),
|
|
152
|
-
};
|
|
153
|
-
const result = distributeWithLargestRemainder(group);
|
|
154
|
-
expect(result.totalPixels).toBe(100);
|
|
155
|
-
expect(result.isExact).toBe(true);
|
|
156
|
-
});
|
|
157
|
-
it('records error for each element', () => {
|
|
158
|
-
const group = {
|
|
159
|
-
parentId: 1,
|
|
160
|
-
childIds: [10, 11, 12],
|
|
161
|
-
axis: 'horizontal',
|
|
162
|
-
parentDimension: 100,
|
|
163
|
-
childDimensions: new Map([
|
|
164
|
-
[10, 100 / 3], // ~33.333
|
|
165
|
-
[11, 100 / 3],
|
|
166
|
-
[12, 100 / 3],
|
|
167
|
-
]),
|
|
168
|
-
};
|
|
169
|
-
const result = distributeWithLargestRemainder(group);
|
|
170
|
-
// First element: 34 - 33.333... = +0.666...
|
|
171
|
-
expect(result.dimensions[0].error).toBeCloseTo(0.6667, 3);
|
|
172
|
-
// Other elements: 33 - 33.333... = -0.333...
|
|
173
|
-
expect(result.dimensions[1].error).toBeCloseTo(-0.3333, 3);
|
|
174
|
-
expect(result.dimensions[2].error).toBeCloseTo(-0.3333, 3);
|
|
175
|
-
});
|
|
176
|
-
});
|
|
177
|
-
describe('applyErrorDistribution (Integration)', () => {
|
|
178
|
-
it('adjusts child bounds to fit parent exactly', () => {
|
|
179
|
-
// Arrange
|
|
180
|
-
const roundedBounds = new Map([
|
|
181
|
-
[1, { x: 0, y: 0, width: 100, height: 50 }], // Parent
|
|
182
|
-
[10, { x: 0, y: 0, width: 33, height: 50 }], // Child 1 (naive)
|
|
183
|
-
[11, { x: 33, y: 0, width: 33, height: 50 }], // Child 2
|
|
184
|
-
[12, { x: 66, y: 0, width: 33, height: 50 }], // Child 3
|
|
185
|
-
// Sum: 33 + 33 + 33 = 99 ← GAP!
|
|
186
|
-
]);
|
|
187
|
-
const containments = [
|
|
188
|
-
{
|
|
189
|
-
parentId: 1,
|
|
190
|
-
childIds: [10, 11, 12],
|
|
191
|
-
axis: 'horizontal',
|
|
192
|
-
},
|
|
193
|
-
];
|
|
194
|
-
// Act
|
|
195
|
-
const result = applyErrorDistribution(roundedBounds, containments);
|
|
196
|
-
// Assert: Children sum to parent width exactly
|
|
197
|
-
const child1 = result.get(10);
|
|
198
|
-
const child2 = result.get(11);
|
|
199
|
-
const child3 = result.get(12);
|
|
200
|
-
const totalWidth = child1.width + child2.width + child3.width;
|
|
201
|
-
expect(totalWidth).toBe(100);
|
|
202
|
-
// Assert: Children are contiguous (no gaps)
|
|
203
|
-
expect(child2.x).toBe(child1.x + child1.width);
|
|
204
|
-
expect(child3.x).toBe(child2.x + child2.width);
|
|
205
|
-
});
|
|
206
|
-
it('handles multiple containment constraints', () => {
|
|
207
|
-
const roundedBounds = new Map([
|
|
208
|
-
// Horizontal container
|
|
209
|
-
[1, { x: 0, y: 0, width: 100, height: 50 }],
|
|
210
|
-
[10, { x: 0, y: 0, width: 33, height: 50 }],
|
|
211
|
-
[11, { x: 33, y: 0, width: 33, height: 50 }],
|
|
212
|
-
[12, { x: 66, y: 0, width: 33, height: 50 }],
|
|
213
|
-
// Vertical container
|
|
214
|
-
[2, { x: 0, y: 50, width: 100, height: 100 }],
|
|
215
|
-
[20, { x: 0, y: 50, width: 100, height: 33 }],
|
|
216
|
-
[21, { x: 0, y: 83, width: 100, height: 33 }],
|
|
217
|
-
[22, { x: 0, y: 116, width: 100, height: 33 }],
|
|
218
|
-
]);
|
|
219
|
-
const containments = [
|
|
220
|
-
{ parentId: 1, childIds: [10, 11, 12], axis: 'horizontal' },
|
|
221
|
-
{ parentId: 2, childIds: [20, 21, 22], axis: 'vertical' },
|
|
222
|
-
];
|
|
223
|
-
const result = applyErrorDistribution(roundedBounds, containments);
|
|
224
|
-
// Horizontal container
|
|
225
|
-
const hTotal = result.get(10).width + result.get(11).width + result.get(12).width;
|
|
226
|
-
expect(hTotal).toBe(100);
|
|
227
|
-
// Vertical container
|
|
228
|
-
const vTotal = result.get(20).height + result.get(21).height + result.get(22).height;
|
|
229
|
-
expect(vTotal).toBe(100);
|
|
230
|
-
});
|
|
231
|
-
});
|
|
232
|
-
describe('Edge Cases', () => {
|
|
233
|
-
it('handles very small fractional differences', () => {
|
|
234
|
-
// 100px / 7 = 14.285714...
|
|
235
|
-
const group = {
|
|
236
|
-
parentId: 1,
|
|
237
|
-
childIds: [1, 2, 3, 4, 5, 6, 7],
|
|
238
|
-
axis: 'horizontal',
|
|
239
|
-
parentDimension: 100,
|
|
240
|
-
childDimensions: new Map([
|
|
241
|
-
[1, 100 / 7],
|
|
242
|
-
[2, 100 / 7],
|
|
243
|
-
[3, 100 / 7],
|
|
244
|
-
[4, 100 / 7],
|
|
245
|
-
[5, 100 / 7],
|
|
246
|
-
[6, 100 / 7],
|
|
247
|
-
[7, 100 / 7],
|
|
248
|
-
]),
|
|
249
|
-
};
|
|
250
|
-
const result = distributeWithLargestRemainder(group);
|
|
251
|
-
expect(result.totalPixels).toBe(100);
|
|
252
|
-
expect(result.isExact).toBe(true);
|
|
253
|
-
// 7 * 14 = 98, shortfall = 2
|
|
254
|
-
// Two elements get 15px, five get 14px
|
|
255
|
-
const fifteens = result.dimensions.filter(d => d.pixels === 15).length;
|
|
256
|
-
const fourteens = result.dimensions.filter(d => d.pixels === 14).length;
|
|
257
|
-
expect(fifteens).toBe(2);
|
|
258
|
-
expect(fourteens).toBe(5);
|
|
259
|
-
});
|
|
260
|
-
it('handles zero-width children', () => {
|
|
261
|
-
const group = {
|
|
262
|
-
parentId: 1,
|
|
263
|
-
childIds: [10, 11],
|
|
264
|
-
axis: 'horizontal',
|
|
265
|
-
parentDimension: 100,
|
|
266
|
-
childDimensions: new Map([
|
|
267
|
-
[10, 100],
|
|
268
|
-
[11, 0],
|
|
269
|
-
]),
|
|
270
|
-
};
|
|
271
|
-
const result = distributeWithLargestRemainder(group);
|
|
272
|
-
expect(result.totalPixels).toBe(100);
|
|
273
|
-
expect(result.dimensions[0].pixels).toBe(100);
|
|
274
|
-
expect(result.dimensions[1].pixels).toBe(0);
|
|
275
|
-
});
|
|
276
|
-
it('uses proportional scaling when children exceed parent', () => {
|
|
277
|
-
// Children sum to 150, but parent is only 100
|
|
278
|
-
const group = {
|
|
279
|
-
parentId: 1,
|
|
280
|
-
childIds: [10, 11, 12],
|
|
281
|
-
axis: 'horizontal',
|
|
282
|
-
parentDimension: 100,
|
|
283
|
-
childDimensions: new Map([
|
|
284
|
-
[10, 50],
|
|
285
|
-
[11, 50],
|
|
286
|
-
[12, 50],
|
|
287
|
-
]),
|
|
288
|
-
};
|
|
289
|
-
const result = distributeWithLargestRemainder(group);
|
|
290
|
-
// Falls back to proportional: each gets ~33.33
|
|
291
|
-
expect(result.totalPixels).toBe(100);
|
|
292
|
-
expect(result.method).toBe('first-fit');
|
|
293
|
-
});
|
|
294
|
-
});
|
|
295
|
-
describe('Proof: No Gaps or Overflow', () => {
|
|
296
|
-
/**
|
|
297
|
-
* Property test: For ANY valid input, sum(children) === parent
|
|
298
|
-
*/
|
|
299
|
-
it('maintains invariant for random inputs', () => {
|
|
300
|
-
const testCases = [
|
|
301
|
-
{ parent: 100, children: [100 / 3, 100 / 3, 100 / 3] },
|
|
302
|
-
{ parent: 1000, children: [1000 / 7, 1000 / 7, 1000 / 7, 1000 / 7, 1000 / 7, 1000 / 7, 1000 / 7] },
|
|
303
|
-
{ parent: 50, children: [12.5, 12.5, 12.5, 12.5] },
|
|
304
|
-
{ parent: 99, children: [33, 33, 33] },
|
|
305
|
-
{ parent: 101, children: [50.5, 50.5] },
|
|
306
|
-
{ parent: 1, children: [0.5, 0.5] },
|
|
307
|
-
{ parent: 2, children: [0.6, 0.7, 0.7] },
|
|
308
|
-
];
|
|
309
|
-
for (const tc of testCases) {
|
|
310
|
-
const group = {
|
|
311
|
-
parentId: 1,
|
|
312
|
-
childIds: tc.children.map((_, i) => i + 10),
|
|
313
|
-
axis: 'horizontal',
|
|
314
|
-
parentDimension: tc.parent,
|
|
315
|
-
childDimensions: new Map(tc.children.map((c, i) => [i + 10, c])),
|
|
316
|
-
};
|
|
317
|
-
const result = distributeWithLargestRemainder(group);
|
|
318
|
-
expect(result.totalPixels).toBe(tc.parent);
|
|
319
|
-
expect(result.isExact).toBe(true);
|
|
320
|
-
}
|
|
321
|
-
});
|
|
322
|
-
});
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for Q-Dimension Event Backpressure Control
|
|
3
|
-
*
|
|
4
|
-
* Validates:
|
|
5
|
-
* 1. Event coalescing (latest-only sampling)
|
|
6
|
-
* 2. Priority queue handling (critical events)
|
|
7
|
-
* 3. Async event isolation and merging
|
|
8
|
-
* 4. P/Q boundary enforcement (T-state keys only)
|
|
9
|
-
*/
|
|
10
|
-
export {};
|