@fundamental-engine/three 0.4.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.
@@ -0,0 +1,58 @@
1
+ /**
2
+ * `ParticlePool` — the particle bridge. The engine runs headless (`render: 'none'`) and exposes its
3
+ * swarm through `FieldHandle.readParticles(buf)` (stride 5: `x, y, z, heat, size`). This pool pulls
4
+ * that buffer each frame and writes it onto a `THREE.Points` geometry via the `FieldProjection` —
5
+ * position from `(x, y, z)`, an `aHeat`/`aSize` attribute per vertex for the material. Zero per-frame
6
+ * allocation: the typed buffers are sized once to `capacity`.
7
+ *
8
+ * The conversion (`write`) is a pure function of the packed buffer + projection, so it is unit-
9
+ * testable without a WebGL context; only the live `sync` needs a running field.
10
+ */
11
+ import { BufferGeometry, Points, ShaderMaterial } from 'three';
12
+ import type { FieldProjection } from './project.ts';
13
+ export interface ParticleStyle {
14
+ /** color of cool (heat 0) matter (default a dim slate). */
15
+ base?: string;
16
+ /** color matter blends toward as it heats (default the field accent blue). */
17
+ accent?: string;
18
+ /** overall point-size multiplier (default 1). */
19
+ size?: number;
20
+ /** perspective focal term — larger = points shrink less with distance (default 300). */
21
+ focal?: number;
22
+ }
23
+ export interface ParticlePoolOptions {
24
+ /** maximum particles the buffers hold; the engine's live count is clamped to this. */
25
+ capacity: number;
26
+ /** the 2D↔3D mapping. */
27
+ projection: FieldProjection;
28
+ /** appearance. */
29
+ style?: ParticleStyle;
30
+ }
31
+ export declare class ParticlePool {
32
+ readonly points: Points;
33
+ readonly geometry: BufferGeometry;
34
+ readonly material: ShaderMaterial;
35
+ readonly capacity: number;
36
+ private readonly projection;
37
+ /** stride-4 staging buffer `readParticles` fills: `[x, y, heat, size, …]`. */
38
+ private readonly packed;
39
+ private readonly position;
40
+ private readonly aHeat;
41
+ private readonly aSize;
42
+ private count;
43
+ constructor(opts: ParticlePoolOptions);
44
+ /** the live particle count written by the last `sync`/`write`. */
45
+ get size(): number;
46
+ /** pull from a `readParticles`-shaped reader and push onto the geometry; returns the count. */
47
+ sync(read: (out: Float32Array) => number): number;
48
+ /**
49
+ * Convert the first `n` particles in the staging buffer into geometry attributes. Pure given the
50
+ * projection — call directly in a test with a pre-filled `packed` buffer (via `staging`).
51
+ */
52
+ write(n: number): number;
53
+ /** the stride-5 staging buffer (`[x, y, z, heat, size]`) — exposed for tests to fill before `write`. */
54
+ get staging(): Float32Array;
55
+ /** release GPU resources. */
56
+ dispose(): void;
57
+ }
58
+ //# sourceMappingURL=particles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"particles.d.ts","sourceRoot":"","sources":["../src/particles.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAEL,cAAc,EAGd,MAAM,EACN,cAAc,EAEf,MAAM,OAAO,CAAC;AACf,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAIpD,MAAM,WAAW,aAAa;IAC5B,2DAA2D;IAC3D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,8EAA8E;IAC9E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iDAAiD;IACjD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,wFAAwF;IACxF,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,mBAAmB;IAClC,sFAAsF;IACtF,QAAQ,EAAE,MAAM,CAAC;IACjB,yBAAyB;IACzB,UAAU,EAAE,eAAe,CAAC;IAC5B,kBAAkB;IAClB,KAAK,CAAC,EAAE,aAAa,CAAC;CACvB;AA2CD,qBAAa,YAAY;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC;IAClC,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC;IAClC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAkB;IAC7C,8EAA8E;IAC9E,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAe;IACtC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAyB;IAClD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAyB;IAC/C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAyB;IAC/C,OAAO,CAAC,KAAK,CAAK;gBAEN,IAAI,EAAE,mBAAmB;IAsBrC,kEAAkE;IAClE,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,+FAA+F;IAC/F,IAAI,CAAC,IAAI,EAAE,CAAC,GAAG,EAAE,YAAY,KAAK,MAAM,GAAG,MAAM;IAIjD;;;OAGG;IACH,KAAK,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM;IA6BxB,wGAAwG;IACxG,IAAI,OAAO,IAAI,YAAY,CAE1B;IAED,6BAA6B;IAC7B,OAAO,IAAI,IAAI;CAIhB"}
@@ -0,0 +1,127 @@
1
+ /**
2
+ * `ParticlePool` — the particle bridge. The engine runs headless (`render: 'none'`) and exposes its
3
+ * swarm through `FieldHandle.readParticles(buf)` (stride 5: `x, y, z, heat, size`). This pool pulls
4
+ * that buffer each frame and writes it onto a `THREE.Points` geometry via the `FieldProjection` —
5
+ * position from `(x, y, z)`, an `aHeat`/`aSize` attribute per vertex for the material. Zero per-frame
6
+ * allocation: the typed buffers are sized once to `capacity`.
7
+ *
8
+ * The conversion (`write`) is a pure function of the packed buffer + projection, so it is unit-
9
+ * testable without a WebGL context; only the live `sync` needs a running field.
10
+ */
11
+ import { AdditiveBlending, BufferGeometry, Color, Float32BufferAttribute, Points, ShaderMaterial, Vector3, } from 'three';
12
+ const _v = new Vector3();
13
+ /** The shader: color by `aHeat` (base→accent), size by `aSize` with perspective attenuation. */
14
+ function particleMaterial(style) {
15
+ return new ShaderMaterial({
16
+ uniforms: {
17
+ uBase: { value: new Color(style.base ?? '#3a4a63') },
18
+ uAccent: { value: new Color(style.accent ?? '#4da3ff') },
19
+ uSize: { value: style.size ?? 1 },
20
+ uFocal: { value: style.focal ?? 300 },
21
+ },
22
+ vertexShader: /* glsl */ `
23
+ attribute float aHeat;
24
+ attribute float aSize;
25
+ varying float vHeat;
26
+ uniform float uSize;
27
+ uniform float uFocal;
28
+ void main() {
29
+ vHeat = aHeat;
30
+ vec4 mv = modelViewMatrix * vec4(position, 1.0);
31
+ float s = aSize * uSize * (uFocal / max(0.0001, -mv.z));
32
+ gl_PointSize = clamp(s, 1.0, 48.0);
33
+ gl_Position = projectionMatrix * mv;
34
+ }
35
+ `,
36
+ fragmentShader: /* glsl */ `
37
+ varying float vHeat;
38
+ uniform vec3 uBase;
39
+ uniform vec3 uAccent;
40
+ void main() {
41
+ float r = length(gl_PointCoord - vec2(0.5));
42
+ if (r > 0.5) discard;
43
+ float glow = smoothstep(0.5, 0.0, r);
44
+ vec3 c = mix(uBase, uAccent, clamp(vHeat, 0.0, 1.0));
45
+ gl_FragColor = vec4(c, glow);
46
+ }
47
+ `,
48
+ transparent: true,
49
+ depthWrite: false,
50
+ blending: AdditiveBlending,
51
+ });
52
+ }
53
+ export class ParticlePool {
54
+ points;
55
+ geometry;
56
+ material;
57
+ capacity;
58
+ projection;
59
+ /** stride-4 staging buffer `readParticles` fills: `[x, y, heat, size, …]`. */
60
+ packed;
61
+ position;
62
+ aHeat;
63
+ aSize;
64
+ count = 0;
65
+ constructor(opts) {
66
+ this.capacity = Math.max(1, opts.capacity | 0);
67
+ this.projection = opts.projection;
68
+ this.packed = new Float32Array(this.capacity * 5);
69
+ this.geometry = new BufferGeometry();
70
+ this.position = new Float32BufferAttribute(new Float32Array(this.capacity * 3), 3);
71
+ this.aHeat = new Float32BufferAttribute(new Float32Array(this.capacity), 1);
72
+ this.aSize = new Float32BufferAttribute(new Float32Array(this.capacity), 1);
73
+ this.position.setUsage(35048 /* DynamicDrawUsage */);
74
+ this.aHeat.setUsage(35048);
75
+ this.aSize.setUsage(35048);
76
+ this.geometry.setAttribute('position', this.position);
77
+ this.geometry.setAttribute('aHeat', this.aHeat);
78
+ this.geometry.setAttribute('aSize', this.aSize);
79
+ this.geometry.setDrawRange(0, 0);
80
+ this.material = particleMaterial(opts.style ?? {});
81
+ this.points = new Points(this.geometry, this.material);
82
+ this.points.frustumCulled = false; // positions live in the attribute, not a static bound
83
+ }
84
+ /** the live particle count written by the last `sync`/`write`. */
85
+ get size() {
86
+ return this.count;
87
+ }
88
+ /** pull from a `readParticles`-shaped reader and push onto the geometry; returns the count. */
89
+ sync(read) {
90
+ return this.write(read(this.packed));
91
+ }
92
+ /**
93
+ * Convert the first `n` particles in the staging buffer into geometry attributes. Pure given the
94
+ * projection — call directly in a test with a pre-filled `packed` buffer (via `staging`).
95
+ */
96
+ write(n) {
97
+ const count = Math.min(n, this.capacity);
98
+ const pos = this.position.array;
99
+ const heat = this.aHeat.array;
100
+ const sz = this.aSize.array;
101
+ for (let i = 0; i < count; i++) {
102
+ const o = i * 5; // [x, y, z, heat, size]
103
+ this.projection.toWorld(this.packed[o], this.packed[o + 1], this.packed[o + 2], this.packed[o + 3], this.packed[o + 4], _v);
104
+ pos[i * 3] = _v.x;
105
+ pos[i * 3 + 1] = _v.y;
106
+ pos[i * 3 + 2] = _v.z;
107
+ heat[i] = this.packed[o + 3];
108
+ sz[i] = this.packed[o + 4];
109
+ }
110
+ this.count = count;
111
+ this.geometry.setDrawRange(0, count);
112
+ this.position.needsUpdate = true;
113
+ this.aHeat.needsUpdate = true;
114
+ this.aSize.needsUpdate = true;
115
+ return count;
116
+ }
117
+ /** the stride-5 staging buffer (`[x, y, z, heat, size]`) — exposed for tests to fill before `write`. */
118
+ get staging() {
119
+ return this.packed;
120
+ }
121
+ /** release GPU resources. */
122
+ dispose() {
123
+ this.geometry.dispose();
124
+ this.material.dispose();
125
+ }
126
+ }
127
+ //# sourceMappingURL=particles.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"particles.js","sourceRoot":"","sources":["../src/particles.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,111 @@
1
+ /**
2
+ * `FieldProjection` — the 2D↔3D coordinate seam. The engine integrates in a flat, CSS-pixel field
3
+ * space (`0..width`, `0..height`); a Three.js scene lives in 3D world units. Every renderer-facing
4
+ * mapping goes through this one interface so the plane model ships now and a volumetric mode can
5
+ * slot in later without changing `FieldLayer` or the backend.
6
+ *
7
+ * - `PlaneProjection`: the field stands up on a quad — pixels map to world units on a plane; the
8
+ * engine's `z` is ignored and `z` is instead lifted from per-particle `heat` for stylistic relief.
9
+ * The right choice for a flat (`depth`-less) field.
10
+ * - `VolumeProjection`: maps the engine's real depth lane (`z ∈ [0, depth)`, the opt-in z axis from
11
+ * z-axis.md) onto a world depth range — a genuinely volumetric swarm. Use it when the field was
12
+ * created with `depth > 0`.
13
+ *
14
+ * Both implement one interface, so `FieldLayer` and `threeBackend` are unchanged by the choice.
15
+ */
16
+ import { Vector3 } from 'three';
17
+ export interface FieldProjection {
18
+ /** the engine's field-space size in CSS pixels — feeds `FieldHost.viewport()`. */
19
+ size(): {
20
+ width: number;
21
+ height: number;
22
+ };
23
+ /**
24
+ * field point `(x, y, z)` + per-particle scalars → world position (writes `target` if given).
25
+ * `z` is the engine's depth lane (always `0` in a flat field); a `PlaneProjection` ignores it.
26
+ */
27
+ toWorld(x: number, y: number, z: number, heat: number, size: number, target?: Vector3): Vector3;
28
+ /** world position → field pixel, for projecting scene objects back onto the field (`z` ignored). */
29
+ toField(p: Vector3): {
30
+ x: number;
31
+ y: number;
32
+ };
33
+ }
34
+ export interface PlaneProjectionOptions {
35
+ /** field-space width in CSS pixels (default 1000). */
36
+ width?: number;
37
+ /** field-space height in CSS pixels (default 600). */
38
+ height?: number;
39
+ /** world units per field pixel (default 0.01 → a 1000px field is 10 world units wide). */
40
+ scale?: number;
41
+ /** world-space `z` lift at `heat === 1`; `0` keeps the field perfectly flat (default 0). */
42
+ relief?: number;
43
+ /** center the plane on the world origin (default true); when false, `(0,0)` field = origin. */
44
+ center?: boolean;
45
+ }
46
+ /**
47
+ * The field on a plane: `(x, y)` field pixels map onto the world XY plane (screen-`y`-down flipped
48
+ * to world-`y`-up), with `z` lifted from `heat`. Allocation-free on the hot path — pass a reused
49
+ * `target` Vector3 to `toWorld`.
50
+ */
51
+ export declare class PlaneProjection implements FieldProjection {
52
+ readonly width: number;
53
+ readonly height: number;
54
+ readonly scale: number;
55
+ readonly relief: number;
56
+ readonly center: boolean;
57
+ constructor(opts?: PlaneProjectionOptions);
58
+ size(): {
59
+ width: number;
60
+ height: number;
61
+ };
62
+ toWorld(x: number, y: number, _z: number, heat: number, _size: number, target?: Vector3): Vector3;
63
+ toField(p: Vector3): {
64
+ x: number;
65
+ y: number;
66
+ };
67
+ }
68
+ export interface VolumeProjectionOptions {
69
+ /** field-space width in CSS pixels (default 1000). */
70
+ width?: number;
71
+ /** field-space height in CSS pixels (default 600). */
72
+ height?: number;
73
+ /** world units per field pixel in x/y (default 0.01). */
74
+ scale?: number;
75
+ /** the engine `depth` (z-axis.md) this maps — match `FieldOptions.depth` (default 300). */
76
+ depth?: number;
77
+ /** world units per unit of engine z; defaults to `scale` (an isotropic volume). */
78
+ depthScale?: number;
79
+ /** center the field in x/y on the world origin (default true). */
80
+ center?: boolean;
81
+ /** center the volume in z too, so the page plane (`z = 0`) sits at world-z 0 and matter extends
82
+ * both ways; when false (default) the plane is at world-z 0 and depth extends toward +z. */
83
+ centerZ?: boolean;
84
+ }
85
+ /**
86
+ * The field as a volume: `(x, y)` map onto the world plane exactly as `PlaneProjection`, and the
87
+ * engine's real depth lane `z ∈ [0, depth)` maps onto a world depth range — a genuinely 3D swarm.
88
+ * Bodies (DOM elements) stay on the `z = 0` page plane; free matter drifts through the volume and is
89
+ * pulled gently back toward the plane (the engine's behavior), so the cloud reads as depth + parallax
90
+ * around the content rather than a slab.
91
+ */
92
+ export declare class VolumeProjection implements FieldProjection {
93
+ readonly width: number;
94
+ readonly height: number;
95
+ readonly scale: number;
96
+ readonly depth: number;
97
+ readonly depthScale: number;
98
+ readonly center: boolean;
99
+ readonly centerZ: boolean;
100
+ constructor(opts?: VolumeProjectionOptions);
101
+ size(): {
102
+ width: number;
103
+ height: number;
104
+ };
105
+ toWorld(x: number, y: number, z: number, _heat: number, _size: number, target?: Vector3): Vector3;
106
+ toField(p: Vector3): {
107
+ x: number;
108
+ y: number;
109
+ };
110
+ }
111
+ //# sourceMappingURL=project.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project.d.ts","sourceRoot":"","sources":["../src/project.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,95 @@
1
+ /**
2
+ * `FieldProjection` — the 2D↔3D coordinate seam. The engine integrates in a flat, CSS-pixel field
3
+ * space (`0..width`, `0..height`); a Three.js scene lives in 3D world units. Every renderer-facing
4
+ * mapping goes through this one interface so the plane model ships now and a volumetric mode can
5
+ * slot in later without changing `FieldLayer` or the backend.
6
+ *
7
+ * - `PlaneProjection`: the field stands up on a quad — pixels map to world units on a plane; the
8
+ * engine's `z` is ignored and `z` is instead lifted from per-particle `heat` for stylistic relief.
9
+ * The right choice for a flat (`depth`-less) field.
10
+ * - `VolumeProjection`: maps the engine's real depth lane (`z ∈ [0, depth)`, the opt-in z axis from
11
+ * z-axis.md) onto a world depth range — a genuinely volumetric swarm. Use it when the field was
12
+ * created with `depth > 0`.
13
+ *
14
+ * Both implement one interface, so `FieldLayer` and `threeBackend` are unchanged by the choice.
15
+ */
16
+ import { Vector3 } from 'three';
17
+ /**
18
+ * The field on a plane: `(x, y)` field pixels map onto the world XY plane (screen-`y`-down flipped
19
+ * to world-`y`-up), with `z` lifted from `heat`. Allocation-free on the hot path — pass a reused
20
+ * `target` Vector3 to `toWorld`.
21
+ */
22
+ export class PlaneProjection {
23
+ width;
24
+ height;
25
+ scale;
26
+ relief;
27
+ center;
28
+ constructor(opts = {}) {
29
+ this.width = opts.width ?? 1000;
30
+ this.height = opts.height ?? 600;
31
+ this.scale = opts.scale ?? 0.01;
32
+ this.relief = opts.relief ?? 0;
33
+ this.center = opts.center ?? true;
34
+ }
35
+ size() {
36
+ return { width: this.width, height: this.height };
37
+ }
38
+ toWorld(x, y, _z, heat, _size, target = new Vector3()) {
39
+ const cx = this.center ? this.width / 2 : 0;
40
+ const cy = this.center ? this.height / 2 : 0;
41
+ return target.set((x - cx) * this.scale, (cy - y) * this.scale, // screen-y is down; world-y is up
42
+ heat * this.relief);
43
+ }
44
+ toField(p) {
45
+ const cx = this.center ? this.width / 2 : 0;
46
+ const cy = this.center ? this.height / 2 : 0;
47
+ return {
48
+ x: p.x / this.scale + cx,
49
+ y: cy - p.y / this.scale,
50
+ };
51
+ }
52
+ }
53
+ /**
54
+ * The field as a volume: `(x, y)` map onto the world plane exactly as `PlaneProjection`, and the
55
+ * engine's real depth lane `z ∈ [0, depth)` maps onto a world depth range — a genuinely 3D swarm.
56
+ * Bodies (DOM elements) stay on the `z = 0` page plane; free matter drifts through the volume and is
57
+ * pulled gently back toward the plane (the engine's behavior), so the cloud reads as depth + parallax
58
+ * around the content rather than a slab.
59
+ */
60
+ export class VolumeProjection {
61
+ width;
62
+ height;
63
+ scale;
64
+ depth;
65
+ depthScale;
66
+ center;
67
+ centerZ;
68
+ constructor(opts = {}) {
69
+ this.width = opts.width ?? 1000;
70
+ this.height = opts.height ?? 600;
71
+ this.scale = opts.scale ?? 0.01;
72
+ this.depth = opts.depth ?? 300;
73
+ this.depthScale = opts.depthScale ?? this.scale;
74
+ this.center = opts.center ?? true;
75
+ this.centerZ = opts.centerZ ?? false;
76
+ }
77
+ size() {
78
+ return { width: this.width, height: this.height };
79
+ }
80
+ toWorld(x, y, z, _heat, _size, target = new Vector3()) {
81
+ const cx = this.center ? this.width / 2 : 0;
82
+ const cy = this.center ? this.height / 2 : 0;
83
+ const cz = this.centerZ ? this.depth / 2 : 0;
84
+ return target.set((x - cx) * this.scale, (cy - y) * this.scale, (z - cz) * this.depthScale);
85
+ }
86
+ toField(p) {
87
+ const cx = this.center ? this.width / 2 : 0;
88
+ const cy = this.center ? this.height / 2 : 0;
89
+ return {
90
+ x: p.x / this.scale + cx,
91
+ y: cy - p.y / this.scale,
92
+ };
93
+ }
94
+ }
95
+ //# sourceMappingURL=project.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project.js","sourceRoot":"","sources":["../src/project.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC;AA2BhC;;;;GAIG;AACH,MAAM,OAAO,eAAe;IACjB,KAAK,CAAS;IACd,MAAM,CAAS;IACf,KAAK,CAAS;IACd,MAAM,CAAS;IACf,MAAM,CAAU;IAEzB,YAAY,OAA+B,EAAE;QAC3C,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC;QAChC,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,GAAG,CAAC;QACjC,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC;QAChC,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,CAAC,CAAC;QAC/B,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC;IACpC,CAAC;IAED,IAAI;QACF,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC;IACpD,CAAC;IAED,OAAO,CAAC,CAAS,EAAE,CAAS,EAAE,EAAU,EAAE,IAAY,EAAE,KAAa,EAAE,MAAM,GAAG,IAAI,OAAO,EAAE;QAC3F,MAAM,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC5C,MAAM,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC7C,OAAO,MAAM,CAAC,GAAG,CACf,CAAC,CAAC,GAAG,EAAE,CAAC,GAAG,IAAI,CAAC,KAAK,EACrB,CAAC,EAAE,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,EAAE,kCAAkC;QACzD,IAAI,GAAG,IAAI,CAAC,MAAM,CACnB,CAAC;IACJ,CAAC;IAED,OAAO,CAAC,CAAU;QAChB,MAAM,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC5C,MAAM,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC7C,OAAO;YACL,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,GAAG,EAAE;YACxB,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK;SACzB,CAAC;IACJ,CAAC;CACF;AAoBD;;;;;;GAMG;AACH,MAAM,OAAO,gBAAgB;IAClB,KAAK,CAAS;IACd,MAAM,CAAS;IACf,KAAK,CAAS;IACd,KAAK,CAAS;IACd,UAAU,CAAS;IACnB,MAAM,CAAU;IAChB,OAAO,CAAU;IAE1B,YAAY,OAAgC,EAAE;QAC5C,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC;QAChC,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,GAAG,CAAC;QACjC,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC;QAChC,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,GAAG,CAAC;QAC/B,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC,KAAK,CAAC;QAChD,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC;QAClC,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,KAAK,CAAC;IACvC,CAAC;IAED,IAAI;QACF,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC;IACpD,CAAC;IAED,OAAO,CAAC,CAAS,EAAE,CAAS,EAAE,CAAS,EAAE,KAAa,EAAE,KAAa,EAAE,MAAM,GAAG,IAAI,OAAO,EAAE;QAC3F,MAAM,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC5C,MAAM,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC7C,MAAM,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC7C,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC,GAAG,IAAI,CAAC,UAAU,CAAC,CAAC;IAC9F,CAAC;IAED,OAAO,CAAC,CAAU;QAChB,MAAM,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC5C,MAAM,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC7C,OAAO;YACL,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,GAAG,EAAE;YACxB,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK;SACzB,CAAC;IACJ,CAAC;CACF"}
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Native 3D field visuals — the field as geometry, not particles. Because the engine is queryable at
3
+ * any point (`FieldHandle.sample`), you can build visuals the 2D canvas can't: an instanced arrow
4
+ * **vector field** and **streamline tubes** that trace the flow through the scene. Both read the live
5
+ * field each `update()`, mapped through the same `FieldProjection` the swarm and bodies use, so they
6
+ * register exactly.
7
+ *
8
+ * The tracing/sampling core is pure (no WebGL) and unit-tested; only the geometry needs a renderer.
9
+ */
10
+ import { Group } from 'three';
11
+ import type { FieldProjection } from './project.ts';
12
+ /** Anything with the engine's `sample` — a `FieldLayer`, a `FieldHandle`, or a test stub. */
13
+ export interface FieldSampler {
14
+ sample(x: number, y: number): {
15
+ x: number;
16
+ y: number;
17
+ };
18
+ }
19
+ /**
20
+ * Trace one streamline from a seed, integrating the field direction. Pure (field-pixel space) —
21
+ * returns the polyline of points it passes through. Stops at the viewport edge, a stall (near-zero
22
+ * field), or `maxSteps`.
23
+ */
24
+ export declare function traceStreamline(field: FieldSampler, seed: {
25
+ x: number;
26
+ y: number;
27
+ }, opts: {
28
+ width: number;
29
+ height: number;
30
+ maxSteps?: number;
31
+ stepLen?: number;
32
+ minMag?: number;
33
+ }): {
34
+ x: number;
35
+ y: number;
36
+ }[];
37
+ export interface VectorFieldOptions {
38
+ /** the 2D↔3D mapping (share the layer's). */
39
+ projection: FieldProjection;
40
+ /** grid spacing in field pixels (default 64). */
41
+ step?: number;
42
+ /** longest arrow in world units, at the peak sampled magnitude (default 0.4). */
43
+ maxLength?: number;
44
+ /** arrow color (default a field blue). */
45
+ color?: string;
46
+ /** world-z the arrows sit at (default 0.02, just off the plane). */
47
+ z?: number;
48
+ }
49
+ export interface FieldVisual {
50
+ /** add this to your scene. */
51
+ readonly object: Group;
52
+ /** re-sample the live field and refresh the geometry — call when the field changed. */
53
+ update(): void;
54
+ /** release GPU resources. */
55
+ dispose(): void;
56
+ }
57
+ /** An instanced grid of arrows showing the field direction + magnitude across the plane. */
58
+ export declare function vectorField(field: FieldSampler, opts: VectorFieldOptions): FieldVisual;
59
+ export interface StreamlineTubesOptions {
60
+ /** the 2D↔3D mapping (share the layer's). */
61
+ projection: FieldProjection;
62
+ /** seed points in field pixels; omit for an auto grid at `step` spacing. */
63
+ seeds?: {
64
+ x: number;
65
+ y: number;
66
+ }[];
67
+ /** auto-seed grid spacing in field pixels when `seeds` is omitted (default 120). */
68
+ step?: number;
69
+ /** integration steps per line (default 80) and step length in field px (default 8). */
70
+ maxSteps?: number;
71
+ stepLen?: number;
72
+ /** tube radius in world units (default 0.03). */
73
+ radius?: number;
74
+ /** tube color (default a field blue). */
75
+ color?: string;
76
+ /** world-z the tubes sit at (default 0.02). */
77
+ z?: number;
78
+ }
79
+ /** Flow lines traced through the field, rendered as tubes — the field's structure as geometry. */
80
+ export declare function streamlineTubes(field: FieldSampler, opts: StreamlineTubesOptions): FieldVisual;
81
+ //# sourceMappingURL=samplers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"samplers.d.ts","sourceRoot":"","sources":["../src/samplers.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAIL,KAAK,EAQN,MAAM,OAAO,CAAC;AACf,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAEpD,6FAA6F;AAC7F,MAAM,WAAW,YAAY;IAC3B,MAAM,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CACxD;AAQD;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,YAAY,EACnB,IAAI,EAAE;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,EAC9B,IAAI,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAC5F;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,EAAE,CAe5B;AAED,MAAM,WAAW,kBAAkB;IACjC,6CAA6C;IAC7C,UAAU,EAAE,eAAe,CAAC;IAC5B,iDAAiD;IACjD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iFAAiF;IACjF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,0CAA0C;IAC1C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,oEAAoE;IACpE,CAAC,CAAC,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,WAAW,WAAW;IAC1B,8BAA8B;IAC9B,QAAQ,CAAC,MAAM,EAAE,KAAK,CAAC;IACvB,uFAAuF;IACvF,MAAM,IAAI,IAAI,CAAC;IACf,6BAA6B;IAC7B,OAAO,IAAI,IAAI,CAAC;CACjB;AAED,4FAA4F;AAC5F,wBAAgB,WAAW,CAAC,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,kBAAkB,GAAG,WAAW,CA6DtF;AAED,MAAM,WAAW,sBAAsB;IACrC,6CAA6C;IAC7C,UAAU,EAAE,eAAe,CAAC;IAC5B,4EAA4E;IAC5E,KAAK,CAAC,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACnC,oFAAoF;IACpF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,uFAAuF;IACvF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,iDAAiD;IACjD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,yCAAyC;IACzC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,+CAA+C;IAC/C,CAAC,CAAC,EAAE,MAAM,CAAC;CACZ;AAED,kGAAkG;AAClG,wBAAgB,eAAe,CAAC,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,sBAAsB,GAAG,WAAW,CAuC9F"}
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Native 3D field visuals — the field as geometry, not particles. Because the engine is queryable at
3
+ * any point (`FieldHandle.sample`), you can build visuals the 2D canvas can't: an instanced arrow
4
+ * **vector field** and **streamline tubes** that trace the flow through the scene. Both read the live
5
+ * field each `update()`, mapped through the same `FieldProjection` the swarm and bodies use, so they
6
+ * register exactly.
7
+ *
8
+ * The tracing/sampling core is pure (no WebGL) and unit-tested; only the geometry needs a renderer.
9
+ */
10
+ import { CatmullRomCurve3, Color, ConeGeometry, Group, InstancedMesh, Matrix4, Mesh, MeshBasicMaterial, Quaternion, TubeGeometry, Vector3, } from 'three';
11
+ const _pos = new Vector3();
12
+ const _quat = new Quaternion();
13
+ const _scl = new Vector3();
14
+ const _mat = new Matrix4();
15
+ const _zAxis = new Vector3(0, 0, 1);
16
+ /**
17
+ * Trace one streamline from a seed, integrating the field direction. Pure (field-pixel space) —
18
+ * returns the polyline of points it passes through. Stops at the viewport edge, a stall (near-zero
19
+ * field), or `maxSteps`.
20
+ */
21
+ export function traceStreamline(field, seed, opts) {
22
+ const { width, height, maxSteps = 80, stepLen = 8, minMag = 1e-6 } = opts;
23
+ const pts = [{ x: seed.x, y: seed.y }];
24
+ let x = seed.x;
25
+ let y = seed.y;
26
+ for (let i = 0; i < maxSteps; i++) {
27
+ const v = field.sample(x, y);
28
+ const m = Math.hypot(v.x, v.y);
29
+ if (m < minMag)
30
+ break;
31
+ x += (v.x / m) * stepLen;
32
+ y += (v.y / m) * stepLen;
33
+ if (x < 0 || y < 0 || x > width || y > height)
34
+ break;
35
+ pts.push({ x, y });
36
+ }
37
+ return pts;
38
+ }
39
+ /** An instanced grid of arrows showing the field direction + magnitude across the plane. */
40
+ export function vectorField(field, opts) {
41
+ const { projection, step = 64, maxLength = 0.4, color = '#7cc0ff', z = 0.02 } = opts;
42
+ const { width, height } = projection.size();
43
+ const cols = Math.max(1, Math.floor(width / step));
44
+ const rows = Math.max(1, Math.floor(height / step));
45
+ const count = cols * rows;
46
+ // a cone pointing +X (so a Z-rotation aims it along the in-plane force direction)
47
+ const geo = new ConeGeometry(0.16, 1, 6);
48
+ geo.rotateZ(-Math.PI / 2);
49
+ geo.translate(0.5, 0, 0);
50
+ const material = new MeshBasicMaterial({ color: new Color(color), transparent: true, opacity: 0.85 });
51
+ const mesh = new InstancedMesh(geo, material, count);
52
+ mesh.frustumCulled = false;
53
+ const group = new Group();
54
+ group.add(mesh);
55
+ const update = () => {
56
+ let max = 1e-6;
57
+ const fx = [];
58
+ const fy = [];
59
+ const vx = [];
60
+ const vy = [];
61
+ for (let r = 0; r < rows; r++) {
62
+ for (let c = 0; c < cols; c++) {
63
+ const px = (c + 0.5) * step;
64
+ const py = (r + 0.5) * step;
65
+ const v = field.sample(px, py);
66
+ const m = Math.hypot(v.x, v.y);
67
+ if (m > max)
68
+ max = m;
69
+ fx.push(px);
70
+ fy.push(py);
71
+ vx.push(v.x);
72
+ vy.push(v.y);
73
+ }
74
+ }
75
+ for (let i = 0; i < count; i++) {
76
+ projection.toWorld(fx[i], fy[i], 0, 0, 0, _pos);
77
+ _pos.z = z;
78
+ const rel = Math.sqrt(Math.hypot(vx[i], vy[i]) / max); // sqrt compression so weak arrows read
79
+ // field-y is down → world-y up, so the world direction is (vx, -vy)
80
+ const angle = Math.atan2(-vy[i], vx[i]);
81
+ _quat.setFromAxisAngle(_zAxis, angle);
82
+ const len = Math.max(0.0001, rel * maxLength);
83
+ _scl.set(len, len, len);
84
+ _mat.compose(_pos, _quat, _scl);
85
+ mesh.setMatrixAt(i, _mat);
86
+ }
87
+ mesh.instanceMatrix.needsUpdate = true;
88
+ };
89
+ update();
90
+ return {
91
+ object: group,
92
+ update,
93
+ dispose: () => {
94
+ geo.dispose();
95
+ material.dispose();
96
+ },
97
+ };
98
+ }
99
+ /** Flow lines traced through the field, rendered as tubes — the field's structure as geometry. */
100
+ export function streamlineTubes(field, opts) {
101
+ const { projection, step = 120, maxSteps = 80, stepLen = 8, radius = 0.03, color = '#9fd4ff', z = 0.02 } = opts;
102
+ const { width, height } = projection.size();
103
+ const material = new MeshBasicMaterial({ color: new Color(color), transparent: true, opacity: 0.7 });
104
+ const group = new Group();
105
+ const seedPoints = () => {
106
+ if (opts.seeds)
107
+ return opts.seeds;
108
+ const out = [];
109
+ for (let y = step / 2; y < height; y += step)
110
+ for (let x = step / 2; x < width; x += step)
111
+ out.push({ x, y });
112
+ return out;
113
+ };
114
+ const update = () => {
115
+ for (const child of group.children)
116
+ child.geometry.dispose();
117
+ group.clear();
118
+ for (const seed of seedPoints()) {
119
+ const pts = traceStreamline(field, seed, { width, height, maxSteps, stepLen });
120
+ if (pts.length < 2)
121
+ continue;
122
+ const path = pts.map((p) => {
123
+ const w = projection.toWorld(p.x, p.y, 0, 0, 0, new Vector3());
124
+ w.z = z;
125
+ return w;
126
+ });
127
+ const tube = new TubeGeometry(new CatmullRomCurve3(path), Math.max(1, path.length - 1), radius, 6, false);
128
+ group.add(new Mesh(tube, material));
129
+ }
130
+ };
131
+ update();
132
+ return {
133
+ object: group,
134
+ update,
135
+ dispose: () => {
136
+ for (const child of group.children)
137
+ child.geometry.dispose();
138
+ group.clear();
139
+ material.dispose();
140
+ },
141
+ };
142
+ }
143
+ //# sourceMappingURL=samplers.js.map