@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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Zach Shallbetter
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,131 @@
1
+ # @fundamental-engine/three
2
+
3
+ **The Three.js door to [`@fundamental-engine/core`](../core)** — run the reciprocal field engine headless and
4
+ render its swarm as a `THREE.Points` layer in your own WebGL scene. The same physics the
5
+ `<field-root>` element and `@fundamental-engine/{vanilla,react}` wrap, bound to a 3D scene instead of the DOM.
6
+
7
+ → Live manual, Lab, and gallery at **[fundamental-engine.com](https://fundamental-engine.com)**.
8
+
9
+ ## Install
10
+
11
+ ```sh
12
+ npm i @fundamental-engine/three three
13
+ ```
14
+
15
+ `three` is a **peer dependency** — you bring your own version (**≥ 0.147**, the tested floor).
16
+ The package touches only long-stable three APIs (the newest are `InstancedMesh`, r109, and
17
+ `Object3D.clear()`, r123); it is built against modern three (0.169) and r147 is verified live in
18
+ a real integration.
19
+
20
+ ### No build step? (CDN / single-file pages)
21
+
22
+ The package is plain ESM, so a page with no bundler consumes it straight from a CDN. Pin the
23
+ `three` peer to **the same revision your page already uses** so the library and your scene share
24
+ one Three.js:
25
+
26
+ ```html
27
+ <script type="module">
28
+ // ?deps pins the peer; match it to your page's three version
29
+ import * as FieldUI from "https://esm.sh/@fundamental-engine/three@0.3.1?deps=three@0.147.0";
30
+ window.FieldUI = FieldUI; // hand it to classic scripts
31
+ window.dispatchEvent(new Event("fieldui-ready")); // module scripts are deferred — signal readiness
32
+ </script>
33
+ <script>
34
+ // a classic script can't await the module — start on the ready signal
35
+ function startField() {
36
+ const layer = window.FieldUI.createFieldLayer({ /* … */ });
37
+ // scene.add(layer.object); tick in your render loop
38
+ }
39
+ if (window.FieldUI) startField();
40
+ else window.addEventListener("fieldui-ready", startField, { once: true });
41
+ </script>
42
+ ```
43
+
44
+ Prefer a fully offline page? Bundle once with esbuild and commit the artifact, mapping the peer
45
+ onto your page's global `THREE`:
46
+
47
+ ```sh
48
+ echo "module.exports = window.THREE" > three-shim.cjs
49
+ npx esbuild node_modules/@fundamental-engine/three/dist/index.js --bundle --format=iife \
50
+ --global-name=FieldUI --alias:three=./three-shim.cjs --outfile=vendor/field-ui-three.js
51
+ ```
52
+
53
+ ## The particle bridge
54
+
55
+ The engine runs in signals-only mode (`render: 'none'`) and exposes its particle pool through
56
+ `FieldHandle.readParticles()`. `FieldLayer` pulls that each frame and writes it onto a `THREE.Points`
57
+ geometry via a `FieldProjection`.
58
+
59
+ ```ts
60
+ import * as THREE from 'three';
61
+ import { createFieldLayer, PlaneProjection } from '@fundamental-engine/three';
62
+
63
+ const scene = new THREE.Scene();
64
+ const renderer = new THREE.WebGLRenderer({ antialias: true });
65
+
66
+ const layer = createFieldLayer({
67
+ projection: new PlaneProjection({ relief: 2 }), // the field on a plane, z lifted from heat
68
+ renderer, // reads the device-pixel ratio
69
+ accent: '#4da3ff',
70
+ });
71
+ scene.add(layer.object);
72
+
73
+ renderer.setAnimationLoop(() => {
74
+ layer.tick(); // the engine self-steps; tick() pulls the latest swarm
75
+ renderer.render(scene, camera);
76
+ });
77
+ ```
78
+
79
+ `FieldLayer` implements the full `FieldHandle` surface, so `layer.burst()`, `layer.flowTo()`,
80
+ `layer.setFormation()`, and `layer.seed()` drive the 3D layer exactly as they drive the DOM field.
81
+
82
+ ## Flat plane or real volume
83
+
84
+ `FieldProjection` maps the engine's CSS-pixel field space to 3D world space:
85
+
86
+ - **`PlaneProjection`** — the field on a quad; `z` is lifted stylistically from per-particle `heat`.
87
+ The right choice for a flat field.
88
+ - **`VolumeProjection`** — maps the engine's real **depth lane** (`z ∈ [0, depth)`, the opt-in z axis)
89
+ onto a world depth range, for a genuinely volumetric swarm.
90
+
91
+ Pass `depth` and the layer defaults to a `VolumeProjection` automatically — bodies stay on the page
92
+ plane (`z = 0`) while free matter drifts through the volume and is pulled gently back, reading as depth
93
+ and parallax around the content:
94
+
95
+ ```ts
96
+ const layer = createFieldLayer({ depth: 300, renderer, accent: '#4da3ff' });
97
+ ```
98
+
99
+ Both projections implement one interface, so `FieldLayer` and `threeBackend` are unchanged by the choice.
100
+
101
+ ## RenderBackend (diagnostic overlays)
102
+
103
+ `threeBackend` implements the engine's structural drawing seam (`RenderBackend`), so the diagnostic
104
+ overlays — streamlines, field-lines, grid, contours, force-vector arrows — draw as scene geometry.
105
+ Inject it via the lower-level `createThreeField`:
106
+
107
+ ```ts
108
+ import { createThreeField, threeBackend, PlaneProjection } from '@fundamental-engine/three';
109
+
110
+ const projection = new PlaneProjection();
111
+ const overlay = threeBackend({ projection });
112
+ scene.add(overlay.object);
113
+
114
+ const field = createThreeField({
115
+ viewport: () => ({ ...projection.size(), dpr: renderer.getPixelRatio() }),
116
+ overlayBackend: overlay,
117
+ overlay: 'streamlines',
118
+ });
119
+ ```
120
+
121
+ The line overlays render fully; numeric label sprites (the `data` reading) are a tracked follow-up.
122
+
123
+ ## Building your own field visuals
124
+
125
+ The package re-exports the engine's field samplers — `forceAt` and `netField` — so you can drive
126
+ your own 3D visuals (streamline tubes, vector grids, density volumes) from the live field without a
127
+ second import.
128
+
129
+ ## License
130
+
131
+ MIT © Zach Shallbetter
@@ -0,0 +1,73 @@
1
+ /**
2
+ * `FieldAgent` — an `Object3D` that rides the field: the creatures primitive (#426). The swarm
3
+ * bridge renders free matter; an agent is a *specific* scene object (a bee, a fish, a drone) that
4
+ * samples the live field at its own position each frame, steers along the force, and writes its
5
+ * world position through the shared `FieldProjection`.
6
+ *
7
+ * Agents are **consumers, not bodies** — they feel the field but exert nothing back unless you also
8
+ * register them with `layer.addBody(...)`. The `sampler` is the small `FieldSampler` interface, so
9
+ * an agent can follow a whole layer, a raw handle, or any custom blend (a wind-only stub, a scaled
10
+ * mix). Pure math + `Object3D` writes: renderer-free and unit-testable.
11
+ *
12
+ * ```ts
13
+ * const bee = new FieldAgent(beeMesh, { projection, sampler: layer, maxSpeed: 90 });
14
+ * // render loop:
15
+ * bee.update(dt);
16
+ * ```
17
+ */
18
+ import type { Object3D } from 'three';
19
+ import type { FieldProjection } from './project.ts';
20
+ import type { FieldSampler } from './samplers.ts';
21
+ export interface FieldAgentOptions {
22
+ /** the 2D↔3D mapping (share the layer's so agents and swarm agree). */
23
+ projection: FieldProjection;
24
+ /** what to follow — anything with `sample(x, y)`: a `FieldLayer`, a `FieldHandle`, a stub. */
25
+ sampler: FieldSampler;
26
+ /** top speed in field px/s (default 80). */
27
+ maxSpeed?: number;
28
+ /** acceleration along the sampled force, px/s² per unit force (default 240). */
29
+ accel?: number;
30
+ /** velocity damping per second — higher coasts to rest sooner in a dead field (default 1.4). */
31
+ drag?: number;
32
+ /** random wander acceleration in px/s² so agents don't railroad one streamline (default 0). */
33
+ wander?: number;
34
+ /** optional world-y hover bob layered on the projected position. */
35
+ hover?: {
36
+ amp: number;
37
+ freq: number;
38
+ };
39
+ /** orient the object along its travel direction each update (default true). */
40
+ faceVelocity?: boolean;
41
+ /** deterministic random source for `wander` (default `Math.random`). */
42
+ rng?: () => number;
43
+ }
44
+ export declare class FieldAgent {
45
+ /** the scene object this agent drives. */
46
+ readonly object: Object3D;
47
+ /** position in field pixels — read/write (set it to spawn or teleport). */
48
+ fieldPosition: {
49
+ x: number;
50
+ y: number;
51
+ };
52
+ /** velocity in field px/s (read/write). */
53
+ velocity: {
54
+ x: number;
55
+ y: number;
56
+ };
57
+ private readonly projection;
58
+ private readonly sampler;
59
+ private readonly maxSpeed;
60
+ private readonly accel;
61
+ private readonly drag;
62
+ private readonly wander;
63
+ private readonly hover?;
64
+ private readonly faceVelocity;
65
+ private readonly rng;
66
+ private t;
67
+ private readonly phase;
68
+ private readonly look;
69
+ constructor(object: Object3D, opts: FieldAgentOptions);
70
+ /** sample → steer → integrate → write the object's position. Call once per frame with dt in s. */
71
+ update(dt: number): void;
72
+ }
73
+ //# sourceMappingURL=agents.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../src/agents.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAGH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AACtC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AACpD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAIlD,MAAM,WAAW,iBAAiB;IAChC,uEAAuE;IACvE,UAAU,EAAE,eAAe,CAAC;IAC5B,8FAA8F;IAC9F,OAAO,EAAE,YAAY,CAAC;IACtB,4CAA4C;IAC5C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gFAAgF;IAChF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gGAAgG;IAChG,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,+FAA+F;IAC/F,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,oEAAoE;IACpE,KAAK,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IACtC,+EAA+E;IAC/E,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,wEAAwE;IACxE,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED,qBAAa,UAAU;IACrB,0CAA0C;IAC1C,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC;IAC1B,2EAA2E;IAC3E,aAAa,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACxC,2CAA2C;IAC3C,QAAQ,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAEnC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAkB;IAC7C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAe;IACvC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAC9B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAgC;IACvD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAU;IACvC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,OAAO,CAAC,CAAC,CAAK;IACd,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAiB;gBAE1B,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,iBAAiB;IAuBrD,kGAAkG;IAClG,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;CAyCzB"}
package/dist/agents.js ADDED
@@ -0,0 +1,114 @@
1
+ /**
2
+ * `FieldAgent` — an `Object3D` that rides the field: the creatures primitive (#426). The swarm
3
+ * bridge renders free matter; an agent is a *specific* scene object (a bee, a fish, a drone) that
4
+ * samples the live field at its own position each frame, steers along the force, and writes its
5
+ * world position through the shared `FieldProjection`.
6
+ *
7
+ * Agents are **consumers, not bodies** — they feel the field but exert nothing back unless you also
8
+ * register them with `layer.addBody(...)`. The `sampler` is the small `FieldSampler` interface, so
9
+ * an agent can follow a whole layer, a raw handle, or any custom blend (a wind-only stub, a scaled
10
+ * mix). Pure math + `Object3D` writes: renderer-free and unit-testable.
11
+ *
12
+ * ```ts
13
+ * const bee = new FieldAgent(beeMesh, { projection, sampler: layer, maxSpeed: 90 });
14
+ * // render loop:
15
+ * bee.update(dt);
16
+ * ```
17
+ */
18
+ import { Vector3 } from 'three';
19
+ const _w = new Vector3();
20
+ export class FieldAgent {
21
+ /** the scene object this agent drives. */
22
+ object;
23
+ /** position in field pixels — read/write (set it to spawn or teleport). */
24
+ fieldPosition;
25
+ /** velocity in field px/s (read/write). */
26
+ velocity;
27
+ projection;
28
+ sampler;
29
+ maxSpeed;
30
+ accel;
31
+ drag;
32
+ wander;
33
+ hover;
34
+ faceVelocity;
35
+ rng;
36
+ t = 0;
37
+ phase;
38
+ look = new Vector3();
39
+ constructor(object, opts) {
40
+ this.object = object;
41
+ this.projection = opts.projection;
42
+ this.sampler = opts.sampler;
43
+ this.maxSpeed = opts.maxSpeed ?? 80;
44
+ this.accel = opts.accel ?? 240;
45
+ this.drag = opts.drag ?? 1.4;
46
+ this.wander = opts.wander ?? 0;
47
+ this.hover = opts.hover;
48
+ this.faceVelocity = opts.faceVelocity ?? true;
49
+ this.rng = opts.rng ?? Math.random;
50
+ this.phase = this.rng() * Math.PI * 2;
51
+ // spawn where the object currently sits (world → field); centre if it has no position yet
52
+ object.getWorldPosition(_w);
53
+ this.fieldPosition = this.projection.toField(_w);
54
+ if (!Number.isFinite(this.fieldPosition.x) || !Number.isFinite(this.fieldPosition.y)) {
55
+ const { width, height } = this.projection.size();
56
+ this.fieldPosition = { x: width / 2, y: height / 2 };
57
+ }
58
+ this.velocity = { x: 0, y: 0 };
59
+ }
60
+ /** sample → steer → integrate → write the object's position. Call once per frame with dt in s. */
61
+ update(dt) {
62
+ if (!(dt > 0))
63
+ return;
64
+ this.t += dt;
65
+ const p = this.fieldPosition;
66
+ const v = this.velocity;
67
+ const f = this.sampler.sample(p.x, p.y);
68
+ v.x += f.x * this.accel * dt;
69
+ v.y += f.y * this.accel * dt;
70
+ if (this.wander > 0) {
71
+ v.x += (this.rng() * 2 - 1) * this.wander * dt;
72
+ v.y += (this.rng() * 2 - 1) * this.wander * dt;
73
+ }
74
+ // drag, then clamp to top speed
75
+ const damp = Math.max(0, 1 - this.drag * dt);
76
+ v.x *= damp;
77
+ v.y *= damp;
78
+ const speed = Math.hypot(v.x, v.y);
79
+ if (speed > this.maxSpeed) {
80
+ v.x = (v.x / speed) * this.maxSpeed;
81
+ v.y = (v.y / speed) * this.maxSpeed;
82
+ }
83
+ p.x += v.x * dt;
84
+ p.y += v.y * dt;
85
+ // keep agents inside the field with a soft edge bounce
86
+ const { width, height } = this.projection.size();
87
+ if (p.x < 0) {
88
+ p.x = 0;
89
+ v.x = Math.abs(v.x);
90
+ }
91
+ if (p.y < 0) {
92
+ p.y = 0;
93
+ v.y = Math.abs(v.y);
94
+ }
95
+ if (p.x > width) {
96
+ p.x = width;
97
+ v.x = -Math.abs(v.x);
98
+ }
99
+ if (p.y > height) {
100
+ p.y = height;
101
+ v.y = -Math.abs(v.y);
102
+ }
103
+ this.projection.toWorld(p.x, p.y, 0, 0, 0, this.object.position);
104
+ if (this.hover)
105
+ this.object.position.y += Math.sin(this.t * this.hover.freq + this.phase) * this.hover.amp;
106
+ if (this.faceVelocity && speed > 1e-3) {
107
+ // look one step ahead along the travel direction, in world space
108
+ this.projection.toWorld(p.x + v.x * 0.1, p.y + v.y * 0.1, 0, 0, 0, this.look);
109
+ this.look.y = this.object.position.y;
110
+ this.object.lookAt(this.look);
111
+ }
112
+ }
113
+ }
114
+ //# sourceMappingURL=agents.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agents.js","sourceRoot":"","sources":["../src/agents.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * `threeBackend` — a `RenderBackend` (the engine's structural drawing seam, #373) that draws the
3
+ * overlay readings as Three.js geometry instead of a 2D canvas. Inject it via
4
+ * `createField({ overlayBackend })` (or `createThreeField`) and add `backend.object` to your scene.
5
+ *
6
+ * The engine describes WHAT to draw in CSS-pixel primitives; this backend owns HOW — mapping those
7
+ * pixels through the `FieldProjection` onto an overlay plane and batching them into a `LineSegments`
8
+ * (arrows, field-lines, grid, contours, traced paths) and a triangle `Mesh` (the data-chip plates).
9
+ * Per-stroke alpha is premultiplied into the vertex color and composited additively, so magnitude
10
+ * still reads. Geometry is rebuilt on each `clear()` (the engine clears once at the top of every
11
+ * overlay frame), so the scene always shows the latest readings.
12
+ *
13
+ * Scope: the line + rect primitives — i.e. every field-structure overlay — render fully. Text
14
+ * labels (the `data` reading's numeric chips) are not yet drawn; `measureText` is honored so the
15
+ * plates size correctly, and label sprites are a tracked follow-up. The line overlays need no text.
16
+ */
17
+ import { Group } from 'three';
18
+ import type { RenderBackend } from '@fundamental-engine/core';
19
+ import type { FieldProjection } from './project.ts';
20
+ export interface ThreeBackendOptions {
21
+ /** the 2D↔3D mapping (share the one your `FieldLayer` uses so overlay and swarm align). */
22
+ projection: FieldProjection;
23
+ /** world-space `z` the overlay draws at — put it just in front of a flat field (default 0.01). */
24
+ z?: number;
25
+ }
26
+ export interface ThreeBackend extends RenderBackend {
27
+ /** add this to your scene to show the overlay readings. */
28
+ readonly object: Group;
29
+ /** release GPU resources. */
30
+ dispose(): void;
31
+ }
32
+ export declare function threeBackend(opts: ThreeBackendOptions): ThreeBackend;
33
+ //# sourceMappingURL=backend.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backend.d.ts","sourceRoot":"","sources":["../src/backend.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAIL,KAAK,EAMN,MAAM,OAAO,CAAC;AACf,OAAO,KAAK,EAAE,aAAa,EAAU,MAAM,0BAA0B,CAAC;AACtE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAIpD,MAAM,WAAW,mBAAmB;IAClC,2FAA2F;IAC3F,UAAU,EAAE,eAAe,CAAC;IAC5B,kGAAkG;IAClG,CAAC,CAAC,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,WAAW,YAAa,SAAQ,aAAa;IACjD,2DAA2D;IAC3D,QAAQ,CAAC,MAAM,EAAE,KAAK,CAAC;IACvB,6BAA6B;IAC7B,OAAO,IAAI,IAAI,CAAC;CACjB;AAQD,wBAAgB,YAAY,CAAC,IAAI,EAAE,mBAAmB,GAAG,YAAY,CA2GpE"}
@@ -0,0 +1,120 @@
1
+ /**
2
+ * `threeBackend` — a `RenderBackend` (the engine's structural drawing seam, #373) that draws the
3
+ * overlay readings as Three.js geometry instead of a 2D canvas. Inject it via
4
+ * `createField({ overlayBackend })` (or `createThreeField`) and add `backend.object` to your scene.
5
+ *
6
+ * The engine describes WHAT to draw in CSS-pixel primitives; this backend owns HOW — mapping those
7
+ * pixels through the `FieldProjection` onto an overlay plane and batching them into a `LineSegments`
8
+ * (arrows, field-lines, grid, contours, traced paths) and a triangle `Mesh` (the data-chip plates).
9
+ * Per-stroke alpha is premultiplied into the vertex color and composited additively, so magnitude
10
+ * still reads. Geometry is rebuilt on each `clear()` (the engine clears once at the top of every
11
+ * overlay frame), so the scene always shows the latest readings.
12
+ *
13
+ * Scope: the line + rect primitives — i.e. every field-structure overlay — render fully. Text
14
+ * labels (the `data` reading's numeric chips) are not yet drawn; `measureText` is honored so the
15
+ * plates size correctly, and label sprites are a tracked follow-up. The line overlays need no text.
16
+ */
17
+ import { AdditiveBlending, BufferGeometry, Float32BufferAttribute, Group, LineBasicMaterial, LineSegments, Mesh, MeshBasicMaterial, Vector3, } from 'three';
18
+ const _v = new Vector3();
19
+ /** A lazily-created 2D context purely for text metrics — never rendered to. */
20
+ function metricsContext() {
21
+ if (typeof document === 'undefined')
22
+ return null;
23
+ return document.createElement('canvas').getContext('2d');
24
+ }
25
+ export function threeBackend(opts) {
26
+ const projection = opts.projection;
27
+ const z = opts.z ?? 0.01;
28
+ const linePos = [];
29
+ const lineCol = [];
30
+ const rectPos = [];
31
+ const rectCol = [];
32
+ const lineGeom = new BufferGeometry();
33
+ const lines = new LineSegments(lineGeom, new LineBasicMaterial({ vertexColors: true, transparent: true, depthWrite: false, blending: AdditiveBlending }));
34
+ lines.frustumCulled = false;
35
+ const rectGeom = new BufferGeometry();
36
+ const rects = new Mesh(rectGeom, new MeshBasicMaterial({ vertexColors: true, transparent: true, depthWrite: false, blending: AdditiveBlending }));
37
+ rects.frustumCulled = false;
38
+ const object = new Group();
39
+ object.add(lines, rects);
40
+ const metrics = metricsContext();
41
+ /** map a CSS-pixel point to the overlay plane and push xyz onto `arr` (overlay owns its own z). */
42
+ function pushPoint(arr, x, y) {
43
+ projection.toWorld(x, y, 0, 0, 0, _v); // z/heat/size irrelevant — the overlay draws at a fixed plane
44
+ arr.push(_v.x, _v.y, z);
45
+ }
46
+ /** premultiplied 0..1 rgb (additive compositing turns alpha into intensity). */
47
+ function pushColor(arr, r, g, b, a) {
48
+ arr.push((r / 255) * a, (g / 255) * a, (b / 255) * a);
49
+ }
50
+ function flush() {
51
+ lineGeom.setAttribute('position', new Float32BufferAttribute(Float32Array.from(linePos), 3));
52
+ lineGeom.setAttribute('color', new Float32BufferAttribute(Float32Array.from(lineCol), 3));
53
+ lineGeom.setDrawRange(0, linePos.length / 3);
54
+ rectGeom.setAttribute('position', new Float32BufferAttribute(Float32Array.from(rectPos), 3));
55
+ rectGeom.setAttribute('color', new Float32BufferAttribute(Float32Array.from(rectCol), 3));
56
+ rectGeom.setDrawRange(0, rectPos.length / 3);
57
+ }
58
+ return {
59
+ object,
60
+ size() {
61
+ // the projection owns the field→world transform; nothing to size on the GPU here.
62
+ },
63
+ clear() {
64
+ // finalize the frame just drawn, then reset the accumulators for the next one.
65
+ flush();
66
+ linePos.length = 0;
67
+ lineCol.length = 0;
68
+ rectPos.length = 0;
69
+ rectCol.length = 0;
70
+ },
71
+ segments(packed, stroke) {
72
+ for (let i = 0; i + 3 < packed.length; i += 4) {
73
+ pushPoint(linePos, packed[i], packed[i + 1]);
74
+ pushPoint(linePos, packed[i + 2], packed[i + 3]);
75
+ pushColor(lineCol, stroke.r, stroke.g, stroke.b, stroke.alpha);
76
+ pushColor(lineCol, stroke.r, stroke.g, stroke.b, stroke.alpha);
77
+ }
78
+ },
79
+ polyline(points, stroke) {
80
+ // expand the connected polyline into the disjoint-segment buffer (pairs of consecutive points)
81
+ for (let i = 0; i + 3 < points.length; i += 2) {
82
+ pushPoint(linePos, points[i], points[i + 1]);
83
+ pushPoint(linePos, points[i + 2], points[i + 3]);
84
+ pushColor(lineCol, stroke.r, stroke.g, stroke.b, stroke.alpha);
85
+ pushColor(lineCol, stroke.r, stroke.g, stroke.b, stroke.alpha);
86
+ }
87
+ },
88
+ rect(x, y, w, h, r, g, b, alpha) {
89
+ // two triangles (x,y)-(x+w,y+h); the data-chip plate
90
+ const x2 = x + w;
91
+ const y2 = y + h;
92
+ pushPoint(rectPos, x, y);
93
+ pushPoint(rectPos, x2, y);
94
+ pushPoint(rectPos, x2, y2);
95
+ pushPoint(rectPos, x, y);
96
+ pushPoint(rectPos, x2, y2);
97
+ pushPoint(rectPos, x, y2);
98
+ for (let k = 0; k < 6; k++)
99
+ pushColor(rectCol, r, g, b, alpha);
100
+ },
101
+ text() {
102
+ // label sprites are a tracked follow-up; the line overlays need no text, and the chip plate
103
+ // (rect) still renders. measureText is honored so plate sizing stays correct.
104
+ },
105
+ measureText(value) {
106
+ if (metrics) {
107
+ metrics.font = '10px ui-monospace, SFMono-Regular, Menlo, monospace';
108
+ return metrics.measureText(value).width;
109
+ }
110
+ return value.length * 6; // coarse fallback when no 2D context is available
111
+ },
112
+ dispose() {
113
+ lineGeom.dispose();
114
+ rectGeom.dispose();
115
+ lines.material.dispose();
116
+ rects.material.dispose();
117
+ },
118
+ };
119
+ }
120
+ //# sourceMappingURL=backend.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backend.js","sourceRoot":"","sources":["../src/backend.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Meshes as bodies — the 3D form of field-ui's core idea ("every element is a body"). A
3
+ * `THREE.Object3D` registers as a field body: it bends the field (bodies are force sources), the
4
+ * swarm responds to it, and the field's local density bends *it* back through feedback. And because
5
+ * a body **carries a data record**, a mesh can be a meaningful agent — a bloom carrying its genome,
6
+ * a hive accumulating honey — not just a force.
7
+ *
8
+ * It needs no core change: the engine builds a body from anything implementing the small element
9
+ * contract (`getAttribute` / `hasAttribute` / `dataset` / `getBoundingClientRect`), so each body is a
10
+ * lightweight non-DOM "virtual element" whose rect is the mesh's position projected onto the field
11
+ * plane, refreshed live as the mesh moves. Feedback is routed to the body via a custom sink, so
12
+ * `density` / `load` / `lit` land on the mesh (drive a uniform, accumulate a value) instead of CSS.
13
+ */
14
+ import type { Object3D } from 'three';
15
+ import type { FeedbackChannels, FeedbackSink } from '@fundamental-engine/core';
16
+ import type { FieldProjection } from './project.ts';
17
+ export interface FieldBodySpec {
18
+ /** force token(s) the body radiates — e.g. `'attract'`, `'gravity'`, `['charge', 'swirl']`. */
19
+ tokens: string | readonly string[];
20
+ /** force magnitude (`data-strength`). */
21
+ strength?: number;
22
+ /** influence radius in field pixels (`data-range`). */
23
+ range?: number;
24
+ /** swirl / charge sign (`data-spin`). */
25
+ spin?: number;
26
+ /** heading in radians for directional forces (`data-angle`). */
27
+ angle?: number;
28
+ /** the body's tint (`data-color`) — the `pigment` force dyes passing matter with it, so the
29
+ * swarm visibly carries this body's color to wherever it drifts next (conserved color transport). */
30
+ color?: string;
31
+ /** opt into density/load feedback back onto the mesh (default `true`). */
32
+ feedback?: boolean;
33
+ /** the body's box half-size in field pixels — its footprint, not its range (default 20). */
34
+ sizePx?: number;
35
+ /** the carried record: a genome, an inventory, anything. Opaque to the engine; yours to read. */
36
+ data?: unknown;
37
+ /** raw `data-*` passthrough (without the `data-` prefix) for tokens this spec doesn't model. */
38
+ attrs?: Record<string, string>;
39
+ /** called each frame the body has feedback, with its live channels — drive a uniform from here. */
40
+ onFeedback?: (channels: FeedbackChannels, body: FieldBody) => void;
41
+ }
42
+ export interface FieldBody {
43
+ /** the registered scene object. */
44
+ readonly object: Object3D;
45
+ /** the carried record — read it when a pollinator reaches this bloom, mutate it freely. */
46
+ data: unknown;
47
+ /** the body's most recent feedback (density/load/lit/…); `{}` until the field writes. */
48
+ readonly channels: Readonly<FeedbackChannels>;
49
+ /** unregister this body from the field. */
50
+ remove(): void;
51
+ }
52
+ /**
53
+ * Manages the mesh-bodies for one field. Provides the `root` the host scans and the `sink` that
54
+ * routes feedback to meshes; a `FieldLayer` wires both. Add via `layer.addBody(...)`.
55
+ */
56
+ export declare class FieldBodyRegistry {
57
+ private bodies;
58
+ private projection;
59
+ private onChange;
60
+ constructor(projection: FieldProjection);
61
+ /** project a scene object's world position onto the field plane (field pixels). */
62
+ projectToField(object: Object3D): {
63
+ x: number;
64
+ y: number;
65
+ };
66
+ /** internal — the FieldLayer re-points this if the projection changes. */
67
+ setProjection(projection: FieldProjection): void;
68
+ /** internal — the FieldLayer wires this to `field.scan()` so adds/removes re-register. */
69
+ setOnChange(fn: () => void): void;
70
+ /** the scan root the host hands the engine: `[data-body]` queries return the live mesh-bodies. */
71
+ get root(): ParentNode;
72
+ /** the feedback sink the field writes through — lands density/load/lit on the body's mesh. */
73
+ get sink(): FeedbackSink;
74
+ add(object: Object3D, spec: FieldBodySpec): FieldBody;
75
+ remove(body: FieldBody): void;
76
+ /** every registered body (read-only). */
77
+ all(): readonly FieldBody[];
78
+ }
79
+ //# sourceMappingURL=bodies.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bodies.d.ts","sourceRoot":"","sources":["../src/bodies.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AACtC,OAAO,KAAK,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAC/E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAIpD,MAAM,WAAW,aAAa;IAC5B,+FAA+F;IAC/F,MAAM,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAAC;IACnC,yCAAyC;IACzC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,uDAAuD;IACvD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,yCAAyC;IACzC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,gEAAgE;IAChE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;0GACsG;IACtG,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,4FAA4F;IAC5F,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iGAAiG;IACjG,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,gGAAgG;IAChG,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,mGAAmG;IACnG,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,gBAAgB,EAAE,IAAI,EAAE,SAAS,KAAK,IAAI,CAAC;CACpE;AAED,MAAM,WAAW,SAAS;IACxB,mCAAmC;IACnC,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC;IAC1B,2FAA2F;IAC3F,IAAI,EAAE,OAAO,CAAC;IACd,yFAAyF;IACzF,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC,gBAAgB,CAAC,CAAC;IAC9C,2CAA2C;IAC3C,MAAM,IAAI,IAAI,CAAC;CAChB;AAsFD;;;GAGG;AACH,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,MAAM,CAAkB;IAChC,OAAO,CAAC,UAAU,CAAkB;IACpC,OAAO,CAAC,QAAQ,CAAwB;gBAE5B,UAAU,EAAE,eAAe;IAIvC,mFAAmF;IACnF,cAAc,CAAC,MAAM,EAAE,QAAQ,GAAG;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE;IAK1D,0EAA0E;IAC1E,aAAa,CAAC,UAAU,EAAE,eAAe,GAAG,IAAI;IAIhD,0FAA0F;IAC1F,WAAW,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,IAAI;IAIjC,kGAAkG;IAClG,IAAI,IAAI,IAAI,UAAU,CAMrB;IAED,8FAA8F;IAC9F,IAAI,IAAI,IAAI,YAAY,CAIvB;IAED,GAAG,CAAC,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,aAAa,GAAG,SAAS;IAOrD,MAAM,CAAC,IAAI,EAAE,SAAS,GAAG,IAAI;IAQ7B,yCAAyC;IACzC,GAAG,IAAI,SAAS,SAAS,EAAE;CAG5B"}