@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,458 @@
1
+ /**
2
+ * Q-Dimension Event Backpressure Control
3
+ *
4
+ * High-frequency DOM events (mousemove, scroll, pointermove) can fire
5
+ * hundreds of times per second. Without backpressure, each event would
6
+ * trigger a full constraint graph evaluation, overwhelming the solver.
7
+ *
8
+ * ## Strategy: Latest-Only Sampling with Frame Alignment
9
+ *
10
+ * ```
11
+ * Event Stream (60+ events/frame):
12
+ * ─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─►
13
+ * │ │ │ │ │ │ │ │ │ │ │ │
14
+ * └─┴─┴─┴─┬─┴─┴─┴─┴─┬─┴─┴─► Frame boundaries
15
+ * │ │
16
+ * ▼ ▼
17
+ * Sampled: [latest] [latest] ← One value per frame
18
+ * ```
19
+ *
20
+ * This module provides:
21
+ * 1. Per-entity, per-component event coalescing
22
+ * 2. Frame-aligned sampling (sync with rAF)
23
+ * 3. Configurable throttle strategies
24
+ * 4. Priority queues for critical events (click > mousemove)
25
+ */
26
+ export var EventPriority;
27
+ (function (EventPriority) {
28
+ /** Immediate: click, keydown (user intent) */
29
+ EventPriority[EventPriority["CRITICAL"] = 3] = "CRITICAL";
30
+ /** High: pointerdown/up (gesture start/end) */
31
+ EventPriority[EventPriority["HIGH"] = 2] = "HIGH";
32
+ /** Normal: scroll, wheel (continuous) */
33
+ EventPriority[EventPriority["NORMAL"] = 1] = "NORMAL";
34
+ /** Low: pointermove (high frequency, lossy OK) */
35
+ EventPriority[EventPriority["LOW"] = 0] = "LOW";
36
+ })(EventPriority || (EventPriority = {}));
37
+ /**
38
+ * Double-buffered event accumulator.
39
+ *
40
+ * ## Data Structure
41
+ *
42
+ * ```
43
+ * writeBuffer (current frame events):
44
+ * Map<CoalesceKey, QDimensionEvent>
45
+ * - Key: "entity42:x"
46
+ * - Value: Latest event for that entity+component
47
+ * - Overwrites: Yes (latest-only sampling)
48
+ *
49
+ * priorityQueue (critical events):
50
+ * Array<QDimensionEvent>
51
+ * - Never coalesced (click, keydown must all fire)
52
+ * - Processed first
53
+ *
54
+ * pendingAsyncEvents (async callback events):
55
+ * Array<QDimensionEvent>
56
+ * - Events from async callbacks (fetch, setTimeout, promises)
57
+ * - Isolated from sync events to prevent race conditions
58
+ * - Merged at tick start via mergeAsyncEvents()
59
+ * ```
60
+ *
61
+ * ## Async Atomicity (Phase 2 Remediation)
62
+ *
63
+ * Events from async callbacks (fetch handlers, setTimeout, etc.) arrive
64
+ * outside the rAF tick boundary. Without isolation, they could interleave
65
+ * with sync events causing non-deterministic ordering.
66
+ *
67
+ * Solution: Async events are buffered separately and merged atomically
68
+ * at the START of each tick, before any sync event processing.
69
+ */
70
+ export class EventBuffer {
71
+ config;
72
+ /** Coalesced events (latest-only per entity+component) */
73
+ writeBuffer = new Map();
74
+ /** Non-coalesced critical events (click, keydown) */
75
+ priorityQueue = [];
76
+ /** Last event time per key (for throttling) */
77
+ lastEventTime = new Map();
78
+ /**
79
+ * Pending async events (from fetch, setTimeout, promises).
80
+ *
81
+ * These are isolated from sync events and merged at tick start
82
+ * to ensure deterministic ordering.
83
+ */
84
+ pendingAsyncEvents = [];
85
+ constructor(config = {}) {
86
+ this.config = {
87
+ maxEventsPerFrame: 50,
88
+ lowPriorityThrottleMs: 16, // ~60fps
89
+ enableCoalescing: true,
90
+ ...config,
91
+ };
92
+ }
93
+ /**
94
+ * Push a Q-dimension event into the buffer.
95
+ *
96
+ * ## Coalescing Rules
97
+ *
98
+ * 1. CRITICAL priority: Always queued, never coalesced
99
+ * 2. HIGH/NORMAL/LOW: Coalesced by entity+state (latest wins)
100
+ * 3. LOW with throttle: Dropped if within throttle window
101
+ */
102
+ push(event) {
103
+ const key = `${event.entityId}:${event.targetState}`;
104
+ // Critical events bypass coalescing
105
+ if (event.priority === EventPriority.CRITICAL) {
106
+ this.priorityQueue.push(event);
107
+ return;
108
+ }
109
+ // Throttle check for low-priority events
110
+ if (event.priority === EventPriority.LOW) {
111
+ const lastTime = this.lastEventTime.get(key) ?? 0;
112
+ if (event.timestamp - lastTime < this.config.lowPriorityThrottleMs) {
113
+ // Drop: within throttle window
114
+ return;
115
+ }
116
+ }
117
+ // Coalesce: overwrite previous event for same entity+component
118
+ if (this.config.enableCoalescing) {
119
+ this.writeBuffer.set(key, event);
120
+ }
121
+ else {
122
+ // No coalescing: treat as priority queue
123
+ this.priorityQueue.push(event);
124
+ }
125
+ this.lastEventTime.set(key, event.timestamp);
126
+ }
127
+ /**
128
+ * Flush buffer for frame processing.
129
+ *
130
+ * Returns events in priority order:
131
+ * 1. All CRITICAL events (in order received)
132
+ * 2. Coalesced events (limited by maxEventsPerFrame)
133
+ *
134
+ * ## Frame Alignment
135
+ *
136
+ * This method is called once per rAF tick. Events that arrive
137
+ * after flush() but before next tick accumulate in the buffer.
138
+ */
139
+ flush() {
140
+ const result = [];
141
+ const limit = this.config.maxEventsPerFrame;
142
+ // 1. Critical events first (never dropped)
143
+ for (const event of this.priorityQueue) {
144
+ result.push({
145
+ entityId: event.entityId,
146
+ state: event.targetState,
147
+ value: event.value,
148
+ timestamp: event.timestamp,
149
+ });
150
+ }
151
+ this.priorityQueue = [];
152
+ // 2. Coalesced events (up to limit)
153
+ const remaining = limit - result.length;
154
+ if (remaining > 0) {
155
+ const coalesced = Array.from(this.writeBuffer.values())
156
+ .sort((a, b) => b.priority - a.priority) // Higher priority first
157
+ .slice(0, remaining);
158
+ for (const event of coalesced) {
159
+ result.push({
160
+ entityId: event.entityId,
161
+ state: event.targetState,
162
+ value: event.value,
163
+ timestamp: event.timestamp,
164
+ });
165
+ }
166
+ }
167
+ // Clear coalesced buffer (events not taken are dropped)
168
+ this.writeBuffer.clear();
169
+ return result;
170
+ }
171
+ /**
172
+ * Get current buffer sizes (for debugging/metrics).
173
+ */
174
+ getStats() {
175
+ return {
176
+ priorityQueueSize: this.priorityQueue.length,
177
+ coalescedSize: this.writeBuffer.size,
178
+ asyncPendingSize: this.pendingAsyncEvents.length,
179
+ };
180
+ }
181
+ /**
182
+ * Push an event from an async callback.
183
+ *
184
+ * Use this method for events originating from:
185
+ * - fetch() handlers
186
+ * - setTimeout / setInterval callbacks
187
+ * - Promise .then() / .catch() handlers
188
+ * - WebSocket message handlers
189
+ * - IndexedDB callbacks
190
+ * - Any other async context
191
+ *
192
+ * ## Why Separate From push()?
193
+ *
194
+ * Async callbacks can fire at any time, potentially mid-tick or between
195
+ * ticks. If mixed with sync events without ordering guarantees, the result
196
+ * is non-deterministic constraint evaluation.
197
+ *
198
+ * By isolating async events, we ensure:
199
+ * 1. All async events are processed in FIFO order
200
+ * 2. They are merged BEFORE sync events at tick start
201
+ * 3. The tick sees a consistent snapshot of async state
202
+ *
203
+ * ## Example
204
+ *
205
+ * ```typescript
206
+ * fetch('/api/data').then(response => {
207
+ * // WRONG: buffer.push(event) - could race with sync events
208
+ * // CORRECT: buffer.pushAsync(event) - isolated until tick start
209
+ * buffer.pushAsync({
210
+ * entityId: 42,
211
+ * eventType: 'custom',
212
+ * targetState: 'animation_t',
213
+ * value: response.progress,
214
+ * timestamp: performance.now(),
215
+ * priority: EventPriority.NORMAL,
216
+ * });
217
+ * });
218
+ * ```
219
+ */
220
+ pushAsync(event) {
221
+ this.pendingAsyncEvents.push(event);
222
+ }
223
+ /**
224
+ * Merge pending async events into the main buffers.
225
+ *
226
+ * MUST be called at the START of each tick, BEFORE flush().
227
+ *
228
+ * This ensures:
229
+ * 1. Async events are processed before sync events from the same frame
230
+ * 2. No async events can arrive mid-flush (atomicity)
231
+ * 3. Deterministic ordering: async (FIFO) → sync (coalesced/priority)
232
+ *
233
+ * ## Call Site
234
+ *
235
+ * AtomicRenderLoop.tick():
236
+ * ```typescript
237
+ * function tick(timestamp) {
238
+ * // FIRST: Merge async events atomically
239
+ * eventBuffer.mergeAsyncEvents();
240
+ *
241
+ * // THEN: Flush and process all events
242
+ * const mutations = eventBuffer.flush();
243
+ * // ... rest of tick
244
+ * }
245
+ * ```
246
+ */
247
+ mergeAsyncEvents() {
248
+ if (this.pendingAsyncEvents.length === 0) {
249
+ return;
250
+ }
251
+ // Process async events through the normal push() pipeline
252
+ // This applies coalescing and priority rules consistently
253
+ for (const event of this.pendingAsyncEvents) {
254
+ this.push(event);
255
+ }
256
+ // Clear the async buffer
257
+ this.pendingAsyncEvents = [];
258
+ }
259
+ }
260
+ // =============================================================================
261
+ // Event Controller (DOM Binding Layer)
262
+ // =============================================================================
263
+ /**
264
+ * DOM event controller with automatic backpressure.
265
+ *
266
+ * ## Data Flow
267
+ *
268
+ * ```
269
+ * DOM Event (mousemove)
270
+ * │
271
+ * ▼
272
+ * EventController.handleEvent()
273
+ * │
274
+ * ├─▶ Compute T-vector value (from event data)
275
+ * │
276
+ * ├─▶ Wrap as QDimensionEvent
277
+ * │
278
+ * ▼
279
+ * EventBuffer.push()
280
+ * │
281
+ * ├─▶ Throttle check (LOW priority)
282
+ * │
283
+ * ├─▶ Coalesce (latest-only)
284
+ * │
285
+ * ▼
286
+ * Buffer accumulates until next rAF
287
+ * │
288
+ * ▼
289
+ * AtomicRenderLoop.tick()
290
+ * │
291
+ * ├─▶ EventBuffer.flush()
292
+ * │
293
+ * ▼
294
+ * CoalescedEvent[] → ConstraintSolver
295
+ * ```
296
+ */
297
+ export class EventController {
298
+ buffer;
299
+ entityElements = new Map();
300
+ boundHandlers = new Map();
301
+ constructor(buffer) {
302
+ this.buffer = buffer;
303
+ }
304
+ /**
305
+ * Register a DOM element for event handling.
306
+ */
307
+ registerElement(entityId, element) {
308
+ this.entityElements.set(entityId, element);
309
+ }
310
+ /**
311
+ * Bind an event type to a T-vector state key.
312
+ *
313
+ * ## Ouroboros Prevention
314
+ *
315
+ * The valueMapper function MUST return a SEMANTIC state value, not a
316
+ * raw spatial coordinate. For example:
317
+ *
318
+ * CORRECT (semantic state):
319
+ * - `'hover'` → (e) => e.type === 'pointerenter' ? 1 : 0
320
+ * - `'scroll_y'` → (e) => e.target.scrollTop / e.target.scrollHeight
321
+ * - `'drag_progress'` → (e) => computeNormalizedDragProgress(e)
322
+ *
323
+ * FORBIDDEN (spatial coordinate - would violate P/Q boundary):
324
+ * - `'x'` → (e) => e.clientX // NEVER DO THIS
325
+ * - `'y'` → (e) => e.clientY // NEVER DO THIS
326
+ *
327
+ * @param entityId - Entity to bind the event to
328
+ * @param eventType - DOM event type
329
+ * @param targetState - T-vector state key (semantic, not spatial)
330
+ * @param valueMapper - Function to compute state value from DOM event
331
+ */
332
+ bindEvent(entityId, eventType, targetState, valueMapper) {
333
+ const element = this.entityElements.get(entityId);
334
+ if (!element)
335
+ return;
336
+ const handler = (domEvent) => {
337
+ const qEvent = {
338
+ entityId,
339
+ eventType,
340
+ targetState,
341
+ value: valueMapper(domEvent),
342
+ timestamp: performance.now(),
343
+ priority: this.getPriority(eventType),
344
+ };
345
+ this.buffer.push(qEvent);
346
+ };
347
+ const key = `${entityId}:${eventType}`;
348
+ this.boundHandlers.set(key, handler);
349
+ // Use passive listeners where possible (scroll, wheel, pointermove)
350
+ const passive = ['scroll', 'wheel', 'pointermove'].includes(eventType);
351
+ element.addEventListener(eventType, handler, { passive });
352
+ }
353
+ /**
354
+ * Unbind all events for an entity.
355
+ */
356
+ unbindEntity(entityId) {
357
+ const element = this.entityElements.get(entityId);
358
+ if (!element)
359
+ return;
360
+ for (const [key, handler] of this.boundHandlers) {
361
+ if (key.startsWith(`${entityId}:`)) {
362
+ const eventType = key.split(':')[1];
363
+ element.removeEventListener(eventType, handler);
364
+ this.boundHandlers.delete(key);
365
+ }
366
+ }
367
+ this.entityElements.delete(entityId);
368
+ }
369
+ /**
370
+ * Map event type to priority.
371
+ */
372
+ getPriority(eventType) {
373
+ switch (eventType) {
374
+ case 'click':
375
+ case 'keydown':
376
+ case 'keyup':
377
+ return EventPriority.CRITICAL;
378
+ case 'pointerdown':
379
+ case 'pointerup':
380
+ case 'focus':
381
+ case 'blur':
382
+ return EventPriority.HIGH;
383
+ case 'scroll':
384
+ case 'wheel':
385
+ return EventPriority.NORMAL;
386
+ case 'pointermove':
387
+ return EventPriority.LOW;
388
+ default:
389
+ return EventPriority.NORMAL;
390
+ }
391
+ }
392
+ }
393
+ // =============================================================================
394
+ // Usage Example
395
+ // =============================================================================
396
+ /**
397
+ * ## Integration with Render Loop
398
+ *
399
+ * ```typescript
400
+ * const buffer = new EventBuffer({ maxEventsPerFrame: 50 });
401
+ * const controller = new EventController(buffer);
402
+ *
403
+ * // Register entity's DOM element
404
+ * controller.registerElement(entity.id, domElement);
405
+ *
406
+ * // CORRECT: Bind semantic state (hover) to T-vector
407
+ * controller.bindEvent(
408
+ * entity.id,
409
+ * 'pointerenter',
410
+ * 'hover',
411
+ * () => 1 // hover = true
412
+ * );
413
+ * controller.bindEvent(
414
+ * entity.id,
415
+ * 'pointerleave',
416
+ * 'hover',
417
+ * () => 0 // hover = false
418
+ * );
419
+ *
420
+ * // CORRECT: Bind normalized scroll position
421
+ * controller.bindEvent(
422
+ * entity.id,
423
+ * 'scroll',
424
+ * 'scroll_y',
425
+ * (e: Event) => {
426
+ * const target = e.target as HTMLElement;
427
+ * const maxScroll = target.scrollHeight - target.clientHeight;
428
+ * return maxScroll > 0 ? target.scrollTop / maxScroll : 0;
429
+ * }
430
+ * );
431
+ *
432
+ * // In render loop tick():
433
+ * const events = buffer.flush();
434
+ *
435
+ * // Events are T-vector STATE mutations, not spatial coordinates
436
+ * // The constraint solver evaluates P-dimension coordinates AS A FUNCTION
437
+ * // of T-vector state (via constraints like: A.x = 100 when T.hover = 1)
438
+ * const stateMutations = events.map(e => ({
439
+ * entityId: e.entityId,
440
+ * state: e.state, // Semantic state key (hover, scroll_y, etc.)
441
+ * value: e.value, // State value
442
+ * timestamp: e.timestamp,
443
+ * }));
444
+ *
445
+ * // Constraint solver derives X, Y, Z from T-vector state
446
+ * constraintSolver.evaluateWithTVector(stateMutations);
447
+ * ```
448
+ *
449
+ * ## Architectural Guarantee: P/Q Boundary Preservation
450
+ *
451
+ * By restricting Q-dimension events to T-vector STATE keys (not spatial
452
+ * coordinates), we ensure that:
453
+ *
454
+ * 1. Mouse coordinates (clientX/Y) are NEVER directly assigned to P-dimension
455
+ * 2. P-dimension coordinates are always derived via constraint evaluation
456
+ * 3. The constraint graph is the SINGLE SOURCE OF TRUTH for spatial layout
457
+ * 4. LEAN 4 decidability proofs remain valid (no floating-point pollution)
458
+ */