@vectojs/core 1.14.0 → 1.16.0
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/{chunk-XIEQHSBB.mjs → chunk-AQTO7OSU.mjs} +253 -496
- package/dist/{chunk-2Z23LTH3.js → chunk-DUYB4GX4.js} +310 -551
- package/dist/{chunk-BEUIB3U7.js → chunk-IYPLG4Z4.js} +93 -43
- package/dist/{chunk-L5BCKFQE.mjs → chunk-JFU56BX4.mjs} +48 -0
- package/dist/index.d.ts +4 -12
- package/dist/index.js +2155 -456
- package/dist/index.mjs +1977 -279
- package/dist/layout/index.d.ts +1 -3
- package/dist/layout.js +3 -15
- package/dist/layout.mjs +2 -16
- package/dist/renderer/CanvasRenderer.d.ts +22 -0
- package/dist/renderer/IRenderer.d.ts +13 -0
- package/dist/renderer.js +4 -3
- package/dist/renderer.mjs +1 -1
- package/dist/text/MSDFTextEntity.d.ts +30 -1
- package/dist/text/index.d.ts +1 -5
- package/dist/text.js +5 -15
- package/dist/text.mjs +6 -17
- package/dist/tree/ComputeParticleEntity.d.ts +18 -0
- package/dist/tree/DOMPortalEntity.d.ts +18 -0
- package/dist/tree/Entity.d.ts +122 -6
- package/dist/tree/Scene.d.ts +380 -0
- package/dist/wasm/anim-backend.d.ts +86 -0
- package/dist/wasm/asset.d.ts +25 -0
- package/dist/wasm/asset.js +7 -0
- package/dist/wasm/asset.mjs +5 -0
- package/dist/wasm/backend.d.ts +154 -0
- package/dist/wasm/hit-backend.d.ts +92 -0
- package/dist/wasm/hit-store.d.ts +48 -0
- package/dist/wasm/particle-backend.d.ts +104 -0
- package/dist/wasm/scene-store.d.ts +28 -0
- package/dist/wasm/soa.d.ts +146 -0
- package/dist/wasm/vectojs_core.wasm +0 -0
- package/package.json +17 -8
- package/dist/animation/drivers.d.ts +0 -48
- package/dist/animation/easing.d.ts +0 -16
- package/dist/chunk-4AR425AR.js +0 -1121
- package/dist/chunk-BA5HUUDF.js +0 -760
- package/dist/chunk-IESDTEJ4.mjs +0 -1121
- package/dist/chunk-X7I465AQ.mjs +0 -760
- package/dist/layout/LayoutEngine.d.ts +0 -289
- package/dist/layout/LayoutWorker.d.ts +0 -23
- package/dist/layout/LayoutWorkerManager.d.ts +0 -26
- package/dist/layout/LayoutWorkerSource.d.ts +0 -1
- package/dist/layout/measure.d.ts +0 -20
- package/dist/math/SpatialHashGrid.d.ts +0 -53
- package/dist/math/SpringPhysics.d.ts +0 -13
- package/dist/text/ArabicShaper.d.ts +0 -10
- package/dist/text/BidiResolver.d.ts +0 -5
- package/dist/text/MSDFFont.d.ts +0 -129
- package/dist/text/PreparedContentGrid.d.ts +0 -60
- package/dist/text/Typography.d.ts +0 -11
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WASM transform backend: uploads a {@link TransformStore}, runs the f64x2 SIMD
|
|
3
|
+
* kernel, and reads world matrices back. This is an invisible accelerator —
|
|
4
|
+
* {@link composeJS} computes the identical result and is the permanent fallback,
|
|
5
|
+
* so a caller that cannot instantiate WASM (CSP, no SIMD, missing asset) simply
|
|
6
|
+
* keeps using the JS path. Failure is the default state, not an error path.
|
|
7
|
+
*
|
|
8
|
+
* The seam is two batched crossings per compose (upload, readback), never per
|
|
9
|
+
* entity: at ~12-31 ns/crossing a per-entity call at 100k would cost >1 ms/frame.
|
|
10
|
+
*/
|
|
11
|
+
import type { TransformStore } from './soa';
|
|
12
|
+
/** Which kernel to run. `simd` is the default; `scalar` exists for A/B and for
|
|
13
|
+
* the (theoretical) case of a build without simd128. Both are f64 and
|
|
14
|
+
* bit-identical to {@link composeJS}. */
|
|
15
|
+
export type Kernel = 'simd' | 'scalar';
|
|
16
|
+
/**
|
|
17
|
+
* A live WASM backend bound to one module instance. `compose` is allocation-free
|
|
18
|
+
* after the first call at a given high-water capacity; growing past it re-`init`s
|
|
19
|
+
* and re-views memory (a `WebAssembly.Memory.buffer` detaches on growth, so the
|
|
20
|
+
* typed-array views must be rebuilt then — done here, never mid-compose).
|
|
21
|
+
*/
|
|
22
|
+
export declare class WasmTransformBackend {
|
|
23
|
+
readonly available: true;
|
|
24
|
+
private readonly ex;
|
|
25
|
+
private cap;
|
|
26
|
+
private runCap;
|
|
27
|
+
private vx;
|
|
28
|
+
private vy;
|
|
29
|
+
private vsx;
|
|
30
|
+
private vsy;
|
|
31
|
+
private vcos;
|
|
32
|
+
private vsin;
|
|
33
|
+
private vop;
|
|
34
|
+
private vwa;
|
|
35
|
+
private vwb;
|
|
36
|
+
private vwc;
|
|
37
|
+
private vwd;
|
|
38
|
+
private vwe;
|
|
39
|
+
private vwf;
|
|
40
|
+
private vwo;
|
|
41
|
+
private vbx;
|
|
42
|
+
private vby;
|
|
43
|
+
private vbw;
|
|
44
|
+
private vbh;
|
|
45
|
+
private vaminx;
|
|
46
|
+
private vaminy;
|
|
47
|
+
private vamaxx;
|
|
48
|
+
private vamaxy;
|
|
49
|
+
private vrp;
|
|
50
|
+
private vrs;
|
|
51
|
+
private vrl;
|
|
52
|
+
constructor(instance: WebAssembly.Instance);
|
|
53
|
+
/** Compose world matrices for `store` in WASM, writing back into its
|
|
54
|
+
* `wa..wo` arrays. Result is bit-identical to `composeJS(store)`. */
|
|
55
|
+
compose(store: TransformStore, kernel?: Kernel): void;
|
|
56
|
+
/**
|
|
57
|
+
* Run the kernel only, over data already resident in WASM memory — no upload,
|
|
58
|
+
* no readback. This is the per-frame cost the *designed* integration pays:
|
|
59
|
+
* entity accessors write `x/y/rotation` straight into the wasm input views
|
|
60
|
+
* (via {@link inputView}) and the renderer reads world matrices straight from
|
|
61
|
+
* the wasm output views (via {@link worldView}), so the batch copies in
|
|
62
|
+
* {@link compose} do not happen every frame. `compose` must have run at least
|
|
63
|
+
* once at the current capacity to size the store and set the run count; call
|
|
64
|
+
* {@link uploadRuns} after a topology change.
|
|
65
|
+
*/
|
|
66
|
+
runKernel(kernel?: Kernel): void;
|
|
67
|
+
/** Upload only the run table + count (topology), leaving per-entity inputs to
|
|
68
|
+
* the resident views. Call when the tree structure changes, not per frame. */
|
|
69
|
+
uploadRuns(store: TransformStore): void;
|
|
70
|
+
/** The resident wasm input views (`x,y,sx,sy,cos,sin,opacity`), valid until
|
|
71
|
+
* the next capacity growth. Writing here is what makes uploads unnecessary. */
|
|
72
|
+
inputView(): {
|
|
73
|
+
x: Float64Array;
|
|
74
|
+
y: Float64Array;
|
|
75
|
+
sx: Float64Array;
|
|
76
|
+
sy: Float64Array;
|
|
77
|
+
cos: Float64Array;
|
|
78
|
+
sin: Float64Array;
|
|
79
|
+
opacity: Float64Array;
|
|
80
|
+
};
|
|
81
|
+
/** The resident wasm world-matrix output views (`wa..wo`). Reading here is
|
|
82
|
+
* what makes readback unnecessary. */
|
|
83
|
+
worldView(): {
|
|
84
|
+
wa: Float64Array;
|
|
85
|
+
wb: Float64Array;
|
|
86
|
+
wc: Float64Array;
|
|
87
|
+
wd: Float64Array;
|
|
88
|
+
we: Float64Array;
|
|
89
|
+
wf: Float64Array;
|
|
90
|
+
wo: Float64Array;
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
93
|
+
* Compute world-space AABBs for `store` in WASM (G1+), writing back into its
|
|
94
|
+
* `aminx/aminy/amaxx/amaxy` arrays. Uploads the local bounds, runs the AABB
|
|
95
|
+
* pass, reads results back. Result is bit-identical to `computeAabbsJS(store)`.
|
|
96
|
+
* `compose` (or `runKernel`) must have populated the world matrices first —
|
|
97
|
+
* this pass reads them. For the resident (no-copy) integration, write bounds
|
|
98
|
+
* via {@link boundsView} and read via {@link aabbView} + call
|
|
99
|
+
* {@link runAabbs} instead.
|
|
100
|
+
*/
|
|
101
|
+
computeAabbs(store: TransformStore): void;
|
|
102
|
+
/** Run the AABB pass only, over `count` entities already resident in wasm
|
|
103
|
+
* memory (bounds written via {@link boundsView}, world matrices already
|
|
104
|
+
* composed). No upload/readback — the per-frame resident path. */
|
|
105
|
+
runAabbs(count: number): void;
|
|
106
|
+
/** Resident wasm local-bounds input views (`bx,by,bw,bh`) for the AABB pass. */
|
|
107
|
+
boundsView(): {
|
|
108
|
+
bx: Float64Array;
|
|
109
|
+
by: Float64Array;
|
|
110
|
+
bw: Float64Array;
|
|
111
|
+
bh: Float64Array;
|
|
112
|
+
};
|
|
113
|
+
/** Resident wasm world-AABB output views (`aminx,aminy,amaxx,amaxy`). */
|
|
114
|
+
aabbView(): {
|
|
115
|
+
aminx: Float64Array;
|
|
116
|
+
aminy: Float64Array;
|
|
117
|
+
amaxx: Float64Array;
|
|
118
|
+
amaxy: Float64Array;
|
|
119
|
+
};
|
|
120
|
+
private ensure;
|
|
121
|
+
/** Rebuild typed-array views after an init() (which may have grown, and thus
|
|
122
|
+
* detached, the memory buffer). */
|
|
123
|
+
private refreshViews;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Instantiate synchronously (Node/tests, or a worker). Rejected on the browser
|
|
127
|
+
* main thread for modules >4 KB — use {@link instantiateAsync} there. Returns
|
|
128
|
+
* `null` if compilation/instantiation throws, so callers fall back to JS.
|
|
129
|
+
*/
|
|
130
|
+
export declare function instantiateSync(bytes: BufferSource): WasmTransformBackend | null;
|
|
131
|
+
/**
|
|
132
|
+
* Instantiate asynchronously (browser main thread). Returns `null` on any
|
|
133
|
+
* failure — CSP `wasm-unsafe-eval`, unsupported SIMD, corrupt/missing bytes —
|
|
134
|
+
* so the caller keeps using the JS path. This is the loader the Scene hot-swap
|
|
135
|
+
* (gated integration) will await.
|
|
136
|
+
*/
|
|
137
|
+
export declare function instantiateAsync(bytes: BufferSource): Promise<WasmTransformBackend | null>;
|
|
138
|
+
/**
|
|
139
|
+
* Anything the transform core can be loaded from: raw bytes, a URL/path string
|
|
140
|
+
* or {@link URL} to fetch, or a {@link Response} (or a promise of one) — e.g.
|
|
141
|
+
* `fetch(new URL('./vectojs_core.wasm', import.meta.url))`, the shape every
|
|
142
|
+
* bundler emits for a co-located `.wasm` asset.
|
|
143
|
+
*/
|
|
144
|
+
export type WasmModuleSource = BufferSource | string | URL | Response | Promise<Response>;
|
|
145
|
+
/**
|
|
146
|
+
* Instantiate from a URL/Response using streaming compilation when the platform
|
|
147
|
+
* supports it (the module compiles while it downloads — the fastest cold start),
|
|
148
|
+
* and transparently falling back to fetch → arrayBuffer → instantiate when
|
|
149
|
+
* `WebAssembly.instantiateStreaming` is unavailable or the response's MIME type
|
|
150
|
+
* is not `application/wasm` (some engines reject the stream in that case, e.g. a
|
|
151
|
+
* dev server serving `application/octet-stream`). Returns `null` on any failure
|
|
152
|
+
* so the caller keeps the JS path — loading the accelerator is never an error path.
|
|
153
|
+
*/
|
|
154
|
+
export declare function instantiateStreaming(source: string | URL | Response | Promise<Response>): Promise<WasmTransformBackend | null>;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WASM hit-test broad-phase backend: builds a dense viewport grid from world
|
|
3
|
+
* AABBs and answers "which entities overlap this cell" so a pointer query scans
|
|
4
|
+
* a handful of candidates instead of every entity in the tree. This is an
|
|
5
|
+
* invisible accelerator — the JS depth-first walk ({@link Entity.isPointInside}
|
|
6
|
+
* over every node) is the permanent fallback, so a caller that cannot
|
|
7
|
+
* instantiate WASM simply keeps using it. Failure is the default state, not an
|
|
8
|
+
* error path.
|
|
9
|
+
*
|
|
10
|
+
* The grid is a coarse pre-filter only: it never decides a hit by itself.
|
|
11
|
+
* {@link candidatesAt} returns the AABB-overlapping candidates in a cell
|
|
12
|
+
* (ascending entity index — scan from the end for topmost-first); the caller
|
|
13
|
+
* re-checks each one against its entity's own precise `isPointInside` (so
|
|
14
|
+
* non-rectangular hit shapes stay correct) before trusting a result.
|
|
15
|
+
*/
|
|
16
|
+
export declare class HitTestBackend {
|
|
17
|
+
private readonly ex;
|
|
18
|
+
private entityCap;
|
|
19
|
+
private cellCap;
|
|
20
|
+
private itemCap;
|
|
21
|
+
private vminx;
|
|
22
|
+
private vminy;
|
|
23
|
+
private vmaxx;
|
|
24
|
+
private vmaxy;
|
|
25
|
+
private vCellStart;
|
|
26
|
+
private vCellCount;
|
|
27
|
+
private vItems;
|
|
28
|
+
/** Grid geometry from the last {@link ensure} call. */
|
|
29
|
+
gridW: number;
|
|
30
|
+
gridH: number;
|
|
31
|
+
cellSize: number;
|
|
32
|
+
constructor(instance: WebAssembly.Instance);
|
|
33
|
+
/** The resident AABB input views (`minx/miny/maxx/maxy`), valid until the
|
|
34
|
+
* next capacity growth. Writing here is what {@link build} reads from. */
|
|
35
|
+
inputView(): {
|
|
36
|
+
minx: Float64Array;
|
|
37
|
+
miny: Float64Array;
|
|
38
|
+
maxx: Float64Array;
|
|
39
|
+
maxy: Float64Array;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Size (and grow, if needed) capacity for `count` entities over
|
|
43
|
+
* `[0,vw] x [0,vh]` at `cellSize`, and record the grid geometry
|
|
44
|
+
* {@link candidatesAt} needs. Call this BEFORE writing AABBs into
|
|
45
|
+
* {@link inputView} — a capacity growth detaches the previous views, so
|
|
46
|
+
* writing first and sizing after would write into a stale buffer.
|
|
47
|
+
*/
|
|
48
|
+
ensure(count: number, vw: number, vh: number, cellSize: number): void;
|
|
49
|
+
/**
|
|
50
|
+
* Run the kernel's bucketing over whatever is currently resident in
|
|
51
|
+
* {@link inputView} (write the AABBs there, and call {@link ensure} first).
|
|
52
|
+
* Returns `false` if the build overflowed its item budget — the caller must
|
|
53
|
+
* not trust {@link candidatesAt} results for this build and should fall back
|
|
54
|
+
* to the JS walk instead (never return a wrong hit).
|
|
55
|
+
*/
|
|
56
|
+
runBuild(count: number, vw: number, vh: number, cellSize: number): boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Entity indices whose AABB overlaps the cell containing `(px, py)`, in
|
|
59
|
+
* ascending index order (scan from the end for topmost/highest-index
|
|
60
|
+
* first), or `null` if the point falls outside the built grid. This is a
|
|
61
|
+
* coarse candidate LIST, not a hit result — the caller must still confirm
|
|
62
|
+
* each candidate's AABB contains the point and re-check its precise
|
|
63
|
+
* `isPointInside`.
|
|
64
|
+
*/
|
|
65
|
+
candidatesAt(px: number, py: number): Int32Array | null;
|
|
66
|
+
private growIfNeeded;
|
|
67
|
+
/** Rebuild typed-array views after a growing `hit_init` (which detaches the
|
|
68
|
+
* memory buffer). */
|
|
69
|
+
private refreshViews;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Instantiate synchronously (Node/tests, or a worker). Rejected on the browser
|
|
73
|
+
* main thread for modules >4 KB — use {@link instantiateAsync} there. Returns
|
|
74
|
+
* `null` if compilation/instantiation throws, so callers fall back to JS.
|
|
75
|
+
*/
|
|
76
|
+
export declare function instantiateSync(bytes: BufferSource): HitTestBackend | null;
|
|
77
|
+
/**
|
|
78
|
+
* Instantiate asynchronously (browser main thread). Returns `null` on any
|
|
79
|
+
* failure — CSP `wasm-unsafe-eval`, unsupported, corrupt/missing bytes — so the
|
|
80
|
+
* caller keeps using the JS path.
|
|
81
|
+
*/
|
|
82
|
+
export declare function instantiateAsync(bytes: BufferSource): Promise<HitTestBackend | null>;
|
|
83
|
+
/** Anything the hit-test core can be loaded from, matching the transform
|
|
84
|
+
* core's {@link WasmModuleSource} loading ergonomics. */
|
|
85
|
+
export type HitModuleSource = BufferSource | string | URL | Response | Promise<Response>;
|
|
86
|
+
/**
|
|
87
|
+
* Instantiate from a URL/Response using streaming compilation when the
|
|
88
|
+
* platform supports it, falling back to fetch → arrayBuffer → instantiate when
|
|
89
|
+
* unavailable or the response's MIME type is rejected. Returns `null` on any
|
|
90
|
+
* failure so the caller keeps the JS path.
|
|
91
|
+
*/
|
|
92
|
+
export declare function instantiateStreaming(source: string | URL | Response | Promise<Response>): Promise<HitTestBackend | null>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gather world AABBs from a live scene subtree for the WASM hit-test grid.
|
|
3
|
+
*
|
|
4
|
+
* This mirrors `Scene.findHitRecursively`'s exact traversal (self, then EVERY
|
|
5
|
+
* child in array order, recursively — `node.children` unconditionally, so
|
|
6
|
+
* DOM-portal descendants and anything renderNode's rendering-specific
|
|
7
|
+
* early-returns skip are still covered) so the grid-accelerated path can never
|
|
8
|
+
* see a different entity set than the JS walk it replaces.
|
|
9
|
+
*
|
|
10
|
+
* Indices are assigned in PRE-ORDER (self before children, children in array
|
|
11
|
+
* order): a node's own index is always lower than every index in its own
|
|
12
|
+
* subtree, and an earlier sibling's whole subtree is entirely lower than a
|
|
13
|
+
* later sibling's. Since `findHitRecursively` checks later siblings' subtrees
|
|
14
|
+
* before earlier ones, and a node's children before the node itself, "highest
|
|
15
|
+
* index wins" over this numbering is EXACTLY equivalent to that traversal's
|
|
16
|
+
* topmost-hit priority — the invariant the WASM grid kernel's `idx > best`
|
|
17
|
+
* comparison (see hit.rs) and the boundless-entity merge below both rely on.
|
|
18
|
+
*
|
|
19
|
+
* Entities without `getBounds()` (opted out of culling — "never known bounds")
|
|
20
|
+
* cannot be spatially indexed at all; they are collected into `boundless`
|
|
21
|
+
* instead, each tagged with its index, so the caller can still check them and
|
|
22
|
+
* compare against the grid's best candidate by that same index.
|
|
23
|
+
*/
|
|
24
|
+
import type { Entity } from '../tree/Entity';
|
|
25
|
+
export interface HitGatherResult {
|
|
26
|
+
/** Total entities visited (bounded + boundless); the grid's `count`. */
|
|
27
|
+
count: number;
|
|
28
|
+
/** index -> Entity, for bounded entities (grid slot == array index). */
|
|
29
|
+
slotEntity: Entity[];
|
|
30
|
+
/** Entities with no `getBounds()`, each with its pre-order index, ascending. */
|
|
31
|
+
boundless: Array<{
|
|
32
|
+
entity: Entity;
|
|
33
|
+
index: number;
|
|
34
|
+
}>;
|
|
35
|
+
/** World AABBs, indexed like `slotEntity` (a boundless index has garbage —
|
|
36
|
+
* never read, since those entities are only resolved via `boundless`). */
|
|
37
|
+
minx: Float64Array;
|
|
38
|
+
miny: Float64Array;
|
|
39
|
+
maxx: Float64Array;
|
|
40
|
+
maxy: Float64Array;
|
|
41
|
+
}
|
|
42
|
+
/** Walk `root` and all descendants into a flat, pre-order-indexed AABB set.
|
|
43
|
+
* `currentFrame` is the scene's frame counter — passed through to
|
|
44
|
+
* {@link Entity._readWorldCache} so the common case (every entity was
|
|
45
|
+
* rendered this same frame, which the "gather right after a render" call
|
|
46
|
+
* site always guarantees) reads six cached scalars per entity instead of
|
|
47
|
+
* allocating a fresh `{a,b,c,d,e,f}` object via `getWorldTransform()`. */
|
|
48
|
+
export declare function gatherHitAABBs(root: Entity, currentFrame: number): HitGatherResult;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WASM particle-simulation backend (G4): advances the whole particle buffer for
|
|
3
|
+
* a `ComputeParticleEntity` in one `particle_step` call, instead of the JS
|
|
4
|
+
* per-particle `updateCPU` loop. This is the CPU fallback path — it runs exactly
|
|
5
|
+
* when there is no GPU — so the machines that most need it get the batched
|
|
6
|
+
* kernel. The JS `updateCPU` remains the permanent fallback when WASM cannot
|
|
7
|
+
* instantiate.
|
|
8
|
+
*
|
|
9
|
+
* ## f32, and its own differential oracle
|
|
10
|
+
*
|
|
11
|
+
* The particle buffer is a `Float32Array` (matches the WGSL compute shader), so
|
|
12
|
+
* the kernel (`crates/vectojs-core-rs/src/particle.rs`) commits to **f32** and is
|
|
13
|
+
* NOT bit-comparable to the f64 transform core. Its oracle is
|
|
14
|
+
* {@link particleStepReferenceF32} below — a JS f32 reference that rounds every
|
|
15
|
+
* intermediate to f32 (`Math.fround`) in the same op order and uses
|
|
16
|
+
* `sqrt(dx*dx+dy*dy)` (not `Math.hypot`, which is correctly-rounded f64). With
|
|
17
|
+
* those two rules the reference is bit-identical to the kernel, since f32
|
|
18
|
+
* add/sub/mul/div/sqrt of f32 operands round once whether done in f32 or
|
|
19
|
+
* f64-then-`fround`. (The shipped `updateCPU` stays f64 and differs by <1 ULP
|
|
20
|
+
* per step — the accepted CPU-vs-GPU-class divergence the survey documents.)
|
|
21
|
+
*
|
|
22
|
+
* ## SoA transpose
|
|
23
|
+
*
|
|
24
|
+
* The render/GPU buffer is AoS stride-8. The backend transposes position/
|
|
25
|
+
* velocity/origin/life into per-field f32 arrays ({@link gather}), runs the
|
|
26
|
+
* kernel, and scatters position/velocity/life back ({@link scatter}); origin and
|
|
27
|
+
* `size` never change during the sim.
|
|
28
|
+
*/
|
|
29
|
+
/** Per-field SoA views over the kernel's linear memory. Position/velocity/life
|
|
30
|
+
* are read back each frame; origin is upload-once. */
|
|
31
|
+
export interface ParticleView {
|
|
32
|
+
px: Float32Array;
|
|
33
|
+
py: Float32Array;
|
|
34
|
+
vx: Float32Array;
|
|
35
|
+
vy: Float32Array;
|
|
36
|
+
ox: Float32Array;
|
|
37
|
+
oy: Float32Array;
|
|
38
|
+
life: Float32Array;
|
|
39
|
+
}
|
|
40
|
+
/** Scalar simulation parameters + the current explosion impulse, passed to
|
|
41
|
+
* `particle_step`. Mirrors `updateCPU`'s arguments. */
|
|
42
|
+
export interface ParticleStepParams {
|
|
43
|
+
dt: number;
|
|
44
|
+
mouseX: number;
|
|
45
|
+
mouseY: number;
|
|
46
|
+
width: number;
|
|
47
|
+
height: number;
|
|
48
|
+
springK: number;
|
|
49
|
+
damping: number;
|
|
50
|
+
bounceDamping: number;
|
|
51
|
+
maxVelocity: number;
|
|
52
|
+
explosion: {
|
|
53
|
+
x: number;
|
|
54
|
+
y: number;
|
|
55
|
+
force: number;
|
|
56
|
+
} | null;
|
|
57
|
+
}
|
|
58
|
+
export declare class ParticleBackend {
|
|
59
|
+
private readonly ex;
|
|
60
|
+
private cap;
|
|
61
|
+
private view;
|
|
62
|
+
constructor(instance: WebAssembly.Instance);
|
|
63
|
+
/** The resident SoA views, valid until the next capacity growth. */
|
|
64
|
+
particleView(): ParticleView;
|
|
65
|
+
/**
|
|
66
|
+
* Size (and grow, if needed) capacity for `count` particles. Call BEFORE
|
|
67
|
+
* writing into {@link particleView} — a growth detaches the previous views.
|
|
68
|
+
*/
|
|
69
|
+
ensure(count: number): void;
|
|
70
|
+
/**
|
|
71
|
+
* Advance `count` particles one step in place. Returns `true` when at least
|
|
72
|
+
* one live particle is still moving or off-origin beyond epsilon (the fused
|
|
73
|
+
* `hasPendingAnimations` flag), so the caller need not re-scan the buffer.
|
|
74
|
+
*/
|
|
75
|
+
step(count: number, p: ParticleStepParams): boolean;
|
|
76
|
+
/** Transpose the AoS stride-8 buffer into the SoA views (position/velocity/
|
|
77
|
+
* life every frame; origin upload-once when `withOrigin`). */
|
|
78
|
+
gather(data: Float32Array, count: number, withOrigin: boolean): void;
|
|
79
|
+
/** Scatter the mutated position/velocity/life back into the AoS buffer. */
|
|
80
|
+
scatter(data: Float32Array, count: number): void;
|
|
81
|
+
private refreshViews;
|
|
82
|
+
}
|
|
83
|
+
/** Instantiate synchronously (Node/tests/worker). Returns `null` on failure so
|
|
84
|
+
* callers fall back to the JS `updateCPU`. */
|
|
85
|
+
export declare function instantiateSync(bytes: BufferSource): ParticleBackend | null;
|
|
86
|
+
/** Instantiate asynchronously (browser main thread). Returns `null` on any
|
|
87
|
+
* failure so the caller keeps using the JS `updateCPU`. */
|
|
88
|
+
export declare function instantiateAsync(bytes: BufferSource): Promise<ParticleBackend | null>;
|
|
89
|
+
/** Anything the particle core can be loaded from. */
|
|
90
|
+
export type ParticleModuleSource = BufferSource | string | URL | Response | Promise<Response>;
|
|
91
|
+
/** Instantiate from a URL/Response with streaming compilation when available,
|
|
92
|
+
* falling back to fetch → arrayBuffer → instantiate. Returns `null` on any
|
|
93
|
+
* failure so the caller keeps the JS path. */
|
|
94
|
+
export declare function instantiateStreaming(source: string | URL | Response | Promise<Response>): Promise<ParticleBackend | null>;
|
|
95
|
+
/**
|
|
96
|
+
* JS f32 reference for `particle_step`, operating on the same {@link ParticleView}
|
|
97
|
+
* SoA arrays in place and returning the fused pending-animation flag. This is the
|
|
98
|
+
* kernel's differential oracle: every intermediate is rounded to f32 with
|
|
99
|
+
* `Math.fround` in the SAME op order as `particle.rs`, and distance uses
|
|
100
|
+
* `sqrt(dx*dx+dy*dy)` (NOT `Math.hypot`). It is therefore bit-identical to the
|
|
101
|
+
* Rust kernel — the differential test asserts exact equality. (This is NOT the
|
|
102
|
+
* shipped fallback; `ComputeParticleEntity.updateCPU` stays f64.)
|
|
103
|
+
*/
|
|
104
|
+
export declare function particleStepReferenceF32(view: ParticleView, count: number, p: ParticleStepParams): boolean;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build an SoA {@link TransformStore} from a live scene subtree.
|
|
3
|
+
*
|
|
4
|
+
* This is the bridge between the `Entity` tree and the WASM/JS transform core:
|
|
5
|
+
* it reads each entity's local transform (`x/y/scaleX/scaleY/rotation/opacity`)
|
|
6
|
+
* and emits the flat `InputNode[]` that {@link buildStore} turns into the
|
|
7
|
+
* depth-ordered, contiguous-sibling-run layout the kernel requires. It returns a
|
|
8
|
+
* map from `Entity` to its store index so a caller can read each entity's world
|
|
9
|
+
* matrix back out of `store.worldView()`/`store.wa…` after composition.
|
|
10
|
+
*
|
|
11
|
+
* The passed `root` becomes store index 0 at the identity world transform — its
|
|
12
|
+
* own local transform is ignored, matching how `Scene` seeds `renderNode` with
|
|
13
|
+
* the identity for `root`/`overlayRoot`. It touches no render state; wiring it
|
|
14
|
+
* into `Scene.render` is a separate, flagged step.
|
|
15
|
+
*/
|
|
16
|
+
import type { Entity } from '../tree/Entity';
|
|
17
|
+
import { type TransformStore } from './soa';
|
|
18
|
+
export interface TreeStore {
|
|
19
|
+
store: TransformStore;
|
|
20
|
+
/** Entity -> its index in `store` (into `wa…`/`worldView`). */
|
|
21
|
+
indexOf: Map<Entity, number>;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Walk `root` and all descendants (via `entity.children`) into an SoA store.
|
|
25
|
+
* Time O(n) in the subtree size; allocates one store per call (Stage 1 rebuilds
|
|
26
|
+
* per frame — Stage 3 will cache on structural change).
|
|
27
|
+
*/
|
|
28
|
+
export declare function buildTreeStore(root: Entity): TreeStore;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SoA transform store and the JS reference composer.
|
|
3
|
+
*
|
|
4
|
+
* This is the data-layout half of the Rust/WASM transform core. `buildStore`
|
|
5
|
+
* turns a tree into the pure Structure-of-Arrays layout the WASM kernel
|
|
6
|
+
* (`crates/vectojs-core-rs`) requires, and `composeJS` computes world matrices
|
|
7
|
+
* over that same layout in TypeScript. The two MUST produce bit-identical f64
|
|
8
|
+
* results — `composeJS` is both the correctness oracle for the differential
|
|
9
|
+
* test and the permanent fallback when WASM is unavailable (CSP, no SIMD,
|
|
10
|
+
* missing asset). Neither this file nor the loader touches `Entity`/`Scene`;
|
|
11
|
+
* wiring them into the render walk is the gated integration step (see the task
|
|
12
|
+
* plan), so this layer ships and is tested on its own.
|
|
13
|
+
*
|
|
14
|
+
* ## Why the layout is shaped this way
|
|
15
|
+
*
|
|
16
|
+
* - **Pure SoA** (one flat array per field): consecutive entities' `x` values
|
|
17
|
+
* are adjacent, so a `v128` load fetches two at once. An interleaved record
|
|
18
|
+
* would put them 8*N bytes apart and make SIMD unreachable.
|
|
19
|
+
* - **Contiguous sibling runs in depth order**: WASM SIMD has no gather, so the
|
|
20
|
+
* parent matrix must be loop-invariant across a run. `buildStore` emits every
|
|
21
|
+
* parent's children as one contiguous run and always emits a parent before
|
|
22
|
+
* its children, so a run's parent world matrix is already composed when the
|
|
23
|
+
* run is processed. The scene root is index 0 at the identity transform.
|
|
24
|
+
*/
|
|
25
|
+
/** A node's local transform (the six animatable scalars). */
|
|
26
|
+
export interface LocalTransform {
|
|
27
|
+
x: number;
|
|
28
|
+
y: number;
|
|
29
|
+
scaleX: number;
|
|
30
|
+
scaleY: number;
|
|
31
|
+
rotation: number;
|
|
32
|
+
opacity: number;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* One input node. `parent` is an index into the input array, or `-1` for the
|
|
36
|
+
* single root. Input order is arbitrary; `buildStore` re-indexes into store
|
|
37
|
+
* order (root at 0, then depth-ordered contiguous sibling runs).
|
|
38
|
+
*/
|
|
39
|
+
export interface InputNode extends LocalTransform {
|
|
40
|
+
parent: number;
|
|
41
|
+
/**
|
|
42
|
+
* Local render bounds `[bx, by, bw, bh]` for the world-AABB pass
|
|
43
|
+
* ({@link computeAabbsJS} / the crate's `compute_aabbs`). Optional; defaults to
|
|
44
|
+
* `[0, 0, 0, 0]`, matching `Entity.getWorldBounds`'s `[0,0,width,height]`
|
|
45
|
+
* fallback when a node has no render-specific box (callers pass `width`/
|
|
46
|
+
* `height` as `bw`/`bh`).
|
|
47
|
+
*/
|
|
48
|
+
bx?: number;
|
|
49
|
+
by?: number;
|
|
50
|
+
bw?: number;
|
|
51
|
+
bh?: number;
|
|
52
|
+
}
|
|
53
|
+
/** A composed world matrix (`a b c d e f`) plus accumulated opacity. */
|
|
54
|
+
export interface WorldMatrix {
|
|
55
|
+
a: number;
|
|
56
|
+
b: number;
|
|
57
|
+
c: number;
|
|
58
|
+
d: number;
|
|
59
|
+
e: number;
|
|
60
|
+
f: number;
|
|
61
|
+
opacity: number;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The SoA store: input fields, output world-matrix fields, and the sibling-run
|
|
65
|
+
* table. Field arrays are sized `count + 8` so a 2-lane SIMD tail can read one
|
|
66
|
+
* slot past the logical end without a scalar remainder loop (mirrors the
|
|
67
|
+
* crate's `+8` padding). `storeIndexOf[k]` maps input index `k` to store index.
|
|
68
|
+
*/
|
|
69
|
+
export interface TransformStore {
|
|
70
|
+
count: number;
|
|
71
|
+
capacity: number;
|
|
72
|
+
x: Float64Array;
|
|
73
|
+
y: Float64Array;
|
|
74
|
+
sx: Float64Array;
|
|
75
|
+
sy: Float64Array;
|
|
76
|
+
cos: Float64Array;
|
|
77
|
+
sin: Float64Array;
|
|
78
|
+
opacity: Float64Array;
|
|
79
|
+
wa: Float64Array;
|
|
80
|
+
wb: Float64Array;
|
|
81
|
+
wc: Float64Array;
|
|
82
|
+
wd: Float64Array;
|
|
83
|
+
we: Float64Array;
|
|
84
|
+
wf: Float64Array;
|
|
85
|
+
wo: Float64Array;
|
|
86
|
+
bx: Float64Array;
|
|
87
|
+
by: Float64Array;
|
|
88
|
+
bw: Float64Array;
|
|
89
|
+
bh: Float64Array;
|
|
90
|
+
aminx: Float64Array;
|
|
91
|
+
aminy: Float64Array;
|
|
92
|
+
amaxx: Float64Array;
|
|
93
|
+
amaxy: Float64Array;
|
|
94
|
+
runParent: Int32Array;
|
|
95
|
+
runStart: Int32Array;
|
|
96
|
+
runLen: Int32Array;
|
|
97
|
+
runCount: number;
|
|
98
|
+
storeIndexOf: Int32Array;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Build the SoA store from a node list. Exactly one node must have
|
|
102
|
+
* `parent === -1` (the root); it becomes store index 0 at the identity world
|
|
103
|
+
* transform (its own local transform is ignored, matching how `Scene` leaves
|
|
104
|
+
* root/overlayRoot at identity). Throws on a missing or duplicate root.
|
|
105
|
+
*
|
|
106
|
+
* Emission is BFS per parent: dequeue a parent, emit ALL its children as one
|
|
107
|
+
* contiguous run, enqueue them. This guarantees (a) each parent's children are
|
|
108
|
+
* contiguous — required for SIMD — and (b) a parent is assigned a lower store
|
|
109
|
+
* index and composed before its children.
|
|
110
|
+
*
|
|
111
|
+
* Time: O(n). Space: O(n).
|
|
112
|
+
*/
|
|
113
|
+
export declare function buildStore(nodes: InputNode[]): TransformStore;
|
|
114
|
+
/**
|
|
115
|
+
* Compose world matrices over the store in TypeScript — the reference oracle
|
|
116
|
+
* and the permanent JS fallback. Bit-identical to the crate's `compose_scalar`:
|
|
117
|
+
* same Canvas `T * S * R` order, same operation order, same f64 arithmetic.
|
|
118
|
+
* Seeds the root (index 0) to identity, then walks runs in order.
|
|
119
|
+
*/
|
|
120
|
+
export declare function composeJS(s: TransformStore): void;
|
|
121
|
+
/** Read the world matrix at store index `i`. */
|
|
122
|
+
export declare function readWorld(s: TransformStore, i: number): WorldMatrix;
|
|
123
|
+
/** A world-space axis-aligned bounding box (min/max corner). */
|
|
124
|
+
export interface WorldAabb {
|
|
125
|
+
minX: number;
|
|
126
|
+
minY: number;
|
|
127
|
+
maxX: number;
|
|
128
|
+
maxY: number;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Transform each node's local bounds `[bx, by, bw, bh]` through its already-
|
|
132
|
+
* composed world matrix into a world-space AABB (the min/max of the four
|
|
133
|
+
* transformed corners), writing `aminx/aminy/amaxx/amaxy`. This is the reference
|
|
134
|
+
* oracle and permanent JS fallback for the crate's `compute_aabbs`, and is
|
|
135
|
+
* bit-identical to `Entity.getWorldBounds` — same corner-selection bit trick
|
|
136
|
+
* (`i&1`/`i&2`), same `a*x + c*y + e` / `b*x + d*y + f` op order, same
|
|
137
|
+
* `Math.min`/`Math.max` accumulation over exactly four corners.
|
|
138
|
+
*
|
|
139
|
+
* Must run AFTER {@link composeJS} (or a `compose_*` kernel), which fills the
|
|
140
|
+
* world matrices this reads. A flat pass over `[0, count)` — the world matrices
|
|
141
|
+
* already encode the hierarchy, so no run walk is needed.
|
|
142
|
+
*/
|
|
143
|
+
export declare function computeAabbsJS(s: TransformStore): void;
|
|
144
|
+
/** Read the world-space AABB at store index `i` (valid after `computeAabbsJS`
|
|
145
|
+
* or the crate's `compute_aabbs`). */
|
|
146
|
+
export declare function readAabb(s: TransformStore, i: number): WorldAabb;
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vectojs/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.16.0",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -46,26 +46,35 @@
|
|
|
46
46
|
"types": "./dist/text/index.d.ts",
|
|
47
47
|
"import": "./dist/text.mjs",
|
|
48
48
|
"require": "./dist/text.js"
|
|
49
|
-
}
|
|
49
|
+
},
|
|
50
|
+
"./wasm": {
|
|
51
|
+
"types": "./dist/wasm/asset.d.ts",
|
|
52
|
+
"import": "./dist/wasm/asset.mjs",
|
|
53
|
+
"require": "./dist/wasm/asset.js"
|
|
54
|
+
},
|
|
55
|
+
"./vectojs_core.wasm": "./dist/wasm/vectojs_core.wasm"
|
|
50
56
|
},
|
|
51
57
|
"files": [
|
|
52
58
|
"dist",
|
|
53
59
|
"THIRD_PARTY_NOTICES.md"
|
|
54
60
|
],
|
|
55
61
|
"scripts": {
|
|
56
|
-
"
|
|
57
|
-
"build": "node scripts/build-worker.js && tsup && tsc -p tsconfig.build.json",
|
|
62
|
+
"build": "(cd ../math && bun run build) && (cd ../text && bun run build) && (cd ../layout && bun run build) && (cd ../animation && bun run build) && tsup && tsc -p tsconfig.build.json",
|
|
58
63
|
"test": "vitest run",
|
|
59
64
|
"test:e2e": "bun e2e/hidpi.e2e.ts && bun e2e/text-projection.e2e.ts"
|
|
60
65
|
},
|
|
61
|
-
"dependencies": {
|
|
66
|
+
"dependencies": {
|
|
67
|
+
"@vectojs/animation": "^0.1.1",
|
|
68
|
+
"@vectojs/layout": "^0.2.0",
|
|
69
|
+
"@vectojs/math": "^0.1.1",
|
|
70
|
+
"@vectojs/text": "^0.2.0"
|
|
71
|
+
},
|
|
62
72
|
"devDependencies": {
|
|
63
73
|
"@vitest/coverage-v8": "^4.1.10",
|
|
64
|
-
"bidi-js": "^1.0.3",
|
|
65
74
|
"esbuild": "^0.28.1",
|
|
66
75
|
"jsdom": "^29.1.1",
|
|
76
|
+
"puppeteer-core": "^25.3.0",
|
|
67
77
|
"tsup": "^8.3.5",
|
|
68
|
-
"vitest": "^4.1.10"
|
|
69
|
-
"puppeteer-core": "^25.3.0"
|
|
78
|
+
"vitest": "^4.1.10"
|
|
70
79
|
}
|
|
71
80
|
}
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { type EasingFn, type EasingName } from './easing';
|
|
2
|
-
export interface SpringConfig {
|
|
3
|
-
stiffness?: number;
|
|
4
|
-
damping?: number;
|
|
5
|
-
mass?: number;
|
|
6
|
-
}
|
|
7
|
-
export interface TweenConfig {
|
|
8
|
-
duration: number;
|
|
9
|
-
easing?: EasingName | EasingFn;
|
|
10
|
-
delay?: number;
|
|
11
|
-
}
|
|
12
|
-
/** A motion config. Presence of `duration` selects a tween; otherwise a spring. */
|
|
13
|
-
export type MotionConfig = 'spring' | SpringConfig | TweenConfig;
|
|
14
|
-
export declare function isTweenConfig(c: MotionConfig): c is TweenConfig;
|
|
15
|
-
/** Backs one animating property. Ticked in ms; writes `value`. */
|
|
16
|
-
export interface PropertyDriver {
|
|
17
|
-
value: number;
|
|
18
|
-
/** The current destination — applied exactly when the animation completes, so a
|
|
19
|
-
* finished spring lands on target rather than within its rest epsilon. */
|
|
20
|
-
readonly target: number;
|
|
21
|
-
/** Change the destination. Spring keeps velocity; tween restarts from current value. */
|
|
22
|
-
retarget(to: number): void;
|
|
23
|
-
tick(dtMs: number): void;
|
|
24
|
-
isDone(): boolean;
|
|
25
|
-
}
|
|
26
|
-
export declare class TweenDriver implements PropertyDriver {
|
|
27
|
-
value: number;
|
|
28
|
-
private from;
|
|
29
|
-
private to;
|
|
30
|
-
private elapsed;
|
|
31
|
-
private readonly duration;
|
|
32
|
-
private readonly delay;
|
|
33
|
-
private readonly ease;
|
|
34
|
-
constructor(from: number, to: number, cfg: TweenConfig);
|
|
35
|
-
get target(): number;
|
|
36
|
-
retarget(to: number): void;
|
|
37
|
-
tick(dtMs: number): void;
|
|
38
|
-
isDone(): boolean;
|
|
39
|
-
}
|
|
40
|
-
export declare class SpringDriver implements PropertyDriver {
|
|
41
|
-
private spring;
|
|
42
|
-
constructor(from: number, to: number, cfg: SpringConfig);
|
|
43
|
-
get value(): number;
|
|
44
|
-
get target(): number;
|
|
45
|
-
retarget(to: number): void;
|
|
46
|
-
tick(dtMs: number): void;
|
|
47
|
-
isDone(): boolean;
|
|
48
|
-
}
|