@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,352 @@
1
+ /**
2
+ * Gradient Shader Mapper: P-Dimension to GPU Shaders
3
+ *
4
+ * This module maps P-dimension gradient entities to GPU shader objects.
5
+ * It handles the critical transition from exact rational arithmetic to GPU-compatible
6
+ * floating-point representation while preserving visual fidelity.
7
+ *
8
+ * ## Architecture
9
+ *
10
+ * ```
11
+ * P-Dimension (Exact) GPU (Float)
12
+ * ─────────────────────────────────────────────────────────────
13
+ *
14
+ * LinearGradient { GpuShaderBackend.Shader
15
+ * start: Rational(1, 3) ────────────▶ MakeLinearGradient(
16
+ * end: Rational(2, 3) [0.333..., 0.666...],
17
+ * stops: [ colors: Float32Array,
18
+ * ColorStop(r=255, ...) positions: Float32Array
19
+ * ] )
20
+ * }
21
+ *
22
+ * ┌─────────────────────────────────────────────────────────────┐
23
+ * │ CRITICAL: Topology-preserving rounding at this boundary │
24
+ * │ │
25
+ * │ - Color channels [0, 255] → [0.0, 1.0] with clamping │
26
+ * │ - Position values [0, 1] stay exact, no interpolation │
27
+ * │ - Control points use same rounding as other coordinates │
28
+ * └─────────────────────────────────────────────────────────────┘
29
+ * ```
30
+ *
31
+ * ## Usage
32
+ *
33
+ * ```typescript
34
+ * const shader = mapLinearGradientToShader(ck, gradient, bounds, dpr);
35
+ * paint.setShader(shader);
36
+ * canvas.drawRect(bounds, paint);
37
+ * ```
38
+ */
39
+ // =============================================================================
40
+ // Core Mapping Functions
41
+ // =============================================================================
42
+ /**
43
+ * Map a P-dimension linear gradient to a GPU shader.
44
+ *
45
+ * @param ck - GPU shader backend instance
46
+ * @param gradient - P-dimension linear gradient definition
47
+ * @param bounds - Rasterized bounds for coordinate transformation
48
+ * @param devicePixelRatio - Device pixel ratio for coordinate scaling
49
+ * @returns GPU shader instance (caller must call delete() when done)
50
+ */
51
+ export function mapLinearGradientToShader(ck, gradient, bounds, devicePixelRatio) {
52
+ // Convert control points to device coordinates
53
+ const startX = rationalToFloat(gradient.start.x) * devicePixelRatio;
54
+ const startY = rationalToFloat(gradient.start.y) * devicePixelRatio;
55
+ const endX = rationalToFloat(gradient.end.x) * devicePixelRatio;
56
+ const endY = rationalToFloat(gradient.end.y) * devicePixelRatio;
57
+ // Sort stops by position (required by GPU shader backend)
58
+ const sortedStops = [...gradient.stops].sort((a, b) => rationalToFloat(a.position) - rationalToFloat(b.position));
59
+ // Convert colors to Float32Array (RGBA format, each channel 0-1)
60
+ const colors = colorStopsToFloat32Array(sortedStops);
61
+ const positions = positionsToFloat32Array(sortedStops);
62
+ // Map tile mode
63
+ const tileMode = mapTileMode(ck, gradient.tileMode);
64
+ return ck.Shader.MakeLinearGradient(new Float32Array([startX, startY]), new Float32Array([endX, endY]), colors, positions, tileMode);
65
+ }
66
+ /**
67
+ * Map a P-dimension radial gradient to a GPU shader.
68
+ *
69
+ * The GPU backend uses two-point conical gradients which can express:
70
+ * - Circle gradients (same center, different radii)
71
+ * - Focal gradients (different centers)
72
+ *
73
+ * @param ck - GPU shader backend instance
74
+ * @param gradient - P-dimension radial gradient definition
75
+ * @param bounds - Rasterized bounds for coordinate transformation
76
+ * @param devicePixelRatio - Device pixel ratio for coordinate scaling
77
+ * @returns GPU shader instance
78
+ */
79
+ export function mapRadialGradientToShader(ck, gradient, bounds, devicePixelRatio) {
80
+ // Convert center to device coordinates
81
+ const centerX = rationalToFloat(gradient.center.x) * devicePixelRatio;
82
+ const centerY = rationalToFloat(gradient.center.y) * devicePixelRatio;
83
+ // For elliptical gradients, we use the larger radius and scale
84
+ // For now, use average of radiusX and radiusY as the radius
85
+ const radiusX = rationalToFloat(gradient.radiusX) * devicePixelRatio;
86
+ const radiusY = rationalToFloat(gradient.radiusY) * devicePixelRatio;
87
+ const radius = Math.max(radiusX, radiusY);
88
+ // Handle focal point (if specified)
89
+ let focalX = centerX;
90
+ let focalY = centerY;
91
+ let focalR = 0;
92
+ if (gradient.focalPoint) {
93
+ focalX = rationalToFloat(gradient.focalPoint.x) * devicePixelRatio;
94
+ focalY = rationalToFloat(gradient.focalPoint.y) * devicePixelRatio;
95
+ }
96
+ if (gradient.focalRadius) {
97
+ focalR = rationalToFloat(gradient.focalRadius) * devicePixelRatio;
98
+ }
99
+ // Sort and convert stops
100
+ const sortedStops = [...gradient.stops].sort((a, b) => rationalToFloat(a.position) - rationalToFloat(b.position));
101
+ const colors = colorStopsToFloat32Array(sortedStops);
102
+ const positions = positionsToFloat32Array(sortedStops);
103
+ const tileMode = mapTileMode(ck, gradient.tileMode);
104
+ // Two-point conical: inner circle to outer circle
105
+ return ck.Shader.MakeTwoPointConicalGradient(new Float32Array([focalX, focalY]), focalR, new Float32Array([centerX, centerY]), radius, colors, positions, tileMode);
106
+ }
107
+ /**
108
+ * Map a P-dimension conic (sweep) gradient to a GPU shader.
109
+ *
110
+ * @param ck - GPU shader backend instance
111
+ * @param gradient - P-dimension conic gradient definition
112
+ * @param bounds - Rasterized bounds for coordinate transformation
113
+ * @param devicePixelRatio - Device pixel ratio for coordinate scaling
114
+ * @returns GPU shader instance
115
+ */
116
+ export function mapConicGradientToShader(ck, gradient, bounds, devicePixelRatio) {
117
+ // Convert center to device coordinates
118
+ const centerX = rationalToFloat(gradient.center.x) * devicePixelRatio;
119
+ const centerY = rationalToFloat(gradient.center.y) * devicePixelRatio;
120
+ // Convert angles (GPU shader expects degrees)
121
+ const startAngle = rationalToFloat(gradient.startAngle) + rationalToFloat(gradient.rotation);
122
+ const endAngle = rationalToFloat(gradient.endAngle) + rationalToFloat(gradient.rotation);
123
+ // Sort and convert stops
124
+ const sortedStops = [...gradient.stops].sort((a, b) => rationalToFloat(a.position) - rationalToFloat(b.position));
125
+ const colors = colorStopsToFloat32Array(sortedStops);
126
+ const positions = positionsToFloat32Array(sortedStops);
127
+ // Sweep gradient always uses Clamp-like behavior
128
+ return ck.Shader.MakeSweepGradient(centerX, centerY, colors, positions, ck.TileMode.Clamp, startAngle, endAngle);
129
+ }
130
+ // =============================================================================
131
+ // Conversion Utilities
132
+ // =============================================================================
133
+ /**
134
+ * Convert a rational number to a floating-point number.
135
+ *
136
+ * This is the critical boundary where exact arithmetic meets GPU floats.
137
+ * The conversion is straightforward division, but precision loss is
138
+ * unavoidable and acceptable at this layer.
139
+ */
140
+ export function rationalToFloat(r) {
141
+ // Handle bigint to number conversion
142
+ // For very large rationals, this may lose precision
143
+ return Number(r.numerator) / Number(r.denominator);
144
+ }
145
+ /**
146
+ * Convert P-dimension color stops to a Float32Array of RGBA values.
147
+ *
148
+ * GPU shader expects colors in RGBA order, with each channel in [0, 1].
149
+ * P-dimension stores RGB in [0, 255] and Alpha in [0, 1].
150
+ *
151
+ * ## Topology-Preserving Rounding (Clamping)
152
+ *
153
+ * Color values are clamped to [0, 1] to ensure GPU-valid input.
154
+ * This preserves the topological ordering of colors even if the
155
+ * original rational values were slightly out of range.
156
+ */
157
+ export function colorStopsToFloat32Array(stops) {
158
+ const array = new Float32Array(stops.length * 4);
159
+ for (let i = 0; i < stops.length; i++) {
160
+ const stop = stops[i];
161
+ const offset = i * 4;
162
+ // Convert [0, 255] rational to [0, 1] float with clamping
163
+ array[offset + 0] = clamp01(rationalToFloat(stop.r) / 255);
164
+ array[offset + 1] = clamp01(rationalToFloat(stop.g) / 255);
165
+ array[offset + 2] = clamp01(rationalToFloat(stop.b) / 255);
166
+ // Alpha is already in [0, 1] in P-dimension
167
+ array[offset + 3] = clamp01(rationalToFloat(stop.a));
168
+ }
169
+ return array;
170
+ }
171
+ /**
172
+ * Convert P-dimension color stop positions to a Float32Array.
173
+ *
174
+ * Positions are kept as-is (already in [0, 1] in P-dimension),
175
+ * with clamping for safety.
176
+ */
177
+ export function positionsToFloat32Array(stops) {
178
+ const array = new Float32Array(stops.length);
179
+ for (let i = 0; i < stops.length; i++) {
180
+ array[i] = clamp01(rationalToFloat(stops[i].position));
181
+ }
182
+ return array;
183
+ }
184
+ /**
185
+ * Clamp a value to [0, 1] range.
186
+ *
187
+ * This is the "topology-preserving rounding" for color values:
188
+ * it ensures the value is valid for GPU while preserving ordering.
189
+ */
190
+ function clamp01(value) {
191
+ return Math.max(0, Math.min(1, value));
192
+ }
193
+ /**
194
+ * Map P-dimension tile mode to GPU tile mode constant.
195
+ */
196
+ function mapTileMode(ck, mode) {
197
+ switch (mode) {
198
+ case 'clamp':
199
+ return ck.TileMode.Clamp;
200
+ case 'repeat':
201
+ return ck.TileMode.Repeat;
202
+ case 'mirror':
203
+ return ck.TileMode.Mirror;
204
+ case 'decal':
205
+ return ck.TileMode.Decal;
206
+ default:
207
+ return ck.TileMode.Clamp;
208
+ }
209
+ }
210
+ // =============================================================================
211
+ // Factory Function for Gradient FillStyle
212
+ // =============================================================================
213
+ /**
214
+ * Create a GPU shader from a FillStyle gradient definition.
215
+ *
216
+ * This is a higher-level factory that integrates with the existing
217
+ * FillStyle type from the AST.
218
+ */
219
+ export function createGradientShader(ck, fillType, stops, bounds, devicePixelRatio) {
220
+ // Convert simplified FillStyle stops to PColorStop format
221
+ const pStops = stops.map((stop, index) => {
222
+ const rgba = parseColorString(stop.color);
223
+ return {
224
+ id: index,
225
+ r: { numerator: BigInt(rgba.r), denominator: 1n },
226
+ g: { numerator: BigInt(rgba.g), denominator: 1n },
227
+ b: { numerator: BigInt(rgba.b), denominator: 1n },
228
+ a: { numerator: BigInt(Math.round(rgba.a * 1000)), denominator: 1000n },
229
+ position: stop.offset,
230
+ };
231
+ });
232
+ if (fillType === 'linear-gradient') {
233
+ // Default linear gradient: top to bottom
234
+ const linearGradient = {
235
+ id: 0,
236
+ start: {
237
+ id: 0,
238
+ x: { numerator: BigInt(Math.round(bounds.x)), denominator: 1n },
239
+ y: { numerator: BigInt(Math.round(bounds.y)), denominator: 1n },
240
+ },
241
+ end: {
242
+ id: 0,
243
+ x: { numerator: BigInt(Math.round(bounds.x)), denominator: 1n },
244
+ y: { numerator: BigInt(Math.round(bounds.y + bounds.height)), denominator: 1n },
245
+ },
246
+ stops: pStops,
247
+ tileMode: 'clamp',
248
+ };
249
+ return mapLinearGradientToShader(ck, linearGradient, bounds, devicePixelRatio);
250
+ }
251
+ else if (fillType === 'radial-gradient') {
252
+ // Default radial gradient: center of bounds, radius to edge
253
+ const cx = bounds.x + bounds.width / 2;
254
+ const cy = bounds.y + bounds.height / 2;
255
+ const radius = Math.max(bounds.width, bounds.height) / 2;
256
+ const radialGradient = {
257
+ id: 0,
258
+ center: {
259
+ id: 0,
260
+ x: { numerator: BigInt(Math.round(cx * 1000)), denominator: 1000n },
261
+ y: { numerator: BigInt(Math.round(cy * 1000)), denominator: 1000n },
262
+ },
263
+ radiusX: { numerator: BigInt(Math.round(radius * 1000)), denominator: 1000n },
264
+ radiusY: { numerator: BigInt(Math.round(radius * 1000)), denominator: 1000n },
265
+ stops: pStops,
266
+ tileMode: 'clamp',
267
+ };
268
+ return mapRadialGradientToShader(ck, radialGradient, bounds, devicePixelRatio);
269
+ }
270
+ return null;
271
+ }
272
+ /**
273
+ * Parse a CSS color string to RGBA values.
274
+ *
275
+ * Supports:
276
+ * - Hex: #RGB, #RGBA, #RRGGBB, #RRGGBBAA
277
+ * - Named colors (basic set)
278
+ */
279
+ function parseColorString(color) {
280
+ // Named colors (basic set)
281
+ const namedColors = {
282
+ black: { r: 0, g: 0, b: 0 },
283
+ white: { r: 255, g: 255, b: 255 },
284
+ red: { r: 255, g: 0, b: 0 },
285
+ green: { r: 0, g: 128, b: 0 },
286
+ blue: { r: 0, g: 0, b: 255 },
287
+ yellow: { r: 255, g: 255, b: 0 },
288
+ cyan: { r: 0, g: 255, b: 255 },
289
+ magenta: { r: 255, g: 0, b: 255 },
290
+ transparent: { r: 0, g: 0, b: 0 },
291
+ };
292
+ const lower = color.toLowerCase().trim();
293
+ if (lower === 'transparent') {
294
+ return { r: 0, g: 0, b: 0, a: 0 };
295
+ }
296
+ if (namedColors[lower]) {
297
+ return { ...namedColors[lower], a: 1 };
298
+ }
299
+ // Hex parsing
300
+ if (lower.startsWith('#')) {
301
+ const hex = lower.slice(1);
302
+ if (hex.length === 3) {
303
+ // #RGB
304
+ return {
305
+ r: parseInt(hex[0] + hex[0], 16),
306
+ g: parseInt(hex[1] + hex[1], 16),
307
+ b: parseInt(hex[2] + hex[2], 16),
308
+ a: 1,
309
+ };
310
+ }
311
+ else if (hex.length === 4) {
312
+ // #RGBA
313
+ return {
314
+ r: parseInt(hex[0] + hex[0], 16),
315
+ g: parseInt(hex[1] + hex[1], 16),
316
+ b: parseInt(hex[2] + hex[2], 16),
317
+ a: parseInt(hex[3] + hex[3], 16) / 255,
318
+ };
319
+ }
320
+ else if (hex.length === 6) {
321
+ // #RRGGBB
322
+ return {
323
+ r: parseInt(hex.slice(0, 2), 16),
324
+ g: parseInt(hex.slice(2, 4), 16),
325
+ b: parseInt(hex.slice(4, 6), 16),
326
+ a: 1,
327
+ };
328
+ }
329
+ else if (hex.length === 8) {
330
+ // #RRGGBBAA
331
+ return {
332
+ r: parseInt(hex.slice(0, 2), 16),
333
+ g: parseInt(hex.slice(2, 4), 16),
334
+ b: parseInt(hex.slice(4, 6), 16),
335
+ a: parseInt(hex.slice(6, 8), 16) / 255,
336
+ };
337
+ }
338
+ }
339
+ // Fallback: black
340
+ return { r: 0, g: 0, b: 0, a: 1 };
341
+ }
342
+ // =============================================================================
343
+ // Exports for Testing
344
+ // =============================================================================
345
+ export const _internals = {
346
+ rationalToFloat,
347
+ clamp01,
348
+ colorStopsToFloat32Array,
349
+ positionsToFloat32Array,
350
+ mapTileMode,
351
+ parseColorString,
352
+ };
@@ -0,0 +1,151 @@
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
+ import type { EntityId, Rational, RasterBounds, PVectorBounds } from '../ast/types';
65
+ /**
66
+ * A coordinate in the pre-rasterization space.
67
+ */
68
+ interface RationalCoord {
69
+ entityId: EntityId;
70
+ edge: 'left' | 'right' | 'top' | 'bottom';
71
+ value: Rational;
72
+ }
73
+ /**
74
+ * Topological constraint between coordinates.
75
+ */
76
+ type TopoConstraint = {
77
+ type: 'equal';
78
+ a: CoordRef;
79
+ b: CoordRef;
80
+ } | {
81
+ type: 'less-than';
82
+ a: CoordRef;
83
+ b: CoordRef;
84
+ } | {
85
+ type: 'adjacent';
86
+ a: CoordRef;
87
+ b: CoordRef;
88
+ };
89
+ interface CoordRef {
90
+ entityId: EntityId;
91
+ edge: 'left' | 'right' | 'top' | 'bottom';
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
+ /** Any topology violations detected (should be empty if algorithm is correct) */
100
+ violations: TopologyViolation[];
101
+ /** Statistics about the rounding process */
102
+ stats: RoundingStats;
103
+ }
104
+ interface TopologyViolation {
105
+ constraint: TopoConstraint;
106
+ message: string;
107
+ }
108
+ interface RoundingStats {
109
+ totalCoordinates: number;
110
+ equivalenceClasses: number;
111
+ constraintsPropagated: number;
112
+ conflictsResolved: number;
113
+ }
114
+ /**
115
+ * Topology-preserving rounding entry point.
116
+ */
117
+ export declare function roundWithTopologyPreservation(entities: Map<EntityId, PVectorBounds>, constraints: TopoConstraint[], devicePixelRatio: number): RoundingResult;
118
+ declare function extractCoordinates(entities: Map<EntityId, PVectorBounds>): RationalCoord[];
119
+ /**
120
+ * Build equivalence classes from coordinates and equality constraints.
121
+ *
122
+ * Two coordinates are in the same class if:
123
+ * 1. They have the same rational value, OR
124
+ * 2. They are connected by an 'equal' or 'adjacent' constraint
125
+ */
126
+ declare function buildEquivalenceClasses(coords: RationalCoord[], constraints: TopoConstraint[]): Map<string, RationalCoord[]>;
127
+ declare function rationalToFloat(r: Rational): number;
128
+ interface PropagationResult {
129
+ adjusted: Map<string, number>;
130
+ conflictsResolved: number;
131
+ }
132
+ /**
133
+ * Propagate rounding decisions through less-than constraints.
134
+ *
135
+ * If A < B in rational space, we must ensure round(A) < round(B) in pixel space.
136
+ * If rounding would violate this, we adjust by:
137
+ * 1. Decreasing A by 1, OR
138
+ * 2. Increasing B by 1
139
+ *
140
+ * We choose the option that minimizes total visual shift.
141
+ */
142
+ declare function propagateConstraints(initial: Map<string, number>, equivClasses: Map<string, RationalCoord[]>, constraints: TopoConstraint[]): PropagationResult;
143
+ declare function verifyTopology(bounds: Map<EntityId, RasterBounds>, constraints: TopoConstraint[]): TopologyViolation[];
144
+ export declare const _internals: {
145
+ extractCoordinates: typeof extractCoordinates;
146
+ buildEquivalenceClasses: typeof buildEquivalenceClasses;
147
+ propagateConstraints: typeof propagateConstraints;
148
+ verifyTopology: typeof verifyTopology;
149
+ rationalToFloat: typeof rationalToFloat;
150
+ };
151
+ export {};