@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.
- package/dist/ast/types.d.ts +403 -0
- package/dist/ast/types.js +33 -0
- package/dist/compiler/chunk-splitter.d.ts +98 -0
- package/dist/compiler/chunk-splitter.js +361 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.js +17 -0
- package/dist/rasterizer/__tests__/error-distribution.test.d.ts +7 -0
- package/dist/rasterizer/__tests__/error-distribution.test.js +322 -0
- package/dist/rasterizer/canvas-mapper.d.ts +280 -0
- package/dist/rasterizer/canvas-mapper.js +414 -0
- package/dist/rasterizer/error-distribution.d.ts +143 -0
- package/dist/rasterizer/error-distribution.js +231 -0
- package/dist/rasterizer/gradient-mapper.d.ts +223 -0
- package/dist/rasterizer/gradient-mapper.js +352 -0
- package/dist/rasterizer/topology-rounding.d.ts +151 -0
- package/dist/rasterizer/topology-rounding.js +347 -0
- package/dist/runtime/__tests__/event-backpressure.test.d.ts +10 -0
- package/dist/runtime/__tests__/event-backpressure.test.js +190 -0
- package/dist/runtime/event-backpressure.d.ts +393 -0
- package/dist/runtime/event-backpressure.js +458 -0
- package/dist/runtime/render-loop.d.ts +277 -0
- package/dist/runtime/render-loop.js +435 -0
- package/dist/runtime/wasm-resource-manager.d.ts +122 -0
- package/dist/runtime/wasm-resource-manager.js +253 -0
- package/dist/runtime/wgpu-renderer-adapter.d.ts +168 -0
- package/dist/runtime/wgpu-renderer-adapter.js +230 -0
- package/dist/semantic/__tests__/semantic-translator.test.d.ts +4 -0
- package/dist/semantic/__tests__/semantic-translator.test.js +203 -0
- package/dist/semantic/semantic-translator.d.ts +229 -0
- package/dist/semantic/semantic-translator.js +398 -0
- package/package.json +28 -0
- package/playwright-report/data/0bafe4e0863f0e244bba68a838f73241f8f2efaa.md +226 -0
- package/playwright-report/data/9281aca8abfb06c6cecb35d5ddd13d61f8c752d8.md +226 -0
- package/playwright-report/index.html +90 -0
- package/playwright.config.ts +160 -0
- package/screenshot-chrome.png +0 -0
- package/screenshots/visual-demo-verification.png +0 -0
- package/screenshots/visual-demo.png +0 -0
- package/src/ast/types.ts +473 -0
- package/src/compiler/chunk-splitter.ts +534 -0
- package/src/index.ts +62 -0
- package/src/rasterizer/__tests__/error-distribution.test.ts +382 -0
- package/src/rasterizer/canvas-mapper.ts +677 -0
- package/src/rasterizer/error-distribution.ts +344 -0
- package/src/rasterizer/gradient-mapper.ts +563 -0
- package/src/rasterizer/topology-rounding.ts +499 -0
- package/src/runtime/__tests__/event-backpressure.test.ts +254 -0
- package/src/runtime/event-backpressure.ts +622 -0
- package/src/runtime/render-loop.ts +660 -0
- package/src/runtime/wasm-resource-manager.ts +349 -0
- package/src/runtime/wgpu-renderer-adapter.ts +318 -0
- package/src/semantic/__tests__/semantic-translator.test.ts +263 -0
- package/src/semantic/semantic-translator.ts +637 -0
- package/test-results/.last-run.json +4 -0
- package/tests/e2e/async-race.spec.ts +612 -0
- package/tests/e2e/bilayer-sync.spec.ts +405 -0
- package/tests/e2e/failures/.gitkeep +0 -0
- package/tests/e2e/fullstack.spec.ts +681 -0
- package/tests/e2e/g1-continuity.spec.ts +703 -0
- package/tests/e2e/golden/.gitkeep +0 -0
- package/tests/e2e/golden/conic-color-wheel.raw +0 -0
- package/tests/e2e/golden/conic-color-wheel.sha256 +1 -0
- package/tests/e2e/golden/conic-rotated.raw +0 -0
- package/tests/e2e/golden/conic-rotated.sha256 +1 -0
- package/tests/e2e/golden/linear-45deg.raw +0 -0
- package/tests/e2e/golden/linear-45deg.sha256 +1 -0
- package/tests/e2e/golden/linear-horizontal.raw +0 -0
- package/tests/e2e/golden/linear-horizontal.sha256 +1 -0
- package/tests/e2e/golden/linear-multi-stop.raw +0 -0
- package/tests/e2e/golden/linear-multi-stop.sha256 +1 -0
- package/tests/e2e/golden/radial-circle-center.raw +0 -0
- package/tests/e2e/golden/radial-circle-center.sha256 +1 -0
- package/tests/e2e/golden/radial-offset.raw +0 -0
- package/tests/e2e/golden/radial-offset.sha256 +1 -0
- package/tests/e2e/golden/tile-mirror.raw +0 -0
- package/tests/e2e/golden/tile-mirror.sha256 +1 -0
- package/tests/e2e/golden/tile-repeat.raw +0 -0
- package/tests/e2e/golden/tile-repeat.sha256 +1 -0
- package/tests/e2e/gradient-animation.spec.ts +606 -0
- package/tests/e2e/memory-stability.spec.ts +396 -0
- package/tests/e2e/path-topology.spec.ts +674 -0
- package/tests/e2e/performance-profile.spec.ts +501 -0
- package/tests/e2e/screenshot.spec.ts +60 -0
- package/tests/e2e/test-harness.html +1005 -0
- package/tests/e2e/text-layout.spec.ts +451 -0
- package/tests/e2e/visual-demo.html +340 -0
- package/tests/e2e/visual-regression.spec.ts +335 -0
- package/tsconfig.json +12 -0
- 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
|
+
*/
|