@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,361 @@
1
+ /**
2
+ * Chunk Splitting Strategy for Progressive Loading
3
+ *
4
+ * This module implements static analysis to partition the constraint graph
5
+ * into Initial and Lazy chunks, enabling Qwik/Wiz-style progressive hydration.
6
+ *
7
+ * ## Core Principle: T-Vector Reachability Analysis
8
+ *
9
+ * An entity belongs to the Initial Chunk if and only if:
10
+ * 1. It is visible at T=0 (initial render time), AND
11
+ * 2. It is reachable from the viewport bounds at T=0
12
+ *
13
+ * An entity belongs to a Lazy Chunk if:
14
+ * 1. It becomes visible only after T changes (user interaction), OR
15
+ * 2. It is outside the initial viewport (below the fold), OR
16
+ * 3. It depends on Q-dimension data that hasn't loaded yet
17
+ *
18
+ * ## Algorithm Overview
19
+ *
20
+ * ```
21
+ * INPUT: Constraint Graph G = (Entities, Constraints)
22
+ * Viewport V = (width, height)
23
+ * Initial Time T₀ = 0
24
+ *
25
+ * OUTPUT: Partition P = { Chunk₀ (initial), Chunk₁, Chunk₂, ... }
26
+ *
27
+ * ALGORITHM:
28
+ * 1. Evaluate all constraints at T=T₀ to get initial positions
29
+ * 2. Mark entities intersecting V as "initially visible"
30
+ * 3. For each Q-dimension binding (event handler):
31
+ * a. Trace which constraints are affected when event fires
32
+ * b. Group affected entities into event-specific lazy chunks
33
+ * 4. For entities below the fold:
34
+ * a. Create viewport-intersection lazy chunks
35
+ * 5. Compute chunk dependency DAG
36
+ * ```
37
+ */
38
+ /**
39
+ * Rule 1: Initial Visibility Rule
40
+ *
41
+ * An entity E is initially visible iff:
42
+ * ∃ constraint C where C.target = E ∧ C.component ∈ {x, y} ∧
43
+ * eval(C, T=0) produces a position within viewport bounds
44
+ *
45
+ * Pseudocode:
46
+ * ```
47
+ * function isInitiallyVisible(entity, constraints, viewport):
48
+ * position = evaluatePosition(entity, constraints, T=0)
49
+ * return intersects(position.bounds, viewport)
50
+ * ```
51
+ */
52
+ function computeInitiallyVisibleEntities(ir, viewport) {
53
+ const visible = new Set();
54
+ // Step 1: Evaluate all constraints at T=0
55
+ const positions = evaluateConstraintsAtT0(ir.constraints);
56
+ // Step 2: Check intersection with viewport
57
+ for (const entityId of ir.entities) {
58
+ const pos = positions.get(entityId);
59
+ if (pos && intersectsViewport(pos, viewport)) {
60
+ visible.add(entityId);
61
+ }
62
+ }
63
+ // Step 3: Include entities referenced by visible entities (transitive closure)
64
+ let changed = true;
65
+ while (changed) {
66
+ changed = false;
67
+ for (const entityId of visible) {
68
+ const refs = getReferencedEntities(entityId, ir.constraints);
69
+ for (const ref of refs) {
70
+ if (!visible.has(ref)) {
71
+ visible.add(ref);
72
+ changed = true;
73
+ }
74
+ }
75
+ }
76
+ }
77
+ return visible;
78
+ }
79
+ /**
80
+ * Rule 2: Event-Triggered Lazy Chunk Rule
81
+ *
82
+ * When an event E triggers constraint C, all entities affected by C
83
+ * (directly or transitively) form a lazy chunk.
84
+ *
85
+ * Affected entities are computed via constraint dependency analysis:
86
+ * ```
87
+ * function getAffectedEntities(constraint):
88
+ * affected = {constraint.target}
89
+ * for each constraint C' that references constraint.target:
90
+ * affected = affected ∪ getAffectedEntities(C')
91
+ * return affected
92
+ * ```
93
+ *
94
+ * Q-dimension involvement:
95
+ * - Events ARE Q-dimension (user input is unpredictable)
96
+ * - Event handlers define the boundary between Initial and Lazy
97
+ * - Each unique event source creates a potential chunk boundary
98
+ */
99
+ function computeEventTriggeredChunks(ir, initiallyVisible) {
100
+ const eventChunks = new Map();
101
+ for (const binding of ir.eventBindings) {
102
+ const chunkKey = `event:${binding.sourceEntity}:${binding.eventType}`;
103
+ // Find the constraint being modified
104
+ const targetConstraint = ir.constraints.find(c => c.id === binding.targetConstraint);
105
+ if (!targetConstraint)
106
+ continue;
107
+ // Compute transitively affected entities
108
+ const affected = computeAffectedEntities(targetConstraint.target, ir.constraints);
109
+ // Filter to entities NOT in initial chunk
110
+ const lazyEntities = new Set();
111
+ for (const entityId of affected) {
112
+ if (!initiallyVisible.has(entityId)) {
113
+ lazyEntities.add(entityId);
114
+ }
115
+ }
116
+ if (lazyEntities.size > 0) {
117
+ eventChunks.set(chunkKey, lazyEntities);
118
+ }
119
+ }
120
+ return eventChunks;
121
+ }
122
+ /**
123
+ * Rule 3: Below-the-Fold Lazy Chunk Rule
124
+ *
125
+ * Entities outside the initial viewport form viewport-intersection chunks.
126
+ * These are loaded when the user scrolls them into view.
127
+ *
128
+ * Chunking strategy:
129
+ * - Divide the below-fold area into horizontal bands
130
+ * - Each band forms a separate chunk
131
+ * - Chunk loading is triggered by Intersection Observer
132
+ */
133
+ function computeViewportIntersectionChunks(ir, viewport, initiallyVisible) {
134
+ const viewportChunks = new Map();
135
+ // Evaluate positions
136
+ const positions = evaluateConstraintsAtT0(ir.constraints);
137
+ // Band height (configurable, default to viewport height)
138
+ const bandHeight = viewport.height;
139
+ for (const entityId of ir.entities) {
140
+ if (initiallyVisible.has(entityId))
141
+ continue;
142
+ const pos = positions.get(entityId);
143
+ if (!pos)
144
+ continue;
145
+ // Determine which band this entity falls into
146
+ const bandIndex = Math.floor(pos.y / bandHeight);
147
+ const chunkKey = `viewport-band:${bandIndex}`;
148
+ if (!viewportChunks.has(chunkKey)) {
149
+ viewportChunks.set(chunkKey, new Set());
150
+ }
151
+ viewportChunks.get(chunkKey).add(entityId);
152
+ }
153
+ return viewportChunks;
154
+ }
155
+ /**
156
+ * Rule 4: Import-Based Chunk Boundary Rule
157
+ *
158
+ * Each imported module can define a chunk boundary.
159
+ * This enables code splitting at the component level.
160
+ *
161
+ * ```
162
+ * import { Button } from "./components/button.vs"
163
+ * // Button's entities MAY be in a separate chunk if:
164
+ * // - They are not initially visible, OR
165
+ * // - They are marked with `lazy: true` in the import
166
+ * ```
167
+ */
168
+ function computeImportChunks(ir, initiallyVisible) {
169
+ const importChunks = new Map();
170
+ for (const imp of ir.imports) {
171
+ const lazyEntities = new Set();
172
+ for (const entityId of imp.exportedEntities) {
173
+ if (!initiallyVisible.has(entityId)) {
174
+ lazyEntities.add(entityId);
175
+ }
176
+ }
177
+ if (lazyEntities.size > 0) {
178
+ const chunkKey = `import:${imp.path}`;
179
+ importChunks.set(chunkKey, lazyEntities);
180
+ }
181
+ }
182
+ return importChunks;
183
+ }
184
+ /**
185
+ * Main chunk splitting function.
186
+ */
187
+ export function splitIntoChunks(ir, viewport) {
188
+ // Step 1: Compute initially visible entities
189
+ const initiallyVisible = computeInitiallyVisibleEntities(ir, viewport);
190
+ // Step 2: Compute event-triggered chunks
191
+ const eventChunks = computeEventTriggeredChunks(ir, initiallyVisible);
192
+ // Step 3: Compute viewport-intersection chunks
193
+ const viewportChunks = computeViewportIntersectionChunks(ir, viewport, initiallyVisible);
194
+ // Step 4: Compute import-based chunks
195
+ const importChunks = computeImportChunks(ir, initiallyVisible);
196
+ // Step 5: Merge and deduplicate chunks
197
+ const allLazyChunks = new Map();
198
+ for (const [key, entities] of eventChunks) {
199
+ allLazyChunks.set(key, entities);
200
+ }
201
+ for (const [key, entities] of viewportChunks) {
202
+ if (!allLazyChunks.has(key)) {
203
+ allLazyChunks.set(key, entities);
204
+ }
205
+ else {
206
+ // Merge
207
+ for (const e of entities) {
208
+ allLazyChunks.get(key).add(e);
209
+ }
210
+ }
211
+ }
212
+ for (const [key, entities] of importChunks) {
213
+ if (!allLazyChunks.has(key)) {
214
+ allLazyChunks.set(key, entities);
215
+ }
216
+ }
217
+ // Step 6: Build chunk objects
218
+ const initialChunk = {
219
+ id: 'initial',
220
+ entityIds: Array.from(initiallyVisible),
221
+ dependsOn: [],
222
+ isInitial: true,
223
+ loadTriggers: [{ type: 'immediate' }],
224
+ };
225
+ const lazyChunks = [];
226
+ const entityToChunk = new Map();
227
+ for (const entityId of initiallyVisible) {
228
+ entityToChunk.set(entityId, 'initial');
229
+ }
230
+ for (const [chunkId, entities] of allLazyChunks) {
231
+ const triggers = computeLoadTriggers(chunkId, entities, ir);
232
+ const chunk = {
233
+ id: chunkId,
234
+ entityIds: Array.from(entities),
235
+ dependsOn: ['initial'], // All lazy chunks depend on initial
236
+ isInitial: false,
237
+ loadTriggers: triggers,
238
+ };
239
+ lazyChunks.push(chunk);
240
+ for (const entityId of entities) {
241
+ entityToChunk.set(entityId, chunkId);
242
+ }
243
+ }
244
+ // Step 7: Compute chunk dependencies
245
+ const chunkDependencies = computeChunkDependencies(initialChunk, lazyChunks, ir.constraints, entityToChunk);
246
+ return {
247
+ initialChunk,
248
+ lazyChunks,
249
+ entityToChunk,
250
+ chunkDependencies,
251
+ };
252
+ }
253
+ // =============================================================================
254
+ // Helper Functions
255
+ // =============================================================================
256
+ function evaluateConstraintsAtT0(constraints) {
257
+ // Simplified evaluation - in production, this would use
258
+ // the full constraint solver with T=0
259
+ const positions = new Map();
260
+ for (const c of constraints) {
261
+ if (!positions.has(c.target)) {
262
+ positions.set(c.target, { x: 0, y: 0, z: 0 });
263
+ }
264
+ const pos = positions.get(c.target);
265
+ if (c.term.type === 'const') {
266
+ const value = rationalToNumber(c.term.value);
267
+ if (c.component === 'x')
268
+ pos.x = value;
269
+ if (c.component === 'y')
270
+ pos.y = value;
271
+ if (c.component === 'z')
272
+ pos.z = value;
273
+ }
274
+ }
275
+ return positions;
276
+ }
277
+ function rationalToNumber(r) {
278
+ return Number(r.numerator) / Number(r.denominator);
279
+ }
280
+ function intersectsViewport(pos, viewport) {
281
+ // Simplified - assumes point intersection
282
+ return (pos.x >= viewport.x &&
283
+ pos.x <= viewport.x + viewport.width &&
284
+ pos.y >= viewport.y &&
285
+ pos.y <= viewport.y + viewport.height);
286
+ }
287
+ function getReferencedEntities(entityId, constraints) {
288
+ const refs = new Set();
289
+ for (const c of constraints) {
290
+ if (c.target !== entityId)
291
+ continue;
292
+ if (c.term.type === 'ref') {
293
+ refs.add(c.term.entityId);
294
+ }
295
+ else if (c.term.type === 'linear') {
296
+ refs.add(c.term.entityId);
297
+ }
298
+ }
299
+ return refs;
300
+ }
301
+ function computeAffectedEntities(startEntity, constraints) {
302
+ const affected = new Set();
303
+ const queue = [startEntity];
304
+ while (queue.length > 0) {
305
+ const current = queue.shift();
306
+ if (affected.has(current))
307
+ continue;
308
+ affected.add(current);
309
+ // Find constraints that reference this entity
310
+ for (const c of constraints) {
311
+ const refEntity = c.term.type === 'ref' ? c.term.entityId :
312
+ c.term.type === 'linear' ? c.term.entityId :
313
+ null;
314
+ if (refEntity === current && !affected.has(c.target)) {
315
+ queue.push(c.target);
316
+ }
317
+ }
318
+ }
319
+ return affected;
320
+ }
321
+ function computeLoadTriggers(chunkId, entities, ir) {
322
+ const triggers = [];
323
+ if (chunkId.startsWith('event:')) {
324
+ const [, entityIdStr, eventType] = chunkId.split(':');
325
+ triggers.push({
326
+ type: 'event',
327
+ eventType,
328
+ targetEntity: parseInt(entityIdStr, 10),
329
+ });
330
+ }
331
+ else if (chunkId.startsWith('viewport-band:')) {
332
+ // Use first entity as intersection target
333
+ const firstEntity = entities.values().next().value;
334
+ if (firstEntity !== undefined) {
335
+ triggers.push({
336
+ type: 'viewport-intersect',
337
+ entityId: firstEntity,
338
+ });
339
+ }
340
+ }
341
+ return triggers;
342
+ }
343
+ function computeChunkDependencies(initial, lazy, constraints, entityToChunk) {
344
+ const deps = new Map();
345
+ deps.set(initial.id, new Set());
346
+ for (const chunk of lazy) {
347
+ const chunkDeps = new Set();
348
+ // Check if any entity in this chunk references an entity in another chunk
349
+ for (const entityId of chunk.entityIds) {
350
+ const refs = getReferencedEntities(entityId, constraints);
351
+ for (const ref of refs) {
352
+ const refChunk = entityToChunk.get(ref);
353
+ if (refChunk && refChunk !== chunk.id) {
354
+ chunkDeps.add(refChunk);
355
+ }
356
+ }
357
+ }
358
+ deps.set(chunk.id, chunkDeps);
359
+ }
360
+ return deps;
361
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * ViewScript Renderer
3
+ *
4
+ * Compiles .vs IR files into target-specific output (wgpu, WebGL, SVG).
5
+ */
6
+ export interface RenderTarget {
7
+ name: string;
8
+ compile(ir: VsIR): Promise<CompiledOutput>;
9
+ }
10
+ export interface VsIR {
11
+ entities: Entity[];
12
+ constraints: Constraint[];
13
+ }
14
+ export interface Entity {
15
+ id: number;
16
+ type: 'point' | 'curve' | 'surface';
17
+ vector: PVector;
18
+ }
19
+ export interface PVector {
20
+ x: number;
21
+ y: number;
22
+ z: number;
23
+ t: number;
24
+ }
25
+ export interface Constraint {
26
+ id: number;
27
+ target: number;
28
+ component: 'x' | 'y' | 'z' | 't';
29
+ relation: 'eq' | 'lt' | 'le' | 'gt' | 'ge';
30
+ term: ConstraintTerm;
31
+ }
32
+ export type ConstraintTerm = {
33
+ type: 'const';
34
+ value: number;
35
+ } | {
36
+ type: 'ref';
37
+ entityId: number;
38
+ component: 'x' | 'y' | 'z' | 't';
39
+ } | {
40
+ type: 'linear';
41
+ coefficient: number;
42
+ entityId: number;
43
+ component: 'x' | 'y' | 'z' | 't';
44
+ offset: number;
45
+ };
46
+ export interface CompiledOutput {
47
+ html: string;
48
+ js: string;
49
+ css: string;
50
+ assets: Map<string, Uint8Array>;
51
+ }
52
+ export declare class WgpuTarget implements RenderTarget {
53
+ name: string;
54
+ compile(ir: VsIR): Promise<CompiledOutput>;
55
+ }
package/dist/index.js ADDED
@@ -0,0 +1,17 @@
1
+ /**
2
+ * ViewScript Renderer
3
+ *
4
+ * Compiles .vs IR files into target-specific output (wgpu, WebGL, SVG).
5
+ */
6
+ export class WgpuTarget {
7
+ name = 'wgpu';
8
+ async compile(ir) {
9
+ // TODO: Implement wgpu compilation
10
+ return {
11
+ html: '',
12
+ js: '',
13
+ css: '',
14
+ assets: new Map(),
15
+ };
16
+ }
17
+ }
@@ -0,0 +1,7 @@
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
+ export {};