@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,534 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chunk Splitting Strategy for Progressive Loading
|
|
3
|
+
*
|
|
4
|
+
* This module implements static analysis to partition the constraint graph
|
|
5
|
+
* into Initial and Lazy chunks, enabling Qwik/Wiz-style progressive hydration.
|
|
6
|
+
*
|
|
7
|
+
* ## Core Principle: T-Vector Reachability Analysis
|
|
8
|
+
*
|
|
9
|
+
* An entity belongs to the Initial Chunk if and only if:
|
|
10
|
+
* 1. It is visible at T=0 (initial render time), AND
|
|
11
|
+
* 2. It is reachable from the viewport bounds at T=0
|
|
12
|
+
*
|
|
13
|
+
* An entity belongs to a Lazy Chunk if:
|
|
14
|
+
* 1. It becomes visible only after T changes (user interaction), OR
|
|
15
|
+
* 2. It is outside the initial viewport (below the fold), OR
|
|
16
|
+
* 3. It depends on Q-dimension data that hasn't loaded yet
|
|
17
|
+
*
|
|
18
|
+
* ## Algorithm Overview
|
|
19
|
+
*
|
|
20
|
+
* ```
|
|
21
|
+
* INPUT: Constraint Graph G = (Entities, Constraints)
|
|
22
|
+
* Viewport V = (width, height)
|
|
23
|
+
* Initial Time T₀ = 0
|
|
24
|
+
*
|
|
25
|
+
* OUTPUT: Partition P = { Chunk₀ (initial), Chunk₁, Chunk₂, ... }
|
|
26
|
+
*
|
|
27
|
+
* ALGORITHM:
|
|
28
|
+
* 1. Evaluate all constraints at T=T₀ to get initial positions
|
|
29
|
+
* 2. Mark entities intersecting V as "initially visible"
|
|
30
|
+
* 3. For each Q-dimension binding (event handler):
|
|
31
|
+
* a. Trace which constraints are affected when event fires
|
|
32
|
+
* b. Group affected entities into event-specific lazy chunks
|
|
33
|
+
* 4. For entities below the fold:
|
|
34
|
+
* a. Create viewport-intersection lazy chunks
|
|
35
|
+
* 5. Compute chunk dependency DAG
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import type {
|
|
40
|
+
EntityId,
|
|
41
|
+
ConstraintId,
|
|
42
|
+
ChunkId,
|
|
43
|
+
Chunk,
|
|
44
|
+
LoadTrigger,
|
|
45
|
+
RenderableEntity,
|
|
46
|
+
Rational,
|
|
47
|
+
PVectorBounds,
|
|
48
|
+
} from '../ast/types';
|
|
49
|
+
|
|
50
|
+
// =============================================================================
|
|
51
|
+
// Input Types (from IR)
|
|
52
|
+
// =============================================================================
|
|
53
|
+
|
|
54
|
+
interface IRConstraint {
|
|
55
|
+
id: ConstraintId;
|
|
56
|
+
target: EntityId;
|
|
57
|
+
component: 'x' | 'y' | 'z' | 't';
|
|
58
|
+
relation: 'eq' | 'lt' | 'le' | 'gt' | 'ge';
|
|
59
|
+
term: IRConstraintTerm;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
type IRConstraintTerm =
|
|
63
|
+
| { type: 'const'; value: Rational }
|
|
64
|
+
| { type: 'ref'; entityId: EntityId; component: 'x' | 'y' | 'z' | 't' }
|
|
65
|
+
| { type: 'linear'; coefficient: Rational; entityId: EntityId; component: 'x' | 'y' | 'z' | 't'; offset: Rational };
|
|
66
|
+
|
|
67
|
+
interface IRModule {
|
|
68
|
+
entities: EntityId[];
|
|
69
|
+
constraints: IRConstraint[];
|
|
70
|
+
eventBindings: EventBinding[];
|
|
71
|
+
imports: IRImport[];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface EventBinding {
|
|
75
|
+
sourceEntity: EntityId;
|
|
76
|
+
eventType: string;
|
|
77
|
+
targetConstraint: ConstraintId;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
interface IRImport {
|
|
81
|
+
path: string;
|
|
82
|
+
exportedEntities: EntityId[];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
interface ViewportBounds {
|
|
86
|
+
x: number;
|
|
87
|
+
y: number;
|
|
88
|
+
width: number;
|
|
89
|
+
height: number;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// =============================================================================
|
|
93
|
+
// Chunk Splitting Algorithm
|
|
94
|
+
// =============================================================================
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Static analysis result for chunk splitting.
|
|
98
|
+
*/
|
|
99
|
+
export interface ChunkSplitResult {
|
|
100
|
+
/** The initial chunk (loaded immediately) */
|
|
101
|
+
initialChunk: Chunk;
|
|
102
|
+
|
|
103
|
+
/** Lazy chunks (loaded on demand) */
|
|
104
|
+
lazyChunks: Chunk[];
|
|
105
|
+
|
|
106
|
+
/** Mapping from entity to chunk */
|
|
107
|
+
entityToChunk: Map<EntityId, ChunkId>;
|
|
108
|
+
|
|
109
|
+
/** Chunk dependency DAG */
|
|
110
|
+
chunkDependencies: Map<ChunkId, Set<ChunkId>>;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Rule 1: Initial Visibility Rule
|
|
115
|
+
*
|
|
116
|
+
* An entity E is initially visible iff:
|
|
117
|
+
* ∃ constraint C where C.target = E ∧ C.component ∈ {x, y} ∧
|
|
118
|
+
* eval(C, T=0) produces a position within viewport bounds
|
|
119
|
+
*
|
|
120
|
+
* Pseudocode:
|
|
121
|
+
* ```
|
|
122
|
+
* function isInitiallyVisible(entity, constraints, viewport):
|
|
123
|
+
* position = evaluatePosition(entity, constraints, T=0)
|
|
124
|
+
* return intersects(position.bounds, viewport)
|
|
125
|
+
* ```
|
|
126
|
+
*/
|
|
127
|
+
function computeInitiallyVisibleEntities(
|
|
128
|
+
ir: IRModule,
|
|
129
|
+
viewport: ViewportBounds,
|
|
130
|
+
): Set<EntityId> {
|
|
131
|
+
const visible = new Set<EntityId>();
|
|
132
|
+
|
|
133
|
+
// Step 1: Evaluate all constraints at T=0
|
|
134
|
+
const positions = evaluateConstraintsAtT0(ir.constraints);
|
|
135
|
+
|
|
136
|
+
// Step 2: Check intersection with viewport
|
|
137
|
+
for (const entityId of ir.entities) {
|
|
138
|
+
const pos = positions.get(entityId);
|
|
139
|
+
if (pos && intersectsViewport(pos, viewport)) {
|
|
140
|
+
visible.add(entityId);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Step 3: Include entities referenced by visible entities (transitive closure)
|
|
145
|
+
let changed = true;
|
|
146
|
+
while (changed) {
|
|
147
|
+
changed = false;
|
|
148
|
+
for (const entityId of visible) {
|
|
149
|
+
const refs = getReferencedEntities(entityId, ir.constraints);
|
|
150
|
+
for (const ref of refs) {
|
|
151
|
+
if (!visible.has(ref)) {
|
|
152
|
+
visible.add(ref);
|
|
153
|
+
changed = true;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return visible;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Rule 2: Event-Triggered Lazy Chunk Rule
|
|
164
|
+
*
|
|
165
|
+
* When an event E triggers constraint C, all entities affected by C
|
|
166
|
+
* (directly or transitively) form a lazy chunk.
|
|
167
|
+
*
|
|
168
|
+
* Affected entities are computed via constraint dependency analysis:
|
|
169
|
+
* ```
|
|
170
|
+
* function getAffectedEntities(constraint):
|
|
171
|
+
* affected = {constraint.target}
|
|
172
|
+
* for each constraint C' that references constraint.target:
|
|
173
|
+
* affected = affected ∪ getAffectedEntities(C')
|
|
174
|
+
* return affected
|
|
175
|
+
* ```
|
|
176
|
+
*
|
|
177
|
+
* Q-dimension involvement:
|
|
178
|
+
* - Events ARE Q-dimension (user input is unpredictable)
|
|
179
|
+
* - Event handlers define the boundary between Initial and Lazy
|
|
180
|
+
* - Each unique event source creates a potential chunk boundary
|
|
181
|
+
*/
|
|
182
|
+
function computeEventTriggeredChunks(
|
|
183
|
+
ir: IRModule,
|
|
184
|
+
initiallyVisible: Set<EntityId>,
|
|
185
|
+
): Map<string, Set<EntityId>> {
|
|
186
|
+
const eventChunks = new Map<string, Set<EntityId>>();
|
|
187
|
+
|
|
188
|
+
for (const binding of ir.eventBindings) {
|
|
189
|
+
const chunkKey = `event:${binding.sourceEntity}:${binding.eventType}`;
|
|
190
|
+
|
|
191
|
+
// Find the constraint being modified
|
|
192
|
+
const targetConstraint = ir.constraints.find(c => c.id === binding.targetConstraint);
|
|
193
|
+
if (!targetConstraint) continue;
|
|
194
|
+
|
|
195
|
+
// Compute transitively affected entities
|
|
196
|
+
const affected = computeAffectedEntities(targetConstraint.target, ir.constraints);
|
|
197
|
+
|
|
198
|
+
// Filter to entities NOT in initial chunk
|
|
199
|
+
const lazyEntities = new Set<EntityId>();
|
|
200
|
+
for (const entityId of affected) {
|
|
201
|
+
if (!initiallyVisible.has(entityId)) {
|
|
202
|
+
lazyEntities.add(entityId);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (lazyEntities.size > 0) {
|
|
207
|
+
eventChunks.set(chunkKey, lazyEntities);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return eventChunks;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Rule 3: Below-the-Fold Lazy Chunk Rule
|
|
216
|
+
*
|
|
217
|
+
* Entities outside the initial viewport form viewport-intersection chunks.
|
|
218
|
+
* These are loaded when the user scrolls them into view.
|
|
219
|
+
*
|
|
220
|
+
* Chunking strategy:
|
|
221
|
+
* - Divide the below-fold area into horizontal bands
|
|
222
|
+
* - Each band forms a separate chunk
|
|
223
|
+
* - Chunk loading is triggered by Intersection Observer
|
|
224
|
+
*/
|
|
225
|
+
function computeViewportIntersectionChunks(
|
|
226
|
+
ir: IRModule,
|
|
227
|
+
viewport: ViewportBounds,
|
|
228
|
+
initiallyVisible: Set<EntityId>,
|
|
229
|
+
): Map<string, Set<EntityId>> {
|
|
230
|
+
const viewportChunks = new Map<string, Set<EntityId>>();
|
|
231
|
+
|
|
232
|
+
// Evaluate positions
|
|
233
|
+
const positions = evaluateConstraintsAtT0(ir.constraints);
|
|
234
|
+
|
|
235
|
+
// Band height (configurable, default to viewport height)
|
|
236
|
+
const bandHeight = viewport.height;
|
|
237
|
+
|
|
238
|
+
for (const entityId of ir.entities) {
|
|
239
|
+
if (initiallyVisible.has(entityId)) continue;
|
|
240
|
+
|
|
241
|
+
const pos = positions.get(entityId);
|
|
242
|
+
if (!pos) continue;
|
|
243
|
+
|
|
244
|
+
// Determine which band this entity falls into
|
|
245
|
+
const bandIndex = Math.floor(pos.y / bandHeight);
|
|
246
|
+
const chunkKey = `viewport-band:${bandIndex}`;
|
|
247
|
+
|
|
248
|
+
if (!viewportChunks.has(chunkKey)) {
|
|
249
|
+
viewportChunks.set(chunkKey, new Set());
|
|
250
|
+
}
|
|
251
|
+
viewportChunks.get(chunkKey)!.add(entityId);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return viewportChunks;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Rule 4: Import-Based Chunk Boundary Rule
|
|
259
|
+
*
|
|
260
|
+
* Each imported module can define a chunk boundary.
|
|
261
|
+
* This enables code splitting at the component level.
|
|
262
|
+
*
|
|
263
|
+
* ```
|
|
264
|
+
* import { Button } from "./components/button.vs"
|
|
265
|
+
* // Button's entities MAY be in a separate chunk if:
|
|
266
|
+
* // - They are not initially visible, OR
|
|
267
|
+
* // - They are marked with `lazy: true` in the import
|
|
268
|
+
* ```
|
|
269
|
+
*/
|
|
270
|
+
function computeImportChunks(
|
|
271
|
+
ir: IRModule,
|
|
272
|
+
initiallyVisible: Set<EntityId>,
|
|
273
|
+
): Map<string, Set<EntityId>> {
|
|
274
|
+
const importChunks = new Map<string, Set<EntityId>>();
|
|
275
|
+
|
|
276
|
+
for (const imp of ir.imports) {
|
|
277
|
+
const lazyEntities = new Set<EntityId>();
|
|
278
|
+
|
|
279
|
+
for (const entityId of imp.exportedEntities) {
|
|
280
|
+
if (!initiallyVisible.has(entityId)) {
|
|
281
|
+
lazyEntities.add(entityId);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (lazyEntities.size > 0) {
|
|
286
|
+
const chunkKey = `import:${imp.path}`;
|
|
287
|
+
importChunks.set(chunkKey, lazyEntities);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return importChunks;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Main chunk splitting function.
|
|
296
|
+
*/
|
|
297
|
+
export function splitIntoChunks(
|
|
298
|
+
ir: IRModule,
|
|
299
|
+
viewport: ViewportBounds,
|
|
300
|
+
): ChunkSplitResult {
|
|
301
|
+
// Step 1: Compute initially visible entities
|
|
302
|
+
const initiallyVisible = computeInitiallyVisibleEntities(ir, viewport);
|
|
303
|
+
|
|
304
|
+
// Step 2: Compute event-triggered chunks
|
|
305
|
+
const eventChunks = computeEventTriggeredChunks(ir, initiallyVisible);
|
|
306
|
+
|
|
307
|
+
// Step 3: Compute viewport-intersection chunks
|
|
308
|
+
const viewportChunks = computeViewportIntersectionChunks(ir, viewport, initiallyVisible);
|
|
309
|
+
|
|
310
|
+
// Step 4: Compute import-based chunks
|
|
311
|
+
const importChunks = computeImportChunks(ir, initiallyVisible);
|
|
312
|
+
|
|
313
|
+
// Step 5: Merge and deduplicate chunks
|
|
314
|
+
const allLazyChunks = new Map<string, Set<EntityId>>();
|
|
315
|
+
|
|
316
|
+
for (const [key, entities] of eventChunks) {
|
|
317
|
+
allLazyChunks.set(key, entities);
|
|
318
|
+
}
|
|
319
|
+
for (const [key, entities] of viewportChunks) {
|
|
320
|
+
if (!allLazyChunks.has(key)) {
|
|
321
|
+
allLazyChunks.set(key, entities);
|
|
322
|
+
} else {
|
|
323
|
+
// Merge
|
|
324
|
+
for (const e of entities) {
|
|
325
|
+
allLazyChunks.get(key)!.add(e);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
for (const [key, entities] of importChunks) {
|
|
330
|
+
if (!allLazyChunks.has(key)) {
|
|
331
|
+
allLazyChunks.set(key, entities);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Step 6: Build chunk objects
|
|
336
|
+
const initialChunk: Chunk = {
|
|
337
|
+
id: 'initial',
|
|
338
|
+
entityIds: Array.from(initiallyVisible),
|
|
339
|
+
dependsOn: [],
|
|
340
|
+
isInitial: true,
|
|
341
|
+
loadTriggers: [{ type: 'immediate' }],
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
const lazyChunks: Chunk[] = [];
|
|
345
|
+
const entityToChunk = new Map<EntityId, ChunkId>();
|
|
346
|
+
|
|
347
|
+
for (const entityId of initiallyVisible) {
|
|
348
|
+
entityToChunk.set(entityId, 'initial');
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
for (const [chunkId, entities] of allLazyChunks) {
|
|
352
|
+
const triggers = computeLoadTriggers(chunkId, entities, ir);
|
|
353
|
+
|
|
354
|
+
const chunk: Chunk = {
|
|
355
|
+
id: chunkId,
|
|
356
|
+
entityIds: Array.from(entities),
|
|
357
|
+
dependsOn: ['initial'], // All lazy chunks depend on initial
|
|
358
|
+
isInitial: false,
|
|
359
|
+
loadTriggers: triggers,
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
lazyChunks.push(chunk);
|
|
363
|
+
|
|
364
|
+
for (const entityId of entities) {
|
|
365
|
+
entityToChunk.set(entityId, chunkId);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Step 7: Compute chunk dependencies
|
|
370
|
+
const chunkDependencies = computeChunkDependencies(
|
|
371
|
+
initialChunk,
|
|
372
|
+
lazyChunks,
|
|
373
|
+
ir.constraints,
|
|
374
|
+
entityToChunk,
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
return {
|
|
378
|
+
initialChunk,
|
|
379
|
+
lazyChunks,
|
|
380
|
+
entityToChunk,
|
|
381
|
+
chunkDependencies,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// =============================================================================
|
|
386
|
+
// Helper Functions
|
|
387
|
+
// =============================================================================
|
|
388
|
+
|
|
389
|
+
function evaluateConstraintsAtT0(
|
|
390
|
+
constraints: IRConstraint[],
|
|
391
|
+
): Map<EntityId, { x: number; y: number; z: number }> {
|
|
392
|
+
// Simplified evaluation - in production, this would use
|
|
393
|
+
// the full constraint solver with T=0
|
|
394
|
+
const positions = new Map<EntityId, { x: number; y: number; z: number }>();
|
|
395
|
+
|
|
396
|
+
for (const c of constraints) {
|
|
397
|
+
if (!positions.has(c.target)) {
|
|
398
|
+
positions.set(c.target, { x: 0, y: 0, z: 0 });
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const pos = positions.get(c.target)!;
|
|
402
|
+
|
|
403
|
+
if (c.term.type === 'const') {
|
|
404
|
+
const value = rationalToNumber(c.term.value);
|
|
405
|
+
if (c.component === 'x') pos.x = value;
|
|
406
|
+
if (c.component === 'y') pos.y = value;
|
|
407
|
+
if (c.component === 'z') pos.z = value;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return positions;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function rationalToNumber(r: Rational): number {
|
|
415
|
+
return Number(r.numerator) / Number(r.denominator);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function intersectsViewport(
|
|
419
|
+
pos: { x: number; y: number },
|
|
420
|
+
viewport: ViewportBounds,
|
|
421
|
+
): boolean {
|
|
422
|
+
// Simplified - assumes point intersection
|
|
423
|
+
return (
|
|
424
|
+
pos.x >= viewport.x &&
|
|
425
|
+
pos.x <= viewport.x + viewport.width &&
|
|
426
|
+
pos.y >= viewport.y &&
|
|
427
|
+
pos.y <= viewport.y + viewport.height
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function getReferencedEntities(
|
|
432
|
+
entityId: EntityId,
|
|
433
|
+
constraints: IRConstraint[],
|
|
434
|
+
): Set<EntityId> {
|
|
435
|
+
const refs = new Set<EntityId>();
|
|
436
|
+
|
|
437
|
+
for (const c of constraints) {
|
|
438
|
+
if (c.target !== entityId) continue;
|
|
439
|
+
|
|
440
|
+
if (c.term.type === 'ref') {
|
|
441
|
+
refs.add(c.term.entityId);
|
|
442
|
+
} else if (c.term.type === 'linear') {
|
|
443
|
+
refs.add(c.term.entityId);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
return refs;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function computeAffectedEntities(
|
|
451
|
+
startEntity: EntityId,
|
|
452
|
+
constraints: IRConstraint[],
|
|
453
|
+
): Set<EntityId> {
|
|
454
|
+
const affected = new Set<EntityId>();
|
|
455
|
+
const queue = [startEntity];
|
|
456
|
+
|
|
457
|
+
while (queue.length > 0) {
|
|
458
|
+
const current = queue.shift()!;
|
|
459
|
+
if (affected.has(current)) continue;
|
|
460
|
+
affected.add(current);
|
|
461
|
+
|
|
462
|
+
// Find constraints that reference this entity
|
|
463
|
+
for (const c of constraints) {
|
|
464
|
+
const refEntity =
|
|
465
|
+
c.term.type === 'ref' ? c.term.entityId :
|
|
466
|
+
c.term.type === 'linear' ? c.term.entityId :
|
|
467
|
+
null;
|
|
468
|
+
|
|
469
|
+
if (refEntity === current && !affected.has(c.target)) {
|
|
470
|
+
queue.push(c.target);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
return affected;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function computeLoadTriggers(
|
|
479
|
+
chunkId: string,
|
|
480
|
+
entities: Set<EntityId>,
|
|
481
|
+
ir: IRModule,
|
|
482
|
+
): LoadTrigger[] {
|
|
483
|
+
const triggers: LoadTrigger[] = [];
|
|
484
|
+
|
|
485
|
+
if (chunkId.startsWith('event:')) {
|
|
486
|
+
const [, entityIdStr, eventType] = chunkId.split(':');
|
|
487
|
+
triggers.push({
|
|
488
|
+
type: 'event',
|
|
489
|
+
eventType,
|
|
490
|
+
targetEntity: parseInt(entityIdStr, 10),
|
|
491
|
+
});
|
|
492
|
+
} else if (chunkId.startsWith('viewport-band:')) {
|
|
493
|
+
// Use first entity as intersection target
|
|
494
|
+
const firstEntity = entities.values().next().value;
|
|
495
|
+
if (firstEntity !== undefined) {
|
|
496
|
+
triggers.push({
|
|
497
|
+
type: 'viewport-intersect',
|
|
498
|
+
entityId: firstEntity,
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
return triggers;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function computeChunkDependencies(
|
|
507
|
+
initial: Chunk,
|
|
508
|
+
lazy: Chunk[],
|
|
509
|
+
constraints: IRConstraint[],
|
|
510
|
+
entityToChunk: Map<EntityId, ChunkId>,
|
|
511
|
+
): Map<ChunkId, Set<ChunkId>> {
|
|
512
|
+
const deps = new Map<ChunkId, Set<ChunkId>>();
|
|
513
|
+
|
|
514
|
+
deps.set(initial.id, new Set());
|
|
515
|
+
|
|
516
|
+
for (const chunk of lazy) {
|
|
517
|
+
const chunkDeps = new Set<ChunkId>();
|
|
518
|
+
|
|
519
|
+
// Check if any entity in this chunk references an entity in another chunk
|
|
520
|
+
for (const entityId of chunk.entityIds) {
|
|
521
|
+
const refs = getReferencedEntities(entityId, constraints);
|
|
522
|
+
for (const ref of refs) {
|
|
523
|
+
const refChunk = entityToChunk.get(ref);
|
|
524
|
+
if (refChunk && refChunk !== chunk.id) {
|
|
525
|
+
chunkDeps.add(refChunk);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
deps.set(chunk.id, chunkDeps);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
return deps;
|
|
534
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ViewScript Renderer
|
|
3
|
+
*
|
|
4
|
+
* Compiles .vs IR files into target-specific output (wgpu, WebGL, SVG).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface RenderTarget {
|
|
8
|
+
name: string;
|
|
9
|
+
compile(ir: VsIR): Promise<CompiledOutput>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface VsIR {
|
|
13
|
+
entities: Entity[];
|
|
14
|
+
constraints: Constraint[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface Entity {
|
|
18
|
+
id: number;
|
|
19
|
+
type: 'point' | 'curve' | 'surface';
|
|
20
|
+
vector: PVector;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface PVector {
|
|
24
|
+
x: number;
|
|
25
|
+
y: number;
|
|
26
|
+
z: number;
|
|
27
|
+
t: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface Constraint {
|
|
31
|
+
id: number;
|
|
32
|
+
target: number;
|
|
33
|
+
component: 'x' | 'y' | 'z' | 't';
|
|
34
|
+
relation: 'eq' | 'lt' | 'le' | 'gt' | 'ge';
|
|
35
|
+
term: ConstraintTerm;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type ConstraintTerm =
|
|
39
|
+
| { type: 'const'; value: number }
|
|
40
|
+
| { type: 'ref'; entityId: number; component: 'x' | 'y' | 'z' | 't' }
|
|
41
|
+
| { type: 'linear'; coefficient: number; entityId: number; component: 'x' | 'y' | 'z' | 't'; offset: number };
|
|
42
|
+
|
|
43
|
+
export interface CompiledOutput {
|
|
44
|
+
html: string;
|
|
45
|
+
js: string;
|
|
46
|
+
css: string;
|
|
47
|
+
assets: Map<string, Uint8Array>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class WgpuTarget implements RenderTarget {
|
|
51
|
+
name = 'wgpu';
|
|
52
|
+
|
|
53
|
+
async compile(ir: VsIR): Promise<CompiledOutput> {
|
|
54
|
+
// TODO: Implement wgpu compilation
|
|
55
|
+
return {
|
|
56
|
+
html: '',
|
|
57
|
+
js: '',
|
|
58
|
+
css: '',
|
|
59
|
+
assets: new Map(),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|