@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,660 @@
1
+ /**
2
+ * Atomic Render Loop for ViewScript
3
+ *
4
+ * This module implements a frame-synchronous render pipeline that guarantees
5
+ * Canvas and DOM layers are updated atomically within a single animation frame.
6
+ *
7
+ * ## Architectural Invariant: No Frame Tearing
8
+ *
9
+ * If a button's visual representation moves at frame N, its DOM hit region
10
+ * MUST also move at frame N. Any desynchronization causes Q-dimension inputs
11
+ * to "hit the void" - a catastrophic UX failure.
12
+ *
13
+ * ## Strategy: Double-Buffered Dirty Tracking
14
+ *
15
+ * ```
16
+ * Frame N:
17
+ * ┌─────────────────────────────────────────────────────────────────┐
18
+ * │ 1. Flush pending T-vector mutations (from Q-dimension events) │
19
+ * │ 2. Evaluate constraint graph (P-dimension solver) │
20
+ * │ 3. Topology-preserving rounding (with error distribution) │
21
+ * │ 4. Diff against previous frame's RasterBounds │
22
+ * │ 5. Batch Canvas draw commands (wgpu) │
23
+ * │ 6. Batch DOM style mutations (transform only, no reflow) │
24
+ * │ 7. Commit: GpuRenderer.flush() + requestAnimationFrame boundary│
25
+ * └─────────────────────────────────────────────────────────────────┘
26
+ * ```
27
+ *
28
+ * ## Reflow Prevention Strategy
29
+ *
30
+ * DOM mutations are restricted to compositor-only properties:
31
+ * - transform: translate3d(x, y, 0) - GPU-accelerated, no reflow
32
+ * - opacity - compositor-only
33
+ * - will-change: transform - hint to browser
34
+ *
35
+ * We NEVER touch: width, height, top, left, margin, padding (reflow triggers)
36
+ */
37
+
38
+ import type {
39
+ EntityId,
40
+ RenderableEntity,
41
+ RasterBounds,
42
+ PVectorBounds,
43
+ ChunkId,
44
+ } from '../ast/types';
45
+
46
+ // =============================================================================
47
+ // Types
48
+ // =============================================================================
49
+
50
+ /**
51
+ * Semantic T-vector state keys.
52
+ * Mirrors the type from event-backpressure.ts.
53
+ */
54
+ type TStateKey =
55
+ | 'hover'
56
+ | 'pressed'
57
+ | 'focused'
58
+ | 'scroll_x'
59
+ | 'scroll_y'
60
+ | 'drag_progress'
61
+ | 'animation_t'
62
+ | 'gesture_phase';
63
+
64
+ /**
65
+ * Pending T-vector state mutation from Q-dimension event.
66
+ *
67
+ * CRITICAL: These are SEMANTIC state mutations, not spatial coordinate
68
+ * assignments. P-dimension coordinates (X, Y, Z) are derived by the
69
+ * constraint solver as functions of T-vector state.
70
+ */
71
+ interface TStateMutation {
72
+ entityId: EntityId;
73
+ /** Semantic state key (hover, scroll_y, etc.) - NOT spatial coordinates */
74
+ state: TStateKey;
75
+ /** State value (boolean as 0/1, normalized 0-1, or discrete phase) */
76
+ value: number;
77
+ timestamp: number;
78
+ }
79
+
80
+ /**
81
+ * Frame state for double buffering.
82
+ */
83
+ interface FrameState {
84
+ /** Raster bounds from previous frame (for diffing) */
85
+ previousBounds: Map<EntityId, RasterBounds>;
86
+
87
+ /** Current frame's computed bounds */
88
+ currentBounds: Map<EntityId, RasterBounds>;
89
+
90
+ /** Entities that changed this frame */
91
+ dirtyEntities: Set<EntityId>;
92
+
93
+ /** Frame number for debugging */
94
+ frameNumber: number;
95
+ }
96
+
97
+ /**
98
+ * Canvas draw command (batched for GPU renderer).
99
+ */
100
+ interface DrawCommand {
101
+ entityId: EntityId;
102
+ type: 'path' | 'text' | 'image' | 'group';
103
+ bounds: RasterBounds;
104
+ payload: unknown; // Type-specific draw data
105
+ }
106
+
107
+ /**
108
+ * DOM mutation command (batched for atomic commit).
109
+ */
110
+ interface DOMMutation {
111
+ entityId: EntityId;
112
+ element: HTMLElement;
113
+ transform: string;
114
+ opacity?: number;
115
+ }
116
+
117
+ /**
118
+ * Render loop configuration.
119
+ */
120
+ export interface RenderLoopConfig {
121
+ /** Target frames per second (default: 60) */
122
+ targetFPS: number;
123
+
124
+ /** Enable debug timing logs */
125
+ debugTiming: boolean;
126
+
127
+ /** Maximum mutations per frame (backpressure) */
128
+ maxMutationsPerFrame: number;
129
+ }
130
+
131
+ // =============================================================================
132
+ // Core Render Loop
133
+ // =============================================================================
134
+
135
+ export class AtomicRenderLoop {
136
+ private config: RenderLoopConfig;
137
+ private frameState: FrameState;
138
+ private pendingMutations: TStateMutation[] = [];
139
+ private isRunning = false;
140
+ private rafHandle: number | null = null;
141
+
142
+ // External dependencies (injected)
143
+ private constraintSolver: ConstraintSolver;
144
+ private topologyRounder: TopologyRounder;
145
+ private canvasRenderer: CanvasRenderer;
146
+ private domLayer: DOMLayer;
147
+
148
+ /**
149
+ * Event buffer for async event atomicity.
150
+ *
151
+ * Injected via setEventBuffer() to support mergeAsyncEvents() at tick start.
152
+ */
153
+ private eventBuffer: EventBufferInterface | null = null;
154
+
155
+ constructor(
156
+ config: Partial<RenderLoopConfig>,
157
+ constraintSolver: ConstraintSolver,
158
+ topologyRounder: TopologyRounder,
159
+ canvasRenderer: CanvasRenderer,
160
+ domLayer: DOMLayer,
161
+ ) {
162
+ this.config = {
163
+ targetFPS: 60,
164
+ debugTiming: false,
165
+ maxMutationsPerFrame: 100,
166
+ ...config,
167
+ };
168
+
169
+ this.frameState = {
170
+ previousBounds: new Map(),
171
+ currentBounds: new Map(),
172
+ dirtyEntities: new Set(),
173
+ frameNumber: 0,
174
+ };
175
+
176
+ this.constraintSolver = constraintSolver;
177
+ this.topologyRounder = topologyRounder;
178
+ this.canvasRenderer = canvasRenderer;
179
+ this.domLayer = domLayer;
180
+ }
181
+
182
+ // ===========================================================================
183
+ // Public API
184
+ // ===========================================================================
185
+
186
+ /**
187
+ * Start the render loop.
188
+ */
189
+ start(): void {
190
+ if (this.isRunning) return;
191
+ this.isRunning = true;
192
+ this.scheduleFrame();
193
+ }
194
+
195
+ /**
196
+ * Stop the render loop.
197
+ */
198
+ stop(): void {
199
+ this.isRunning = false;
200
+ if (this.rafHandle !== null) {
201
+ cancelAnimationFrame(this.rafHandle);
202
+ this.rafHandle = null;
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Queue a T-vector mutation from Q-dimension event.
208
+ * Called from event handlers (backpressure-controlled).
209
+ */
210
+ queueMutation(mutation: TStateMutation): void {
211
+ this.pendingMutations.push(mutation);
212
+ }
213
+
214
+ /**
215
+ * Set the event buffer for async event atomicity.
216
+ *
217
+ * The event buffer is used to merge async events (from fetch, setTimeout, etc.)
218
+ * at the start of each tick, ensuring deterministic event ordering.
219
+ */
220
+ setEventBuffer(buffer: EventBufferInterface): void {
221
+ this.eventBuffer = buffer;
222
+ }
223
+
224
+ // ===========================================================================
225
+ // Frame Execution (Atomic Commit)
226
+ // ===========================================================================
227
+
228
+ /**
229
+ * Execute a single frame tick.
230
+ *
231
+ * CRITICAL: This function executes entirely within a single rAF callback,
232
+ * ensuring Canvas and DOM updates are committed atomically before the
233
+ * browser's compositor thread runs.
234
+ */
235
+ private tick(timestamp: DOMHighResTimeStamp): void {
236
+ if (!this.isRunning) return;
237
+
238
+ const frameStart = performance.now();
239
+ this.frameState.frameNumber++;
240
+
241
+ // -------------------------------------------------------------------------
242
+ // Phase 0: Merge Async Events (MUST be first - async atomicity)
243
+ // -------------------------------------------------------------------------
244
+ // Events from async callbacks (fetch, setTimeout, promises) are isolated
245
+ // in a separate buffer to prevent race conditions. They are merged here
246
+ // at the START of the tick, before any sync event processing.
247
+ //
248
+ // This ensures:
249
+ // 1. Deterministic ordering: async events processed before sync events
250
+ // 2. Atomicity: no async events can arrive mid-tick
251
+ // 3. Consistency: the tick sees a complete snapshot of async state
252
+ if (this.eventBuffer) {
253
+ this.eventBuffer.mergeAsyncEvents();
254
+ }
255
+
256
+ // -------------------------------------------------------------------------
257
+ // Phase 1: Flush Pending Mutations (Backpressure-Limited)
258
+ // -------------------------------------------------------------------------
259
+ const mutations = this.flushMutations();
260
+
261
+ // -------------------------------------------------------------------------
262
+ // Phase 2: Evaluate Constraint Graph
263
+ // -------------------------------------------------------------------------
264
+ const pVectorBounds = this.constraintSolver.evaluate(mutations);
265
+
266
+ // -------------------------------------------------------------------------
267
+ // Phase 3: Topology-Preserving Rounding (with Error Distribution)
268
+ // -------------------------------------------------------------------------
269
+ const rasterResult = this.topologyRounder.round(pVectorBounds);
270
+
271
+ // -------------------------------------------------------------------------
272
+ // Phase 4: Compute Dirty Set (Diff Against Previous Frame)
273
+ // -------------------------------------------------------------------------
274
+ this.computeDirtySet(rasterResult.bounds);
275
+
276
+ // -------------------------------------------------------------------------
277
+ // Phase 5: Batch Canvas Draw Commands
278
+ // -------------------------------------------------------------------------
279
+ const drawCommands = this.buildDrawCommands();
280
+
281
+ // -------------------------------------------------------------------------
282
+ // Phase 6: Batch DOM Mutations (Transform Only)
283
+ // -------------------------------------------------------------------------
284
+ const domMutations = this.buildDOMMutations();
285
+
286
+ // -------------------------------------------------------------------------
287
+ // Phase 7: Atomic Commit
288
+ // -------------------------------------------------------------------------
289
+ this.commitFrame(drawCommands, domMutations);
290
+
291
+ // -------------------------------------------------------------------------
292
+ // Swap Buffers
293
+ // -------------------------------------------------------------------------
294
+ this.swapBuffers();
295
+
296
+ // -------------------------------------------------------------------------
297
+ // Schedule Next Frame
298
+ // -------------------------------------------------------------------------
299
+ const frameEnd = performance.now();
300
+ if (this.config.debugTiming) {
301
+ console.log(`Frame ${this.frameState.frameNumber}: ${(frameEnd - frameStart).toFixed(2)}ms`);
302
+ }
303
+
304
+ this.scheduleFrame();
305
+ }
306
+
307
+ private scheduleFrame(): void {
308
+ this.rafHandle = requestAnimationFrame((ts) => this.tick(ts));
309
+ }
310
+
311
+ // ===========================================================================
312
+ // Phase Implementations
313
+ // ===========================================================================
314
+
315
+ /**
316
+ * Phase 1: Flush pending mutations with backpressure limit.
317
+ */
318
+ private flushMutations(): TStateMutation[] {
319
+ const limit = this.config.maxMutationsPerFrame;
320
+ const flushed = this.pendingMutations.splice(0, limit);
321
+ return flushed;
322
+ }
323
+
324
+ /**
325
+ * Phase 4: Compute which entities changed this frame.
326
+ */
327
+ private computeDirtySet(currentBounds: Map<EntityId, RasterBounds>): void {
328
+ this.frameState.dirtyEntities.clear();
329
+ this.frameState.currentBounds = currentBounds;
330
+
331
+ for (const [entityId, bounds] of currentBounds) {
332
+ const prev = this.frameState.previousBounds.get(entityId);
333
+
334
+ if (!prev || !boundsEqual(prev, bounds)) {
335
+ this.frameState.dirtyEntities.add(entityId);
336
+ }
337
+ }
338
+
339
+ // Detect removed entities
340
+ for (const entityId of this.frameState.previousBounds.keys()) {
341
+ if (!currentBounds.has(entityId)) {
342
+ this.frameState.dirtyEntities.add(entityId);
343
+ }
344
+ }
345
+ }
346
+
347
+ /**
348
+ * Phase 5: Build Canvas draw commands for dirty entities only.
349
+ */
350
+ private buildDrawCommands(): DrawCommand[] {
351
+ const commands: DrawCommand[] = [];
352
+
353
+ for (const entityId of this.frameState.dirtyEntities) {
354
+ const bounds = this.frameState.currentBounds.get(entityId);
355
+ if (!bounds) continue;
356
+
357
+ // Get entity's canvas node and build draw command
358
+ const entity = this.canvasRenderer.getEntity(entityId);
359
+ if (entity?.canvas) {
360
+ commands.push({
361
+ entityId,
362
+ type: entity.canvas.kind,
363
+ bounds,
364
+ payload: entity.canvas,
365
+ });
366
+ }
367
+ }
368
+
369
+ return commands;
370
+ }
371
+
372
+ /**
373
+ * Phase 6: Build DOM mutations (transform-only, no reflow).
374
+ *
375
+ * CRITICAL: We use transform: translate3d() which is compositor-only.
376
+ * This means the GPU handles the positioning without triggering
377
+ * the browser's layout engine.
378
+ */
379
+ private buildDOMMutations(): DOMMutation[] {
380
+ const mutations: DOMMutation[] = [];
381
+
382
+ for (const entityId of this.frameState.dirtyEntities) {
383
+ const bounds = this.frameState.currentBounds.get(entityId);
384
+ if (!bounds) continue;
385
+
386
+ const element = this.domLayer.getElement(entityId);
387
+ if (!element) continue;
388
+
389
+ // Use translate3d for GPU acceleration
390
+ // Note: Element must have position: absolute and will-change: transform
391
+ const transform = `translate3d(${bounds.x}px, ${bounds.y}px, 0)`;
392
+
393
+ mutations.push({
394
+ entityId,
395
+ element,
396
+ transform,
397
+ });
398
+ }
399
+
400
+ return mutations;
401
+ }
402
+
403
+ /**
404
+ * Phase 7: Atomic commit of Canvas and DOM updates.
405
+ *
406
+ * This function guarantees that by the time rAF callback returns:
407
+ * 1. All wgpu draw commands have been flushed to GPU
408
+ * 2. All DOM transforms have been written
409
+ * 3. Browser's compositor will see consistent state
410
+ *
411
+ * ## Strict DOM Proxy Enforcement
412
+ *
413
+ * DOM elements are initialized with `initializeDOMProxyElement()` which
414
+ * installs a Proxy guard. Only `transform` can be modified here.
415
+ */
416
+ private commitFrame(drawCommands: DrawCommand[], domMutations: DOMMutation[]): void {
417
+ // --- Canvas Commit ---
418
+ // GPU renderer batches internally; we just need to issue commands and flush
419
+ for (const cmd of drawCommands) {
420
+ this.canvasRenderer.draw(cmd);
421
+ }
422
+ this.canvasRenderer.flush();
423
+
424
+ // --- DOM Commit (Single Write Batch) ---
425
+ // Using updateDOMProxyTransform which is the only allowed mutation
426
+ for (const mutation of domMutations) {
427
+ updateDOMProxyTransform(
428
+ mutation.element,
429
+ this.frameState.currentBounds.get(mutation.entityId)?.x ?? 0,
430
+ this.frameState.currentBounds.get(mutation.entityId)?.y ?? 0,
431
+ );
432
+ }
433
+
434
+ // No reads after writes in this function = no reflow triggered
435
+ }
436
+
437
+ /**
438
+ * Swap frame buffers for next frame's diff.
439
+ */
440
+ private swapBuffers(): void {
441
+ this.frameState.previousBounds = new Map(this.frameState.currentBounds);
442
+ }
443
+ }
444
+
445
+ // =============================================================================
446
+ // Helper Functions
447
+ // =============================================================================
448
+
449
+ function boundsEqual(a: RasterBounds, b: RasterBounds): boolean {
450
+ return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;
451
+ }
452
+
453
+ // =============================================================================
454
+ // Dependency Interfaces (to be implemented by other modules)
455
+ // =============================================================================
456
+
457
+ /**
458
+ * Constraint solver interface.
459
+ *
460
+ * ## P/Q Boundary Enforcement
461
+ *
462
+ * The solver receives T-vector STATE mutations (hover, scroll_y, etc.)
463
+ * and returns P-dimension SPATIAL coordinates (X, Y, Z).
464
+ *
465
+ * The solver is the ONLY component that derives spatial coordinates.
466
+ * It does so by evaluating constraints of the form:
467
+ *
468
+ * A.x = 100 when A.T.hover = 1
469
+ * A.x = 0 when A.T.hover = 0
470
+ *
471
+ * This ensures the constraint graph is the single source of truth.
472
+ */
473
+ interface ConstraintSolver {
474
+ evaluate(mutations: TStateMutation[]): Map<EntityId, PVectorBounds>;
475
+ }
476
+
477
+ interface TopologyRounder {
478
+ round(bounds: Map<EntityId, PVectorBounds>): {
479
+ bounds: Map<EntityId, RasterBounds>;
480
+ violations: unknown[];
481
+ };
482
+ }
483
+
484
+ interface CanvasRenderer {
485
+ getEntity(id: EntityId): RenderableEntity | undefined;
486
+ draw(command: DrawCommand): void;
487
+ flush(): void;
488
+ }
489
+
490
+ interface DOMLayer {
491
+ getElement(id: EntityId): HTMLElement | undefined;
492
+ initializeProxyElement(id: EntityId, element: HTMLElement, bounds: RasterBounds): void;
493
+ }
494
+
495
+ /**
496
+ * Event buffer interface for async event atomicity.
497
+ *
498
+ * The render loop calls mergeAsyncEvents() at the start of each tick
499
+ * to integrate events from async callbacks (fetch, setTimeout, etc.)
500
+ * before processing sync events.
501
+ */
502
+ interface EventBufferInterface {
503
+ /**
504
+ * Merge pending async events into the main buffers.
505
+ *
506
+ * Called at tick start to ensure deterministic event ordering.
507
+ */
508
+ mergeAsyncEvents(): void;
509
+ }
510
+
511
+ // =============================================================================
512
+ // DOM Proxy Enforcement (Architect Directive: Strict DOM Proxy)
513
+ // =============================================================================
514
+
515
+ /**
516
+ * CSS properties allowed on DOM proxy elements.
517
+ *
518
+ * These are compositor-only properties that do NOT trigger browser reflow.
519
+ * Any attempt to set other properties (margin, padding, display, etc.)
520
+ * will throw a runtime error.
521
+ */
522
+ const ALLOWED_CSS_PROPERTIES = new Set([
523
+ 'transform',
524
+ 'opacity',
525
+ 'pointerEvents',
526
+ 'pointer-events', // Allow both camelCase and kebab-case
527
+ 'willChange',
528
+ 'will-change',
529
+ ]);
530
+
531
+ /**
532
+ * Initialize a DOM element as a strict ViewScript proxy.
533
+ *
534
+ * This function:
535
+ * 1. Sets all required CSS properties for compositor-only updates
536
+ * 2. Wraps the style object in a Proxy to prevent layout-triggering mutations
537
+ * 3. Ensures the element is a "transparent tactile proxy" with no visual rendering
538
+ *
539
+ * ## Architect Directive: Zero Layout Engine Intervention
540
+ *
541
+ * The browser's layout engine (Reflow) must NEVER be invoked by DOM proxy
542
+ * elements. All positioning is done via GPU-accelerated `transform: translate3d()`.
543
+ */
544
+ export function initializeDOMProxyElement(
545
+ element: HTMLElement,
546
+ bounds: RasterBounds,
547
+ ): void {
548
+ // Store original style object for Proxy wrapping
549
+ const originalStyle = element.style;
550
+
551
+ // Apply mandatory CSS for compositor-only updates
552
+ originalStyle.position = 'absolute';
553
+ originalStyle.top = '0';
554
+ originalStyle.left = '0';
555
+ originalStyle.width = `${bounds.width}px`;
556
+ originalStyle.height = `${bounds.height}px`;
557
+ originalStyle.opacity = '0'; // Visually transparent
558
+ originalStyle.pointerEvents = 'auto'; // Tactile proxy active
559
+ originalStyle.willChange = 'transform'; // GPU layer hint
560
+ originalStyle.margin = '0'; // Explicit zero
561
+ originalStyle.padding = '0'; // Explicit zero
562
+ originalStyle.border = 'none'; // Explicit none
563
+ originalStyle.boxSizing = 'border-box'; // Predictable sizing
564
+ originalStyle.overflow = 'hidden'; // No scrollbars
565
+ originalStyle.transform = `translate3d(${bounds.x}px, ${bounds.y}px, 0)`;
566
+
567
+ // Create a Proxy to guard against layout-triggering mutations
568
+ const guardedStyle = new Proxy(originalStyle, {
569
+ set(target, property: string, value: string): boolean {
570
+ // Allow setting allowed properties
571
+ if (ALLOWED_CSS_PROPERTIES.has(property)) {
572
+ return Reflect.set(target, property, value);
573
+ }
574
+
575
+ // Block all other properties with a clear error
576
+ throw new Error(
577
+ `[ViewScript DOM Proxy Violation] Cannot set CSS property '${property}' on DOM proxy element. ` +
578
+ `Only compositor-only properties are allowed: ${[...ALLOWED_CSS_PROPERTIES].join(', ')}. ` +
579
+ `This restriction prevents browser layout engine intervention.`
580
+ );
581
+ },
582
+
583
+ get(target, property: string): unknown {
584
+ return Reflect.get(target, property);
585
+ },
586
+ });
587
+
588
+ // Replace the element's style with the guarded proxy
589
+ // Note: We can't directly assign to element.style, but we can use defineProperty
590
+ Object.defineProperty(element, 'style', {
591
+ value: guardedStyle,
592
+ writable: false,
593
+ configurable: false,
594
+ });
595
+ }
596
+
597
+ /**
598
+ * Update a DOM proxy element's transform (position).
599
+ *
600
+ * This is the ONLY way to change a proxy element's position after initialization.
601
+ * It uses compositor-only transform, avoiding layout recalculation.
602
+ */
603
+ export function updateDOMProxyTransform(
604
+ element: HTMLElement,
605
+ x: number,
606
+ y: number,
607
+ ): void {
608
+ // Direct property access bypasses our Proxy guard (intentionally)
609
+ // because this is an internal, trusted call
610
+ (element as any).style.transform = `translate3d(${x}px, ${y}px, 0)`;
611
+ }
612
+
613
+ // =============================================================================
614
+ // Control Flow Summary (for Architect)
615
+ // =============================================================================
616
+
617
+ /**
618
+ * ## tick() Control Flow (Pseudocode)
619
+ *
620
+ * ```
621
+ * function tick(timestamp):
622
+ * // 1. Backpressure: Take at most N mutations from queue
623
+ * mutations = pendingMutations.splice(0, MAX_PER_FRAME)
624
+ *
625
+ * // 2. P-dimension: Solve constraints with new T-vector values
626
+ * pBounds = constraintSolver.evaluate(mutations)
627
+ *
628
+ * // 3. Rasterize: Rational → Integer with topology preservation
629
+ * rBounds = topologyRounder.round(pBounds)
630
+ *
631
+ * // 4. Diff: Find entities that changed since last frame
632
+ * dirtySet = diff(previousBounds, rBounds)
633
+ *
634
+ * // 5. Canvas: Build draw commands for dirty entities only
635
+ * drawCmds = buildDrawCommands(dirtySet)
636
+ *
637
+ * // 6. DOM: Build transform mutations (no width/height!)
638
+ * domMuts = buildDOMMutations(dirtySet)
639
+ *
640
+ * // 7. ATOMIC COMMIT (no interleaved reads/writes)
641
+ * for cmd in drawCmds: canvasRenderer.draw(cmd)
642
+ * canvasRenderer.flush() // GPU sync point
643
+ * for mut in domMuts: mut.element.style.transform = mut.transform
644
+ *
645
+ * // 8. Swap buffers
646
+ * previousBounds = rBounds
647
+ *
648
+ * // 9. Schedule next frame
649
+ * requestAnimationFrame(tick)
650
+ * ```
651
+ *
652
+ * ## Reflow Prevention Strategy
653
+ *
654
+ * 1. DOM elements are created once with fixed dimensions (via CSS)
655
+ * 2. Position changes use ONLY transform: translate3d()
656
+ * 3. translate3d() is compositor-only (GPU, no layout recalc)
657
+ * 4. will-change: transform hints browser to promote layer
658
+ * 5. All style writes happen before any reads (no thrashing)
659
+ * 6. Size changes require re-creating the element (rare)
660
+ */