@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,322 @@
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
+ });
@@ -0,0 +1,280 @@
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
+ import type { EntityId, Rational, PathCommand } from '../ast/types';
32
+ /**
33
+ * Control point with resolved rational coordinates.
34
+ */
35
+ export interface ResolvedControlPoint {
36
+ id: EntityId;
37
+ x: Rational;
38
+ y: Rational;
39
+ role: 'anchor' | 'handle';
40
+ }
41
+ /**
42
+ * Path segment referencing control points by EntityId.
43
+ */
44
+ export type PathSegmentRef = {
45
+ type: 'moveTo';
46
+ point: EntityId;
47
+ } | {
48
+ type: 'lineTo';
49
+ point: EntityId;
50
+ } | {
51
+ type: 'quadTo';
52
+ control: EntityId;
53
+ point: EntityId;
54
+ } | {
55
+ type: 'cubicTo';
56
+ control1: EntityId;
57
+ control2: EntityId;
58
+ point: EntityId;
59
+ } | {
60
+ type: 'arcTo';
61
+ point: EntityId;
62
+ radiusX: Rational;
63
+ radiusY: Rational;
64
+ xRotation: Rational;
65
+ largeArc: boolean;
66
+ sweep: boolean;
67
+ } | {
68
+ type: 'close';
69
+ };
70
+ /**
71
+ * Path definition with EntityId references.
72
+ */
73
+ export interface PathDefinition {
74
+ id: EntityId;
75
+ segments: PathSegmentRef[];
76
+ fillRule: 'nonzero' | 'evenodd';
77
+ closed: boolean;
78
+ }
79
+ /**
80
+ * Rasterized path ready for GPU renderer consumption.
81
+ */
82
+ export interface RasterizedPath {
83
+ /** Unique path ID */
84
+ id: EntityId;
85
+ /** SVG-style path commands with float coordinates */
86
+ commands: PathCommand[];
87
+ /** Fill rule for winding calculation */
88
+ fillRule: 'nonzero' | 'evenodd';
89
+ /** Whether path is closed */
90
+ closed: boolean;
91
+ /** Computed bounding box (for culling) */
92
+ bounds: {
93
+ minX: number;
94
+ minY: number;
95
+ maxX: number;
96
+ maxY: number;
97
+ };
98
+ }
99
+ /**
100
+ * Maps P-dimension path definitions to rasterized PathEntity objects.
101
+ *
102
+ * @param paths - Path definitions with EntityId references
103
+ * @param controlPoints - Map of resolved control point positions
104
+ * @param devicePixelRatio - DPR for coordinate scaling
105
+ * @returns Rasterized paths ready for GPU renderer
106
+ */
107
+ export declare function mapPathsToCanvas(paths: PathDefinition[], controlPoints: Map<EntityId, ResolvedControlPoint>, devicePixelRatio?: number): RasterizedPath[];
108
+ /**
109
+ * Map a single path definition to rasterized commands.
110
+ */
111
+ declare function mapSinglePath(path: PathDefinition, controlPoints: Map<EntityId, ResolvedControlPoint>, dpr: number): RasterizedPath;
112
+ /**
113
+ * Interface matching the GPU path builder type (subset).
114
+ */
115
+ export interface PathBuilderLike {
116
+ moveTo(x: number, y: number): void;
117
+ lineTo(x: number, y: number): void;
118
+ quadTo(cpx: number, cpy: number, x: number, y: number): void;
119
+ cubicTo(cp1x: number, cp1y: number, cp2x: number, cp2y: number, x: number, y: number): void;
120
+ arcToOval(oval: Float32Array, startAngle: number, sweepAngle: number, forceMoveTo: boolean): void;
121
+ arcToRotated(rx: number, ry: number, xAxisRotate: number, useSmallArc: boolean, isCCW: boolean, x: number, y: number): void;
122
+ close(): void;
123
+ setFillType(fillType: number): void;
124
+ }
125
+ /**
126
+ * Fill type constants.
127
+ */
128
+ export declare const FillType: {
129
+ Winding: number;
130
+ EvenOdd: number;
131
+ };
132
+ /**
133
+ * Build a PathEntity from rasterized path data.
134
+ *
135
+ * @param path - Rasterized path with float coordinates
136
+ * @param pathBuilder - Path builder object to populate
137
+ */
138
+ export declare function buildPathEntity(path: RasterizedPath, pathBuilder: PathBuilderLike): void;
139
+ /**
140
+ * Ensures that paths sharing control points produce bit-identical coordinates.
141
+ *
142
+ * This is critical for seamless curve connections: if two Bezier curves share
143
+ * an endpoint, the rasterized coordinates must be exactly the same to prevent
144
+ * visual gaps or overlaps.
145
+ *
146
+ * ## Algorithm
147
+ *
148
+ * 1. Identify all paths that share control points
149
+ * 2. For shared points, use a single coordinate resolution
150
+ * 3. Both paths reference the same float value
151
+ *
152
+ * @param paths - Paths that may share control points
153
+ * @param controlPoints - Control point definitions
154
+ * @returns Normalized control point map with consistent coordinates
155
+ */
156
+ export declare function normalizeSharedControlPoints(paths: PathDefinition[], controlPoints: Map<EntityId, ResolvedControlPoint>): Map<EntityId, ResolvedControlPoint>;
157
+ declare function getSegmentPointIds(segment: PathSegmentRef): EntityId[];
158
+ /**
159
+ * Validate that all control point references in paths are resolvable.
160
+ */
161
+ export declare function validatePathReferences(paths: PathDefinition[], controlPoints: Map<EntityId, ResolvedControlPoint>): {
162
+ valid: boolean;
163
+ errors: string[];
164
+ };
165
+ /**
166
+ * A resolved Radius entity (scalar value).
167
+ */
168
+ export interface ResolvedRadius {
169
+ id: EntityId;
170
+ value: Rational;
171
+ }
172
+ /**
173
+ * A resolved Arc entity with center, radius, and angles.
174
+ */
175
+ export interface ResolvedArc {
176
+ id: EntityId;
177
+ /** Center control point (already resolved). */
178
+ center: ResolvedControlPoint;
179
+ /** Radius value (already resolved). */
180
+ radius: ResolvedRadius;
181
+ /** Start angle in degrees. */
182
+ startAngle: Rational;
183
+ /** End angle in degrees. */
184
+ endAngle: Rational;
185
+ /** Direction: true = clockwise. */
186
+ clockwise: boolean;
187
+ }
188
+ /**
189
+ * A resolved RoundedRect with corner radii.
190
+ */
191
+ export interface ResolvedRoundedRect {
192
+ id: EntityId;
193
+ /** Bounds (x, y, width, height). */
194
+ bounds: {
195
+ x: Rational;
196
+ y: Rational;
197
+ width: Rational;
198
+ height: Rational;
199
+ };
200
+ /** Corner radii (all resolved). */
201
+ radii: {
202
+ topLeft: ResolvedRadius;
203
+ topRight: ResolvedRadius;
204
+ bottomRight: ResolvedRadius;
205
+ bottomLeft: ResolvedRadius;
206
+ };
207
+ }
208
+ /**
209
+ * Interface matching the GPU canvas drawing surface methods.
210
+ */
211
+ export interface DrawSurfaceLike {
212
+ drawArc(oval: {
213
+ x: number;
214
+ y: number;
215
+ width: number;
216
+ height: number;
217
+ }, startAngle: number, sweepAngle: number, useCenter: boolean, paint: unknown): void;
218
+ drawRoundRect(rect: {
219
+ x: number;
220
+ y: number;
221
+ width: number;
222
+ height: number;
223
+ }, rx: number, ry: number, paint: unknown): void;
224
+ drawRRect(rrect: unknown, paint: unknown): void;
225
+ }
226
+ /**
227
+ * Convert rational to float at RASTERIZATION BOUNDARY ONLY.
228
+ */
229
+ declare function toFloat(r: Rational): number;
230
+ /**
231
+ * Draw an arc to a GPU draw surface.
232
+ *
233
+ * ## Deferred Evaluation (Phase 7)
234
+ *
235
+ * The arc's circumference points are NOT computed in P-dimension.
236
+ * Only the center, radius, and angles are constrained linearly.
237
+ * The actual arc rendering is delegated to the GPU renderer which evaluates
238
+ * the parametric curve (cos/sin) in its native floating-point space.
239
+ *
240
+ * @param canvas - GPU draw surface
241
+ * @param arc - Resolved arc with rational center/radius/angles
242
+ * @param paint - Paint style for the arc
243
+ */
244
+ export declare function drawArc(canvas: DrawSurfaceLike, arc: ResolvedArc, paint: unknown): void;
245
+ /**
246
+ * Draw a rounded rectangle to a GPU draw surface.
247
+ *
248
+ * ## Deferred Evaluation (Phase 7)
249
+ *
250
+ * Corner curves are NOT evaluated in P-dimension. Only the bounds and
251
+ * radius scalars are constrained. The GPU renderer evaluates the corner arcs
252
+ * internally using native floating-point math.
253
+ *
254
+ * @param canvas - GPU draw surface
255
+ * @param rect - Resolved rounded rect with rational bounds/radii
256
+ * @param paint - Paint style
257
+ */
258
+ export declare function drawRoundedRect(canvas: DrawSurfaceLike, rect: ResolvedRoundedRect, paint: unknown): void;
259
+ /**
260
+ * Draw a full circle (special case of arc: 0° to 360°).
261
+ *
262
+ * @param canvas - GPU draw surface
263
+ * @param center - Resolved center control point
264
+ * @param radius - Resolved radius entity
265
+ * @param paint - Paint style
266
+ */
267
+ export declare function drawCircle(canvas: DrawSurfaceLike, center: ResolvedControlPoint, radius: ResolvedRadius, paint: unknown): void;
268
+ /**
269
+ * Validate arc/radius entity references.
270
+ */
271
+ export declare function validateArcReferences(arcs: ResolvedArc[], centers: Map<EntityId, ResolvedControlPoint>, radii: Map<EntityId, ResolvedRadius>): {
272
+ valid: boolean;
273
+ errors: string[];
274
+ };
275
+ export declare const _internals: {
276
+ mapSinglePath: typeof mapSinglePath;
277
+ toFloat: typeof toFloat;
278
+ getSegmentPointIds: typeof getSegmentPointIds;
279
+ };
280
+ export {};