@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,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
|
+
*/
|