@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,622 @@
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
+
27
+ import type { EntityId } from '../ast/types';
28
+
29
+ // =============================================================================
30
+ // Types
31
+ // =============================================================================
32
+
33
+ /**
34
+ * Semantic T-vector state keys.
35
+ *
36
+ * Q-dimension events MUST target these state keys, NOT spatial coordinates.
37
+ * P-dimension spatial coordinates (X, Y, Z) are derived from constraints
38
+ * that reference T-vector state, never directly from Q-dimension input.
39
+ *
40
+ * ## The Ouroboros Prevention Principle
41
+ *
42
+ * If Q-dimension input (e.g., MouseEvent.clientX) were allowed to directly
43
+ * mutate P-dimension spatial coordinates, it would create a self-referential
44
+ * loop: mouse position → entity position → canvas render → mouse position...
45
+ *
46
+ * Instead, Q-dimension input mutates T-vector STATE, and the constraint
47
+ * solver derives spatial coordinates as a FUNCTION of that state.
48
+ */
49
+ export type TStateKey =
50
+ | 'hover' // Boolean: is pointer over this entity?
51
+ | 'pressed' // Boolean: is pointer pressed on this entity?
52
+ | 'focused' // Boolean: does this entity have keyboard focus?
53
+ | 'scroll_x' // Number: horizontal scroll offset (normalized 0-1)
54
+ | 'scroll_y' // Number: vertical scroll offset (normalized 0-1)
55
+ | 'drag_progress' // Number: drag gesture progress (normalized 0-1)
56
+ | 'animation_t' // Number: animation timeline position
57
+ | 'gesture_phase'; // Number: gesture recognizer phase (0=none, 1=began, 2=changed, 3=ended)
58
+
59
+ /**
60
+ * Raw Q-dimension event from DOM.
61
+ *
62
+ * CRITICAL INVARIANT: Q-dimension events target T-vector STATE keys only.
63
+ * They NEVER directly modify P-dimension spatial coordinates (X, Y, Z).
64
+ */
65
+ export interface QDimensionEvent {
66
+ /** Source entity that fired the event */
67
+ entityId: EntityId;
68
+
69
+ /** DOM event type */
70
+ eventType: QEventType;
71
+
72
+ /**
73
+ * Target T-vector state key.
74
+ *
75
+ * ARCHITECTURAL CONSTRAINT: This is a semantic state key, NOT a spatial
76
+ * coordinate. P-dimension coordinates are derived via constraint evaluation.
77
+ */
78
+ targetState: TStateKey;
79
+
80
+ /**
81
+ * State value (interpretation depends on targetState).
82
+ *
83
+ * - Boolean states: 0 = false, 1 = true
84
+ * - Normalized states: 0.0 to 1.0
85
+ * - Phase states: discrete integers
86
+ */
87
+ value: number;
88
+
89
+ /** Event timestamp (performance.now()) */
90
+ timestamp: number;
91
+
92
+ /** Priority (higher = more important) */
93
+ priority: EventPriority;
94
+ }
95
+
96
+ export type QEventType =
97
+ | 'click'
98
+ | 'pointerdown'
99
+ | 'pointerup'
100
+ | 'pointermove'
101
+ | 'scroll'
102
+ | 'wheel'
103
+ | 'keydown'
104
+ | 'keyup'
105
+ | 'focus'
106
+ | 'blur';
107
+
108
+ export enum EventPriority {
109
+ /** Immediate: click, keydown (user intent) */
110
+ CRITICAL = 3,
111
+
112
+ /** High: pointerdown/up (gesture start/end) */
113
+ HIGH = 2,
114
+
115
+ /** Normal: scroll, wheel (continuous) */
116
+ NORMAL = 1,
117
+
118
+ /** Low: pointermove (high frequency, lossy OK) */
119
+ LOW = 0,
120
+ }
121
+
122
+ /**
123
+ * Coalesced event ready for frame processing.
124
+ *
125
+ * This represents a T-vector state mutation, NOT a spatial coordinate change.
126
+ */
127
+ export interface CoalescedEvent {
128
+ entityId: EntityId;
129
+ /** The T-vector state key being mutated */
130
+ state: TStateKey;
131
+ /** The new state value */
132
+ value: number;
133
+ timestamp: number;
134
+ }
135
+
136
+ /**
137
+ * Backpressure configuration.
138
+ */
139
+ export interface BackpressureConfig {
140
+ /** Max events to process per frame */
141
+ maxEventsPerFrame: number;
142
+
143
+ /** Throttle interval for LOW priority events (ms) */
144
+ lowPriorityThrottleMs: number;
145
+
146
+ /** Enable event coalescing (latest-only for same entity+component) */
147
+ enableCoalescing: boolean;
148
+ }
149
+
150
+ // =============================================================================
151
+ // Event Buffer (Per-Entity, Per-Component Coalescing)
152
+ // =============================================================================
153
+
154
+ /**
155
+ * Key for coalescing: entityId + state key
156
+ */
157
+ type CoalesceKey = `${EntityId}:${TStateKey}`;
158
+
159
+ /**
160
+ * Double-buffered event accumulator.
161
+ *
162
+ * ## Data Structure
163
+ *
164
+ * ```
165
+ * writeBuffer (current frame events):
166
+ * Map<CoalesceKey, QDimensionEvent>
167
+ * - Key: "entity42:x"
168
+ * - Value: Latest event for that entity+component
169
+ * - Overwrites: Yes (latest-only sampling)
170
+ *
171
+ * priorityQueue (critical events):
172
+ * Array<QDimensionEvent>
173
+ * - Never coalesced (click, keydown must all fire)
174
+ * - Processed first
175
+ *
176
+ * pendingAsyncEvents (async callback events):
177
+ * Array<QDimensionEvent>
178
+ * - Events from async callbacks (fetch, setTimeout, promises)
179
+ * - Isolated from sync events to prevent race conditions
180
+ * - Merged at tick start via mergeAsyncEvents()
181
+ * ```
182
+ *
183
+ * ## Async Atomicity (Phase 2 Remediation)
184
+ *
185
+ * Events from async callbacks (fetch handlers, setTimeout, etc.) arrive
186
+ * outside the rAF tick boundary. Without isolation, they could interleave
187
+ * with sync events causing non-deterministic ordering.
188
+ *
189
+ * Solution: Async events are buffered separately and merged atomically
190
+ * at the START of each tick, before any sync event processing.
191
+ */
192
+ export class EventBuffer {
193
+ private config: BackpressureConfig;
194
+
195
+ /** Coalesced events (latest-only per entity+component) */
196
+ private writeBuffer: Map<CoalesceKey, QDimensionEvent> = new Map();
197
+
198
+ /** Non-coalesced critical events (click, keydown) */
199
+ private priorityQueue: QDimensionEvent[] = [];
200
+
201
+ /** Last event time per key (for throttling) */
202
+ private lastEventTime: Map<CoalesceKey, number> = new Map();
203
+
204
+ /**
205
+ * Pending async events (from fetch, setTimeout, promises).
206
+ *
207
+ * These are isolated from sync events and merged at tick start
208
+ * to ensure deterministic ordering.
209
+ */
210
+ private pendingAsyncEvents: QDimensionEvent[] = [];
211
+
212
+ constructor(config: Partial<BackpressureConfig> = {}) {
213
+ this.config = {
214
+ maxEventsPerFrame: 50,
215
+ lowPriorityThrottleMs: 16, // ~60fps
216
+ enableCoalescing: true,
217
+ ...config,
218
+ };
219
+ }
220
+
221
+ /**
222
+ * Push a Q-dimension event into the buffer.
223
+ *
224
+ * ## Coalescing Rules
225
+ *
226
+ * 1. CRITICAL priority: Always queued, never coalesced
227
+ * 2. HIGH/NORMAL/LOW: Coalesced by entity+state (latest wins)
228
+ * 3. LOW with throttle: Dropped if within throttle window
229
+ */
230
+ push(event: QDimensionEvent): void {
231
+ const key: CoalesceKey = `${event.entityId}:${event.targetState}`;
232
+
233
+ // Critical events bypass coalescing
234
+ if (event.priority === EventPriority.CRITICAL) {
235
+ this.priorityQueue.push(event);
236
+ return;
237
+ }
238
+
239
+ // Throttle check for low-priority events
240
+ if (event.priority === EventPriority.LOW) {
241
+ const lastTime = this.lastEventTime.get(key) ?? 0;
242
+ if (event.timestamp - lastTime < this.config.lowPriorityThrottleMs) {
243
+ // Drop: within throttle window
244
+ return;
245
+ }
246
+ }
247
+
248
+ // Coalesce: overwrite previous event for same entity+component
249
+ if (this.config.enableCoalescing) {
250
+ this.writeBuffer.set(key, event);
251
+ } else {
252
+ // No coalescing: treat as priority queue
253
+ this.priorityQueue.push(event);
254
+ }
255
+
256
+ this.lastEventTime.set(key, event.timestamp);
257
+ }
258
+
259
+ /**
260
+ * Flush buffer for frame processing.
261
+ *
262
+ * Returns events in priority order:
263
+ * 1. All CRITICAL events (in order received)
264
+ * 2. Coalesced events (limited by maxEventsPerFrame)
265
+ *
266
+ * ## Frame Alignment
267
+ *
268
+ * This method is called once per rAF tick. Events that arrive
269
+ * after flush() but before next tick accumulate in the buffer.
270
+ */
271
+ flush(): CoalescedEvent[] {
272
+ const result: CoalescedEvent[] = [];
273
+ const limit = this.config.maxEventsPerFrame;
274
+
275
+ // 1. Critical events first (never dropped)
276
+ for (const event of this.priorityQueue) {
277
+ result.push({
278
+ entityId: event.entityId,
279
+ state: event.targetState,
280
+ value: event.value,
281
+ timestamp: event.timestamp,
282
+ });
283
+ }
284
+ this.priorityQueue = [];
285
+
286
+ // 2. Coalesced events (up to limit)
287
+ const remaining = limit - result.length;
288
+ if (remaining > 0) {
289
+ const coalesced = Array.from(this.writeBuffer.values())
290
+ .sort((a, b) => b.priority - a.priority) // Higher priority first
291
+ .slice(0, remaining);
292
+
293
+ for (const event of coalesced) {
294
+ result.push({
295
+ entityId: event.entityId,
296
+ state: event.targetState,
297
+ value: event.value,
298
+ timestamp: event.timestamp,
299
+ });
300
+ }
301
+ }
302
+
303
+ // Clear coalesced buffer (events not taken are dropped)
304
+ this.writeBuffer.clear();
305
+
306
+ return result;
307
+ }
308
+
309
+ /**
310
+ * Get current buffer sizes (for debugging/metrics).
311
+ */
312
+ getStats(): { priorityQueueSize: number; coalescedSize: number; asyncPendingSize: number } {
313
+ return {
314
+ priorityQueueSize: this.priorityQueue.length,
315
+ coalescedSize: this.writeBuffer.size,
316
+ asyncPendingSize: this.pendingAsyncEvents.length,
317
+ };
318
+ }
319
+
320
+ /**
321
+ * Push an event from an async callback.
322
+ *
323
+ * Use this method for events originating from:
324
+ * - fetch() handlers
325
+ * - setTimeout / setInterval callbacks
326
+ * - Promise .then() / .catch() handlers
327
+ * - WebSocket message handlers
328
+ * - IndexedDB callbacks
329
+ * - Any other async context
330
+ *
331
+ * ## Why Separate From push()?
332
+ *
333
+ * Async callbacks can fire at any time, potentially mid-tick or between
334
+ * ticks. If mixed with sync events without ordering guarantees, the result
335
+ * is non-deterministic constraint evaluation.
336
+ *
337
+ * By isolating async events, we ensure:
338
+ * 1. All async events are processed in FIFO order
339
+ * 2. They are merged BEFORE sync events at tick start
340
+ * 3. The tick sees a consistent snapshot of async state
341
+ *
342
+ * ## Example
343
+ *
344
+ * ```typescript
345
+ * fetch('/api/data').then(response => {
346
+ * // WRONG: buffer.push(event) - could race with sync events
347
+ * // CORRECT: buffer.pushAsync(event) - isolated until tick start
348
+ * buffer.pushAsync({
349
+ * entityId: 42,
350
+ * eventType: 'custom',
351
+ * targetState: 'animation_t',
352
+ * value: response.progress,
353
+ * timestamp: performance.now(),
354
+ * priority: EventPriority.NORMAL,
355
+ * });
356
+ * });
357
+ * ```
358
+ */
359
+ pushAsync(event: QDimensionEvent): void {
360
+ this.pendingAsyncEvents.push(event);
361
+ }
362
+
363
+ /**
364
+ * Merge pending async events into the main buffers.
365
+ *
366
+ * MUST be called at the START of each tick, BEFORE flush().
367
+ *
368
+ * This ensures:
369
+ * 1. Async events are processed before sync events from the same frame
370
+ * 2. No async events can arrive mid-flush (atomicity)
371
+ * 3. Deterministic ordering: async (FIFO) → sync (coalesced/priority)
372
+ *
373
+ * ## Call Site
374
+ *
375
+ * AtomicRenderLoop.tick():
376
+ * ```typescript
377
+ * function tick(timestamp) {
378
+ * // FIRST: Merge async events atomically
379
+ * eventBuffer.mergeAsyncEvents();
380
+ *
381
+ * // THEN: Flush and process all events
382
+ * const mutations = eventBuffer.flush();
383
+ * // ... rest of tick
384
+ * }
385
+ * ```
386
+ */
387
+ mergeAsyncEvents(): void {
388
+ if (this.pendingAsyncEvents.length === 0) {
389
+ return;
390
+ }
391
+
392
+ // Process async events through the normal push() pipeline
393
+ // This applies coalescing and priority rules consistently
394
+ for (const event of this.pendingAsyncEvents) {
395
+ this.push(event);
396
+ }
397
+
398
+ // Clear the async buffer
399
+ this.pendingAsyncEvents = [];
400
+ }
401
+ }
402
+
403
+ // =============================================================================
404
+ // Event Controller (DOM Binding Layer)
405
+ // =============================================================================
406
+
407
+ /**
408
+ * DOM event controller with automatic backpressure.
409
+ *
410
+ * ## Data Flow
411
+ *
412
+ * ```
413
+ * DOM Event (mousemove)
414
+ * │
415
+ * ▼
416
+ * EventController.handleEvent()
417
+ * │
418
+ * ├─▶ Compute T-vector value (from event data)
419
+ * │
420
+ * ├─▶ Wrap as QDimensionEvent
421
+ * │
422
+ * ▼
423
+ * EventBuffer.push()
424
+ * │
425
+ * ├─▶ Throttle check (LOW priority)
426
+ * │
427
+ * ├─▶ Coalesce (latest-only)
428
+ * │
429
+ * ▼
430
+ * Buffer accumulates until next rAF
431
+ * │
432
+ * ▼
433
+ * AtomicRenderLoop.tick()
434
+ * │
435
+ * ├─▶ EventBuffer.flush()
436
+ * │
437
+ * ▼
438
+ * CoalescedEvent[] → ConstraintSolver
439
+ * ```
440
+ */
441
+ export class EventController {
442
+ private buffer: EventBuffer;
443
+ private entityElements: Map<EntityId, HTMLElement> = new Map();
444
+ private boundHandlers: Map<string, EventListener> = new Map();
445
+
446
+ constructor(buffer: EventBuffer) {
447
+ this.buffer = buffer;
448
+ }
449
+
450
+ /**
451
+ * Register a DOM element for event handling.
452
+ */
453
+ registerElement(entityId: EntityId, element: HTMLElement): void {
454
+ this.entityElements.set(entityId, element);
455
+ }
456
+
457
+ /**
458
+ * Bind an event type to a T-vector state key.
459
+ *
460
+ * ## Ouroboros Prevention
461
+ *
462
+ * The valueMapper function MUST return a SEMANTIC state value, not a
463
+ * raw spatial coordinate. For example:
464
+ *
465
+ * CORRECT (semantic state):
466
+ * - `'hover'` → (e) => e.type === 'pointerenter' ? 1 : 0
467
+ * - `'scroll_y'` → (e) => e.target.scrollTop / e.target.scrollHeight
468
+ * - `'drag_progress'` → (e) => computeNormalizedDragProgress(e)
469
+ *
470
+ * FORBIDDEN (spatial coordinate - would violate P/Q boundary):
471
+ * - `'x'` → (e) => e.clientX // NEVER DO THIS
472
+ * - `'y'` → (e) => e.clientY // NEVER DO THIS
473
+ *
474
+ * @param entityId - Entity to bind the event to
475
+ * @param eventType - DOM event type
476
+ * @param targetState - T-vector state key (semantic, not spatial)
477
+ * @param valueMapper - Function to compute state value from DOM event
478
+ */
479
+ bindEvent(
480
+ entityId: EntityId,
481
+ eventType: QEventType,
482
+ targetState: TStateKey,
483
+ valueMapper: (event: Event) => number,
484
+ ): void {
485
+ const element = this.entityElements.get(entityId);
486
+ if (!element) return;
487
+
488
+ const handler = (domEvent: Event) => {
489
+ const qEvent: QDimensionEvent = {
490
+ entityId,
491
+ eventType,
492
+ targetState,
493
+ value: valueMapper(domEvent),
494
+ timestamp: performance.now(),
495
+ priority: this.getPriority(eventType),
496
+ };
497
+
498
+ this.buffer.push(qEvent);
499
+ };
500
+
501
+ const key = `${entityId}:${eventType}`;
502
+ this.boundHandlers.set(key, handler);
503
+
504
+ // Use passive listeners where possible (scroll, wheel, pointermove)
505
+ const passive = ['scroll', 'wheel', 'pointermove'].includes(eventType);
506
+ element.addEventListener(eventType, handler, { passive });
507
+ }
508
+
509
+ /**
510
+ * Unbind all events for an entity.
511
+ */
512
+ unbindEntity(entityId: EntityId): void {
513
+ const element = this.entityElements.get(entityId);
514
+ if (!element) return;
515
+
516
+ for (const [key, handler] of this.boundHandlers) {
517
+ if (key.startsWith(`${entityId}:`)) {
518
+ const eventType = key.split(':')[1];
519
+ element.removeEventListener(eventType, handler);
520
+ this.boundHandlers.delete(key);
521
+ }
522
+ }
523
+
524
+ this.entityElements.delete(entityId);
525
+ }
526
+
527
+ /**
528
+ * Map event type to priority.
529
+ */
530
+ private getPriority(eventType: QEventType): EventPriority {
531
+ switch (eventType) {
532
+ case 'click':
533
+ case 'keydown':
534
+ case 'keyup':
535
+ return EventPriority.CRITICAL;
536
+
537
+ case 'pointerdown':
538
+ case 'pointerup':
539
+ case 'focus':
540
+ case 'blur':
541
+ return EventPriority.HIGH;
542
+
543
+ case 'scroll':
544
+ case 'wheel':
545
+ return EventPriority.NORMAL;
546
+
547
+ case 'pointermove':
548
+ return EventPriority.LOW;
549
+
550
+ default:
551
+ return EventPriority.NORMAL;
552
+ }
553
+ }
554
+ }
555
+
556
+ // =============================================================================
557
+ // Usage Example
558
+ // =============================================================================
559
+
560
+ /**
561
+ * ## Integration with Render Loop
562
+ *
563
+ * ```typescript
564
+ * const buffer = new EventBuffer({ maxEventsPerFrame: 50 });
565
+ * const controller = new EventController(buffer);
566
+ *
567
+ * // Register entity's DOM element
568
+ * controller.registerElement(entity.id, domElement);
569
+ *
570
+ * // CORRECT: Bind semantic state (hover) to T-vector
571
+ * controller.bindEvent(
572
+ * entity.id,
573
+ * 'pointerenter',
574
+ * 'hover',
575
+ * () => 1 // hover = true
576
+ * );
577
+ * controller.bindEvent(
578
+ * entity.id,
579
+ * 'pointerleave',
580
+ * 'hover',
581
+ * () => 0 // hover = false
582
+ * );
583
+ *
584
+ * // CORRECT: Bind normalized scroll position
585
+ * controller.bindEvent(
586
+ * entity.id,
587
+ * 'scroll',
588
+ * 'scroll_y',
589
+ * (e: Event) => {
590
+ * const target = e.target as HTMLElement;
591
+ * const maxScroll = target.scrollHeight - target.clientHeight;
592
+ * return maxScroll > 0 ? target.scrollTop / maxScroll : 0;
593
+ * }
594
+ * );
595
+ *
596
+ * // In render loop tick():
597
+ * const events = buffer.flush();
598
+ *
599
+ * // Events are T-vector STATE mutations, not spatial coordinates
600
+ * // The constraint solver evaluates P-dimension coordinates AS A FUNCTION
601
+ * // of T-vector state (via constraints like: A.x = 100 when T.hover = 1)
602
+ * const stateMutations = events.map(e => ({
603
+ * entityId: e.entityId,
604
+ * state: e.state, // Semantic state key (hover, scroll_y, etc.)
605
+ * value: e.value, // State value
606
+ * timestamp: e.timestamp,
607
+ * }));
608
+ *
609
+ * // Constraint solver derives X, Y, Z from T-vector state
610
+ * constraintSolver.evaluateWithTVector(stateMutations);
611
+ * ```
612
+ *
613
+ * ## Architectural Guarantee: P/Q Boundary Preservation
614
+ *
615
+ * By restricting Q-dimension events to T-vector STATE keys (not spatial
616
+ * coordinates), we ensure that:
617
+ *
618
+ * 1. Mouse coordinates (clientX/Y) are NEVER directly assigned to P-dimension
619
+ * 2. P-dimension coordinates are always derived via constraint evaluation
620
+ * 3. The constraint graph is the SINGLE SOURCE OF TRUTH for spatial layout
621
+ * 4. LEAN 4 decidability proofs remain valid (no floating-point pollution)
622
+ */