@fundamental-engine/core 0.7.0 → 0.9.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/agents/event-agent.d.ts +4 -2
- package/dist/agents/event-agent.d.ts.map +1 -1
- package/dist/agents/event-agent.js +15 -6
- package/dist/agents/event-agent.js.map +1 -1
- package/dist/config/themes.d.ts +22 -0
- package/dist/config/themes.d.ts.map +1 -0
- package/dist/config/themes.js +9 -0
- package/dist/config/themes.js.map +1 -0
- package/dist/contracts/guards.d.ts +14 -0
- package/dist/contracts/guards.d.ts.map +1 -1
- package/dist/contracts/guards.js +21 -0
- package/dist/contracts/guards.js.map +1 -1
- package/dist/core/agents.d.ts +1 -1
- package/dist/core/agents.d.ts.map +1 -1
- package/dist/core/agents.js +5 -2
- package/dist/core/agents.js.map +1 -1
- package/dist/core/currents.d.ts +12 -0
- package/dist/core/currents.d.ts.map +1 -1
- package/dist/core/currents.js +23 -0
- package/dist/core/currents.js.map +1 -1
- package/dist/core/events.d.ts +37 -0
- package/dist/core/events.d.ts.map +1 -1
- package/dist/core/events.js +52 -0
- package/dist/core/events.js.map +1 -1
- package/dist/core/field.d.ts.map +1 -1
- package/dist/core/field.js +789 -133
- package/dist/core/field.js.map +1 -1
- package/dist/core/frame-harness.d.ts +109 -0
- package/dist/core/frame-harness.d.ts.map +1 -0
- package/dist/core/frame-harness.js +300 -0
- package/dist/core/frame-harness.js.map +1 -0
- package/dist/core/host-headless.d.ts +35 -0
- package/dist/core/host-headless.d.ts.map +1 -0
- package/dist/core/host-headless.js +47 -0
- package/dist/core/host-headless.js.map +1 -0
- package/dist/core/host.d.ts +7 -0
- package/dist/core/host.d.ts.map +1 -1
- package/dist/core/integrator.d.ts +6 -0
- package/dist/core/integrator.d.ts.map +1 -1
- package/dist/core/integrator.js +62 -12
- package/dist/core/integrator.js.map +1 -1
- package/dist/core/math.d.ts +4 -3
- package/dist/core/math.d.ts.map +1 -1
- package/dist/core/math.js +10 -9
- package/dist/core/math.js.map +1 -1
- package/dist/core/scanner.d.ts +1 -1
- package/dist/core/scanner.d.ts.map +1 -1
- package/dist/core/scanner.js +8 -4
- package/dist/core/scanner.js.map +1 -1
- package/dist/core/streamlines.d.ts.map +1 -1
- package/dist/core/streamlines.js +18 -2
- package/dist/core/streamlines.js.map +1 -1
- package/dist/core/types.d.ts +133 -12
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +10 -1
- package/dist/core/types.js.map +1 -1
- package/dist/forces/index.d.ts.map +1 -1
- package/dist/forces/index.js +3 -1
- package/dist/forces/index.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/record/index.d.ts +13 -0
- package/dist/record/index.d.ts.map +1 -0
- package/dist/record/index.js +13 -0
- package/dist/record/index.js.map +1 -0
- package/dist/record/record.d.ts +87 -0
- package/dist/record/record.d.ts.map +1 -0
- package/dist/record/record.js +172 -0
- package/dist/record/record.js.map +1 -0
- package/dist/record/rng.d.ts +15 -0
- package/dist/record/rng.d.ts.map +1 -0
- package/dist/record/rng.js +25 -0
- package/dist/record/rng.js.map +1 -0
- package/dist/version.d.ts +8 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +8 -0
- package/dist/version.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"frame-harness.d.ts","sourceRoot":"","sources":["../../src/core/frame-harness.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* frameHarness — a DOM-free, frame-driving test harness for the element-consumer paths in
|
|
3
|
+
* `field.ts` (§22.3 element capture / relocate / emit, §22.5 capture/release events).
|
|
4
|
+
*
|
|
5
|
+
* `headlessHost` exists for the *signals-first* consumer (an agent reading the field via `addBody`
|
|
6
|
+
* + `onFeedback`): its scan root is empty by design, so the `[data-move]`/`[data-dock]`/`[data-warp]`/
|
|
7
|
+
* `[data-emit]` element machinery in the frame loop is never exercised. PR #260 wired all of that —
|
|
8
|
+
* movers drifting, a `[data-dock]` element collapsing into a sink, warp teleport, element emit, and
|
|
9
|
+
* the `field:captured` / `field:released` / `field:relocated` dispatch — and it has stayed untested
|
|
10
|
+
* because the only `createField` stub (`packages/vanilla/src/field.test.ts`) hands back a `raf` that
|
|
11
|
+
* never fires and a `querySelectorAll` that returns `[]`, so the loop never runs.
|
|
12
|
+
*
|
|
13
|
+
* This harness closes that gap WITHOUT a DOM or a test framework (the repo forbids jsdom). It is a
|
|
14
|
+
* hand-rolled element graph — each {@link HarnessElement} answers exactly the surface the engine
|
|
15
|
+
* reads (`getBoundingClientRect`, `dataset`, `getAttribute`/`hasAttribute`/`setAttribute`/
|
|
16
|
+
* `removeAttribute`, `style`, `dispatchEvent`, `isConnected`, `cloneNode`/`appendChild`/`remove`) —
|
|
17
|
+
* plus a {@link FrameHarnessHost} whose `root.querySelectorAll` returns the live element list and
|
|
18
|
+
* whose loop is **manual**: the engine re-schedules a frame each `raf`, and {@link FrameHarness.step}
|
|
19
|
+
* fires them on a deterministic `dt`. Pair it with a seeded `rng` for a fully reproducible run.
|
|
20
|
+
*
|
|
21
|
+
* ```ts
|
|
22
|
+
* const h = frameHarness({ width: 800, height: 600 });
|
|
23
|
+
* const sink = h.add({ attrs: { 'data-body': 'sink', 'data-absorb': '900', 'data-max': '1' },
|
|
24
|
+
* rect: { left: 400, top: 300, width: 40, height: 40 } });
|
|
25
|
+
* const dock = h.add({ attrs: { 'data-move': '', 'data-dock': '' },
|
|
26
|
+
* rect: { left: 410, top: 310, width: 20, height: 20 } });
|
|
27
|
+
* const field = createField(h.canvas, { host: h.host, render: 'none', waves: false, rng: seededRng() });
|
|
28
|
+
* field.scan();
|
|
29
|
+
* h.step(); // drive one frame — the dock element captures
|
|
30
|
+
* dock.events.includes('field:captured'); // true
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
import { createField } from "./field.js";
|
|
34
|
+
const ZERO_RECT = { left: 0, top: 0, width: 0, height: 0 };
|
|
35
|
+
/**
|
|
36
|
+
* One element in the harness scan root — a hand-rolled stand-in answering the exact surface the
|
|
37
|
+
* engine reads off a DOM element. The fields the frame loop mutates (transforms via `style`, a11y
|
|
38
|
+
* via `aria-hidden`/`inert`, the `data-fx-cap` edge flag) are observable so tests can assert them,
|
|
39
|
+
* and every `field:*` / `forces:*` event the engine dispatches is recorded in {@link events}.
|
|
40
|
+
*/
|
|
41
|
+
export class HarnessElement {
|
|
42
|
+
/** mutable layout box; the engine reads it through `getBoundingClientRect`. Move it to relocate. */
|
|
43
|
+
rect;
|
|
44
|
+
tagName;
|
|
45
|
+
id;
|
|
46
|
+
className = '';
|
|
47
|
+
isConnected = true;
|
|
48
|
+
/** every `dispatchEvent`'d event type, in order (e.g. `'field:captured'`, `'forces:captured'`). */
|
|
49
|
+
events = [];
|
|
50
|
+
/** every dispatched event with its detail, in order — for asserting the payload. */
|
|
51
|
+
dispatched = [];
|
|
52
|
+
dataset = {};
|
|
53
|
+
/** the writable style surface the engine writes transforms / opacity through. Index it by name
|
|
54
|
+
* (e.g. `el.style.transform`, `el.style.opacity`) or via setProperty/getPropertyValue. */
|
|
55
|
+
style;
|
|
56
|
+
/** clones appended by element-emit (§22.3) live here (children), so a test can count them. */
|
|
57
|
+
children = [];
|
|
58
|
+
attrs;
|
|
59
|
+
props = {};
|
|
60
|
+
constructor(spec = {}) {
|
|
61
|
+
this.attrs = { ...(spec.attrs ?? {}) };
|
|
62
|
+
this.rect = spec.rect ? { ...spec.rect } : { ...ZERO_RECT };
|
|
63
|
+
this.tagName = (spec.tag ?? 'DIV').toUpperCase();
|
|
64
|
+
// `id` may be supplied directly OR as an `id` attribute (so emit templates `{ attrs: { id } }`
|
|
65
|
+
// resolve through the same `#id` selector path the engine uses); the field is the source of truth.
|
|
66
|
+
this.id = spec.id ?? this.attrs.id ?? '';
|
|
67
|
+
delete this.attrs.id;
|
|
68
|
+
// mirror `data-*` attributes into dataset (camelCased), the way the DOM does — the engine reads
|
|
69
|
+
// dockable/warpable through hasAttribute but layout/emit/max/active through `dataset`.
|
|
70
|
+
for (const [k, v] of Object.entries(this.attrs)) {
|
|
71
|
+
if (k.startsWith('data-'))
|
|
72
|
+
this.dataset[dashToCamel(k.slice(5))] = v;
|
|
73
|
+
}
|
|
74
|
+
const props = this.props;
|
|
75
|
+
const styleApi = {
|
|
76
|
+
setProperty(k, v) {
|
|
77
|
+
props[k] = v;
|
|
78
|
+
},
|
|
79
|
+
removeProperty(k) {
|
|
80
|
+
delete props[k];
|
|
81
|
+
},
|
|
82
|
+
getPropertyValue(k) {
|
|
83
|
+
return props[k] ?? '';
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
this.style = new Proxy(styleApi, {
|
|
87
|
+
get(target, key) {
|
|
88
|
+
if (key in target)
|
|
89
|
+
return target[key];
|
|
90
|
+
return props[key] ?? '';
|
|
91
|
+
},
|
|
92
|
+
set(_t, key, value) {
|
|
93
|
+
props[key] = value;
|
|
94
|
+
return true;
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
getBoundingClientRect() {
|
|
99
|
+
const r = this.rect;
|
|
100
|
+
return {
|
|
101
|
+
left: r.left,
|
|
102
|
+
top: r.top,
|
|
103
|
+
right: r.left + r.width,
|
|
104
|
+
bottom: r.top + r.height,
|
|
105
|
+
width: r.width,
|
|
106
|
+
height: r.height,
|
|
107
|
+
x: r.left,
|
|
108
|
+
y: r.top,
|
|
109
|
+
toJSON: () => ({}),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
getAttribute(name) {
|
|
113
|
+
if (name === 'aria-hidden' || name === 'inert')
|
|
114
|
+
return this.props[name] ?? null;
|
|
115
|
+
if (name === 'id')
|
|
116
|
+
return this.id || null;
|
|
117
|
+
return this.attrs[name] ?? null;
|
|
118
|
+
}
|
|
119
|
+
hasAttribute(name) {
|
|
120
|
+
if (name === 'aria-hidden' || name === 'inert')
|
|
121
|
+
return name in this.props;
|
|
122
|
+
if (name === 'id')
|
|
123
|
+
return this.id !== '';
|
|
124
|
+
return name in this.attrs;
|
|
125
|
+
}
|
|
126
|
+
setAttribute(name, value) {
|
|
127
|
+
if (name === 'aria-hidden' || name === 'inert') {
|
|
128
|
+
this.props[name] = value;
|
|
129
|
+
}
|
|
130
|
+
else if (name === 'id') {
|
|
131
|
+
this.id = value;
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
this.attrs[name] = value;
|
|
135
|
+
if (name.startsWith('data-'))
|
|
136
|
+
this.dataset[dashToCamel(name.slice(5))] = value;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
removeAttribute(name) {
|
|
140
|
+
if (name === 'aria-hidden' || name === 'inert') {
|
|
141
|
+
delete this.props[name];
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (name === 'id') {
|
|
145
|
+
this.id = ''; // element-emit strips the clone's id (no duplicate ids, §22.3)
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
delete this.attrs[name];
|
|
149
|
+
if (name.startsWith('data-'))
|
|
150
|
+
delete this.dataset[dashToCamel(name.slice(5))];
|
|
151
|
+
}
|
|
152
|
+
dispatchEvent(e) {
|
|
153
|
+
this.events.push(e.type);
|
|
154
|
+
this.dispatched.push({ type: e.type, detail: e.detail });
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
appendChild(child) {
|
|
158
|
+
this.children.push(child);
|
|
159
|
+
child.isConnected = true;
|
|
160
|
+
return child;
|
|
161
|
+
}
|
|
162
|
+
remove() {
|
|
163
|
+
this.isConnected = false;
|
|
164
|
+
}
|
|
165
|
+
cloneNode(_deep) {
|
|
166
|
+
// a deep clone carries the id too (the engine then strips it via removeAttribute('id')).
|
|
167
|
+
return new HarnessElement({ attrs: { ...this.attrs }, rect: { ...this.rect }, tag: this.tagName, id: this.id });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function dashToCamel(s) {
|
|
171
|
+
return s.replace(/-([a-z])/g, (_m, c) => c.toUpperCase());
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Build a frame-driving harness. The returned {@link FrameHarness.host} drives the REAL `field.ts`
|
|
175
|
+
* frame loop manually — `createField` schedules a frame through `raf`, and {@link FrameHarness.step}
|
|
176
|
+
* fires it on a deterministic `dt`. Register elements with {@link FrameHarness.add}, call the field's
|
|
177
|
+
* `scan()` to pick them up, then `step()` to advance frames and assert on `element.events`,
|
|
178
|
+
* `element.style`, and `element.dataset`.
|
|
179
|
+
*/
|
|
180
|
+
export function frameHarness(opts = {}) {
|
|
181
|
+
const W = opts.width ?? 800;
|
|
182
|
+
const H = opts.height ?? 600;
|
|
183
|
+
const elements = [];
|
|
184
|
+
let frame = null;
|
|
185
|
+
let t = 0;
|
|
186
|
+
const noop = () => { };
|
|
187
|
+
const off = () => noop;
|
|
188
|
+
const matchesClause = (el, clause) => {
|
|
189
|
+
// the four selector shapes the engine actually issues: a bare attribute (`[data-move]`), an
|
|
190
|
+
// attribute-value (the warp `data-pair` selector, e.g. `[data-body="anchor"]`), an id (an emit
|
|
191
|
+
// template, `#spark`), and a class. Anything fancier is out of scope (the engine never uses it).
|
|
192
|
+
const attrVal = /^\[([a-z-]+)=["']?([^"'\]]*)["']?\]$/.exec(clause);
|
|
193
|
+
if (attrVal)
|
|
194
|
+
return el.getAttribute(attrVal[1]) === attrVal[2];
|
|
195
|
+
const attr = /^\[([a-z-]+)\]$/.exec(clause);
|
|
196
|
+
if (attr)
|
|
197
|
+
return el.hasAttribute(attr[1]);
|
|
198
|
+
if (clause.startsWith('#'))
|
|
199
|
+
return el.id === clause.slice(1);
|
|
200
|
+
if (clause.startsWith('.'))
|
|
201
|
+
return el.className.split(/\s+/).includes(clause.slice(1));
|
|
202
|
+
return false;
|
|
203
|
+
};
|
|
204
|
+
const matches = (el, selector) => selector
|
|
205
|
+
.split(',')
|
|
206
|
+
.map((s) => s.trim())
|
|
207
|
+
.some((clause) => matchesClause(el, clause));
|
|
208
|
+
const root = {
|
|
209
|
+
querySelectorAll: (selector) => elements.filter((el) => el.isConnected && matches(el, selector)),
|
|
210
|
+
querySelector: (selector) => elements.find((el) => el.isConnected && matches(el, selector)) ?? null,
|
|
211
|
+
contains: (node) => elements.includes(node),
|
|
212
|
+
};
|
|
213
|
+
const host = {
|
|
214
|
+
root,
|
|
215
|
+
viewport: () => ({ width: W, height: H, dpr: 1 }),
|
|
216
|
+
scrollY: () => 0,
|
|
217
|
+
scrollHeight: () => H,
|
|
218
|
+
reducedMotion: () => false,
|
|
219
|
+
hidden: () => false,
|
|
220
|
+
raf: (cb) => {
|
|
221
|
+
frame = cb;
|
|
222
|
+
return 1;
|
|
223
|
+
},
|
|
224
|
+
cancelRaf: () => {
|
|
225
|
+
frame = null;
|
|
226
|
+
},
|
|
227
|
+
// the harness runs with render:'none', so the heatmap-buffer canvas is never requested; throw a
|
|
228
|
+
// clear message if a test asks for a drawing mode (it'd need a real canvas anyway).
|
|
229
|
+
createCanvas: () => {
|
|
230
|
+
throw new Error("frameHarness does not render — create the field with render:'none'.");
|
|
231
|
+
},
|
|
232
|
+
onResize: off,
|
|
233
|
+
onScroll: off,
|
|
234
|
+
onVisibility: off,
|
|
235
|
+
onInput: off,
|
|
236
|
+
onBodyEvent: off,
|
|
237
|
+
tick(dt) {
|
|
238
|
+
t += dt;
|
|
239
|
+
const cb = frame;
|
|
240
|
+
frame = null;
|
|
241
|
+
cb?.(t);
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
// a throwaway canvas — never drawn to under render:'none', but createField's signature wants one.
|
|
245
|
+
const canvas = {
|
|
246
|
+
width: 0,
|
|
247
|
+
height: 0,
|
|
248
|
+
style: {},
|
|
249
|
+
setAttribute: noop,
|
|
250
|
+
getContext: () => null,
|
|
251
|
+
};
|
|
252
|
+
return {
|
|
253
|
+
host,
|
|
254
|
+
canvas,
|
|
255
|
+
elements,
|
|
256
|
+
add(spec) {
|
|
257
|
+
const el = new HarnessElement(spec);
|
|
258
|
+
elements.push(el);
|
|
259
|
+
return el;
|
|
260
|
+
},
|
|
261
|
+
remove(el) {
|
|
262
|
+
el.isConnected = false;
|
|
263
|
+
const i = elements.indexOf(el);
|
|
264
|
+
if (i >= 0)
|
|
265
|
+
elements.splice(i, 1);
|
|
266
|
+
},
|
|
267
|
+
step(n = 1) {
|
|
268
|
+
for (let i = 0; i < n; i++)
|
|
269
|
+
host.tick(16);
|
|
270
|
+
},
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Convenience: build a harness AND a field on it in one call, with the signals-first defaults the
|
|
275
|
+
* harness needs (`render: 'none'`, `waves: false`). Returns both so a test drives `field.scan()` /
|
|
276
|
+
* `harness.step()` and asserts on `harness.elements`.
|
|
277
|
+
*/
|
|
278
|
+
export function frameHarnessField(opts = {}) {
|
|
279
|
+
const { width, height, ...fieldOpts } = opts;
|
|
280
|
+
const harness = frameHarness({ width, height });
|
|
281
|
+
const field = createField(harness.canvas, {
|
|
282
|
+
host: harness.host,
|
|
283
|
+
render: 'none',
|
|
284
|
+
waves: false,
|
|
285
|
+
...fieldOpts,
|
|
286
|
+
});
|
|
287
|
+
return { harness, field };
|
|
288
|
+
}
|
|
289
|
+
/** A tiny deterministic PRNG (mulberry32) for reproducible harness runs — pass as `rng`. */
|
|
290
|
+
export function seededRng(seed = 1) {
|
|
291
|
+
let a = seed >>> 0;
|
|
292
|
+
return () => {
|
|
293
|
+
a |= 0;
|
|
294
|
+
a = (a + 0x6d2b79f5) | 0;
|
|
295
|
+
let t = Math.imul(a ^ (a >>> 15), 1 | a);
|
|
296
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
297
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
//# sourceMappingURL=frame-harness.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"frame-harness.js","sourceRoot":"","sources":["../../src/core/frame-harness.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* headlessHost — the reference {@link FieldHost} for non-DOM, non-visual consumers: an agent reading
|
|
3
|
+
* the field as a salience substrate, a native sidecar, a Node service, or a deterministic test. Where
|
|
4
|
+
* `browserHost()` binds the engine to `window`/`document`/rAF, `headlessHost()` binds it to nothing —
|
|
5
|
+
* an abstract volume the caller sets, a no-op scan root (bodies come via `addBody`, not `[data-body]`),
|
|
6
|
+
* and a **manual** loop the caller drives with `tick()` instead of requestAnimationFrame.
|
|
7
|
+
*
|
|
8
|
+
* Pair it with `render: 'none'` (signals-first): the field runs the full simulation + writes its
|
|
9
|
+
* signals and draws nothing. Read them back per-body through `addBody`'s `onFeedback`, or globally via
|
|
10
|
+
* `sampleScalar` / `readParticles`. No DOM is touched (`host-headless.ts` references only the DOM
|
|
11
|
+
* *types* already in `host.ts`, no globals — `dom-boundary.test.ts` stays green).
|
|
12
|
+
*
|
|
13
|
+
* ```ts
|
|
14
|
+
* const host = headlessHost({ width: 1920, height: 1080 });
|
|
15
|
+
* const field = createField(undefined, { host, render: 'none' });
|
|
16
|
+
* field.addBody({ tokens: ['attract'], rect: () => box, onFeedback: (ch) => read(ch.density) });
|
|
17
|
+
* host.tick(); // advance one frame — call per agent turn or on a schedule
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
import type { FieldHost } from './host.ts';
|
|
21
|
+
export interface HeadlessHostOptions {
|
|
22
|
+
/** the field's coordinate-space width (the abstract "volume"). */
|
|
23
|
+
width: number;
|
|
24
|
+
/** the field's coordinate-space height. */
|
|
25
|
+
height: number;
|
|
26
|
+
}
|
|
27
|
+
/** A {@link FieldHost} the caller drives manually — see {@link headlessHost}. */
|
|
28
|
+
export interface HeadlessHost extends FieldHost {
|
|
29
|
+
/** Advance the field one frame. Pass an explicit timestamp (ms), or omit to auto-step ~1/60 s. */
|
|
30
|
+
tick(t?: number): void;
|
|
31
|
+
/** Resize the abstract volume (the field's coordinate space). */
|
|
32
|
+
resize(width: number, height: number): void;
|
|
33
|
+
}
|
|
34
|
+
export declare function headlessHost(opts: HeadlessHostOptions): HeadlessHost;
|
|
35
|
+
//# sourceMappingURL=host-headless.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"host-headless.d.ts","sourceRoot":"","sources":["../../src/core/host-headless.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,KAAK,EAAE,SAAS,EAAgB,MAAM,WAAW,CAAC;AAEzD,MAAM,WAAW,mBAAmB;IAClC,kEAAkE;IAClE,KAAK,EAAE,MAAM,CAAC;IACd,2CAA2C;IAC3C,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,iFAAiF;AACjF,MAAM,WAAW,YAAa,SAAQ,SAAS;IAC7C,kGAAkG;IAClG,IAAI,CAAC,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,iEAAiE;IACjE,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7C;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,mBAAmB,GAAG,YAAY,CAiDpE"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export function headlessHost(opts) {
|
|
2
|
+
let w = opts.width;
|
|
3
|
+
let h = opts.height;
|
|
4
|
+
let frame = null;
|
|
5
|
+
let t = 0;
|
|
6
|
+
// a no-op scan root — a headless field has no `[data-body]` DOM; bodies are registered via addBody.
|
|
7
|
+
const root = {
|
|
8
|
+
querySelectorAll: () => [],
|
|
9
|
+
querySelector: () => null,
|
|
10
|
+
contains: () => false,
|
|
11
|
+
};
|
|
12
|
+
return {
|
|
13
|
+
root,
|
|
14
|
+
viewport: () => ({ width: w, height: h, dpr: 1 }),
|
|
15
|
+
scrollY: () => 0,
|
|
16
|
+
scrollHeight: () => h,
|
|
17
|
+
reducedMotion: () => false,
|
|
18
|
+
hidden: () => false,
|
|
19
|
+
// the loop is manual: raf stashes the next frame, tick() fires it. The consumer owns the cadence.
|
|
20
|
+
raf: (cb) => {
|
|
21
|
+
frame = cb;
|
|
22
|
+
return 1;
|
|
23
|
+
},
|
|
24
|
+
cancelRaf: () => {
|
|
25
|
+
frame = null;
|
|
26
|
+
},
|
|
27
|
+
createCanvas: () => {
|
|
28
|
+
throw new Error("headlessHost does not render — use render:'none' and the signal read-outs (onFeedback / sampleScalar / readParticles).");
|
|
29
|
+
},
|
|
30
|
+
onResize: () => () => { },
|
|
31
|
+
onScroll: () => () => { },
|
|
32
|
+
onVisibility: () => () => { },
|
|
33
|
+
onInput: () => () => { },
|
|
34
|
+
onBodyEvent: () => () => { },
|
|
35
|
+
tick(at) {
|
|
36
|
+
t = at ?? t + 1000 / 60;
|
|
37
|
+
const cb = frame;
|
|
38
|
+
frame = null;
|
|
39
|
+
cb?.(t);
|
|
40
|
+
},
|
|
41
|
+
resize(width, height) {
|
|
42
|
+
w = width;
|
|
43
|
+
h = height;
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=host-headless.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"host-headless.js","sourceRoot":"","sources":["../../src/core/host-headless.ts"],"names":[],"mappings":"AAoCA,MAAM,UAAU,YAAY,CAAC,IAAyB;IACpD,IAAI,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC;IACnB,IAAI,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC;IACpB,IAAI,KAAK,GAAiC,IAAI,CAAC;IAC/C,IAAI,CAAC,GAAG,CAAC,CAAC;IACV,oGAAoG;IACpG,MAAM,IAAI,GAAG;QACX,gBAAgB,EAAE,GAAG,EAAE,CAAC,EAAoC;QAC5D,aAAa,EAAE,GAAG,EAAE,CAAC,IAAI;QACzB,QAAQ,EAAE,GAAG,EAAE,CAAC,KAAK;KACG,CAAC;IAE3B,OAAO;QACL,IAAI;QACJ,QAAQ,EAAE,GAAiB,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC;QAC/D,OAAO,EAAE,GAAG,EAAE,CAAC,CAAC;QAChB,YAAY,EAAE,GAAG,EAAE,CAAC,CAAC;QACrB,aAAa,EAAE,GAAG,EAAE,CAAC,KAAK;QAC1B,MAAM,EAAE,GAAG,EAAE,CAAC,KAAK;QACnB,kGAAkG;QAClG,GAAG,EAAE,CAAC,EAAE,EAAE,EAAE;YACV,KAAK,GAAG,EAAE,CAAC;YACX,OAAO,CAAC,CAAC;QACX,CAAC;QACD,SAAS,EAAE,GAAG,EAAE;YACd,KAAK,GAAG,IAAI,CAAC;QACf,CAAC;QACD,YAAY,EAAE,GAAG,EAAE;YACjB,MAAM,IAAI,KAAK,CACb,wHAAwH,CACzH,CAAC;QACJ,CAAC;QACD,QAAQ,EAAE,GAAG,EAAE,CAAC,GAAG,EAAE,GAAE,CAAC;QACxB,QAAQ,EAAE,GAAG,EAAE,CAAC,GAAG,EAAE,GAAE,CAAC;QACxB,YAAY,EAAE,GAAG,EAAE,CAAC,GAAG,EAAE,GAAE,CAAC;QAC5B,OAAO,EAAE,GAAG,EAAE,CAAC,GAAG,EAAE,GAAE,CAAC;QACvB,WAAW,EAAE,GAAG,EAAE,CAAC,GAAG,EAAE,GAAE,CAAC;QAE3B,IAAI,CAAC,EAAE;YACL,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,IAAI,GAAG,EAAE,CAAC;YACxB,MAAM,EAAE,GAAG,KAAK,CAAC;YACjB,KAAK,GAAG,IAAI,CAAC;YACb,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC;QACV,CAAC;QACD,MAAM,CAAC,KAAK,EAAE,MAAM;YAClB,CAAC,GAAG,KAAK,CAAC;YACV,CAAC,GAAG,MAAM,CAAC;QACb,CAAC;KACF,CAAC;AACJ,CAAC"}
|
package/dist/core/host.d.ts
CHANGED
|
@@ -12,6 +12,13 @@ export interface HostViewport {
|
|
|
12
12
|
width: number;
|
|
13
13
|
height: number;
|
|
14
14
|
dpr: number;
|
|
15
|
+
/** field-space origin in the host's measurement coords — the top-left the field is drawn from.
|
|
16
|
+
* `0,0` for a window-scoped host (the default); a CONTAINED host (`containerHost`) returns its
|
|
17
|
+
* element's `left,top` so the field, its bodies, and its canvas all live in container-local space.
|
|
18
|
+
* `measureBodies` and the thread/move readouts subtract it. Optional → 0 keeps every window host
|
|
19
|
+
* and test byte-identical. */
|
|
20
|
+
originX?: number;
|
|
21
|
+
originY?: number;
|
|
15
22
|
}
|
|
16
23
|
export interface FieldHost {
|
|
17
24
|
/** the subtree scanned for `[data-body]` / `[data-move]` / `[data-on]` / `[data-hot]` / `[data-formation]`. */
|
package/dist/core/host.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"host.d.ts","sourceRoot":"","sources":["../../src/core/host.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,sEAAsE;AACtE,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"host.d.ts","sourceRoot":"","sources":["../../src/core/host.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,sEAAsE;AACtE,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ;;;;mCAI+B;IAC/B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,SAAS;IACxB,+GAA+G;IAC/G,IAAI,EAAE,UAAU,CAAC;IACjB,0DAA0D;IAC1D,QAAQ,IAAI,YAAY,CAAC;IACzB,4CAA4C;IAC5C,OAAO,IAAI,MAAM,CAAC;IAClB,2FAA2F;IAC3F,YAAY,IAAI,MAAM,CAAC;IACvB,iEAAiE;IACjE,aAAa,IAAI,OAAO,CAAC;IACzB,0EAA0E;IAC1E,MAAM,IAAI,OAAO,CAAC;IAClB,iDAAiD;IACjD,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,GAAG,MAAM,CAAC;IACrC,gCAAgC;IAChC,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,kDAAkD;IAClD,YAAY,IAAI,iBAAiB,CAAC;IAClC,4DAA4D;IAC5D,QAAQ,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC;IACrC,mDAAmD;IACnD,QAAQ,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC;IACrC,uEAAuE;IACvE,YAAY,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC;IACzC,0FAA0F;IAC1F,OAAO,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC;IACpC,qFAAqF;IACrF,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;CAC/D"}
|
|
@@ -19,6 +19,12 @@ export interface StepInput {
|
|
|
19
19
|
conditions: ConditionRegistry;
|
|
20
20
|
/** the carrier waves — free particles drift along their slope (§2.3). */
|
|
21
21
|
waves?: readonly Wave[];
|
|
22
|
+
waveStyle?: 'linear' | 'circular';
|
|
23
|
+
waveCenter?: {
|
|
24
|
+
x: number;
|
|
25
|
+
y: number;
|
|
26
|
+
} | null;
|
|
27
|
+
separation?: number;
|
|
22
28
|
}
|
|
23
29
|
export declare function step(input: StepInput): void;
|
|
24
30
|
//# sourceMappingURL=integrator.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"integrator.d.ts","sourceRoot":"","sources":["../../src/core/integrator.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,iBAAiB,EAAE,GAAG,EAAS,aAAa,EAAY,MAAM,YAAY,CAAC;AAC/F,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,OAAO,
|
|
1
|
+
{"version":3,"file":"integrator.d.ts","sourceRoot":"","sources":["../../src/core/integrator.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,iBAAiB,EAAE,GAAG,EAAS,aAAa,EAAY,MAAM,YAAY,CAAC;AAC/F,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,OAAO,EAAoC,KAAK,IAAI,EAAE,MAAM,eAAe,CAAC;AAK5E,eAAO,MAAM,QAAQ,OAAO,CAAC;AAC7B,eAAO,MAAM,UAAU,QAAQ,CAAC;AAGhC,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,UAAU,CAAC;IAClB,MAAM,EAAE,SAAS,IAAI,EAAE,CAAC;IACxB,GAAG,EAAE,GAAG,CAAC;IACT,MAAM,EAAE,aAAa,CAAC;IACtB,UAAU,EAAE,iBAAiB,CAAC;IAC9B,yEAAyE;IACzE,KAAK,CAAC,EAAE,SAAS,IAAI,EAAE,CAAC;IACxB,SAAS,CAAC,EAAE,QAAQ,GAAG,UAAU,CAAC;IAClC,UAAU,CAAC,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAC7C,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AA2CD,wBAAgB,IAAI,CAAC,KAAK,EAAE,SAAS,GAAG,IAAI,CAkY3C"}
|
package/dist/core/integrator.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* untouched. Reduced motion (`dt = 0`) freezes the sim (§18).
|
|
8
8
|
*/
|
|
9
9
|
import { accretionTarget } from "./formations.js";
|
|
10
|
-
import { waveYat, waveSlope } from "./currents.js";
|
|
10
|
+
import { waveYat, waveSlope, waveDistance } from "./currents.js";
|
|
11
11
|
import { netField } from "./streamlines.js";
|
|
12
12
|
import { screenFactor } from "./math.js";
|
|
13
13
|
import { classifyBodyTokens } from "../config/forces.config.js";
|
|
@@ -55,7 +55,7 @@ function applyForce(f, b, p, env, inv) {
|
|
|
55
55
|
p.vz = bvz + (p.vz - bvz) * inv;
|
|
56
56
|
}
|
|
57
57
|
export function step(input) {
|
|
58
|
-
const { store, bodies, env, forces, conditions, waves } = input;
|
|
58
|
+
const { store, bodies, env, forces, conditions, waves, separation } = input;
|
|
59
59
|
const dt = env.dt;
|
|
60
60
|
if (dt === 0)
|
|
61
61
|
return;
|
|
@@ -112,18 +112,51 @@ export function step(input) {
|
|
|
112
112
|
const pz = p.z;
|
|
113
113
|
// wave current (§2.3): near a wave line, drift along its slope like debris.
|
|
114
114
|
if (hasWaves) {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
115
|
+
if (input.waveStyle === 'circular') {
|
|
116
|
+
let near = null;
|
|
117
|
+
let nd = 1e9;
|
|
118
|
+
let nearR = 0;
|
|
119
|
+
let nearRWave = 0;
|
|
120
|
+
let nearTheta = 0;
|
|
121
|
+
const c = input.waveCenter || { x: W / 2, y: H / 2 };
|
|
122
|
+
for (const w of waves) {
|
|
123
|
+
const res = waveDistance(w, p.x, p.y, env.t, W, H, 'circular', c);
|
|
124
|
+
if (res.dist < nd) {
|
|
125
|
+
nd = res.dist;
|
|
126
|
+
near = w;
|
|
127
|
+
nearR = res.r;
|
|
128
|
+
nearRWave = res.rWave;
|
|
129
|
+
nearTheta = res.theta;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (near && nd < 70) {
|
|
133
|
+
const factor = 1 - nd / 70;
|
|
134
|
+
// Tangential drift
|
|
135
|
+
const tx = -Math.sin(nearTheta) * near.dir;
|
|
136
|
+
const ty = Math.cos(nearTheta) * near.dir;
|
|
137
|
+
p.vx += tx * 0.035 * factor;
|
|
138
|
+
p.vy += ty * 0.035 * factor;
|
|
139
|
+
// Radial pull towards the wave radius
|
|
140
|
+
const rx = Math.cos(nearTheta) * Math.sign(nearRWave - nearR);
|
|
141
|
+
const ry = Math.sin(nearTheta) * Math.sign(nearRWave - nearR);
|
|
142
|
+
p.vx += rx * 0.05 * factor;
|
|
143
|
+
p.vy += ry * 0.05 * factor;
|
|
122
144
|
}
|
|
123
145
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
146
|
+
else {
|
|
147
|
+
let near = null;
|
|
148
|
+
let nd = 1e9;
|
|
149
|
+
for (const w of waves) {
|
|
150
|
+
const d = Math.abs(waveYat(w, p.x, env.t, H) - p.y);
|
|
151
|
+
if (d < nd) {
|
|
152
|
+
nd = d;
|
|
153
|
+
near = w;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (near && nd < 70) {
|
|
157
|
+
p.vx += near.dir * 0.035 * (1 - nd / 70);
|
|
158
|
+
p.vy += waveSlope(near, p.x, env.t) * 0.1 * (1 - nd / 70);
|
|
159
|
+
}
|
|
127
160
|
}
|
|
128
161
|
}
|
|
129
162
|
// formation currents (§7), before the body forces: a lateral lane, an
|
|
@@ -302,6 +335,23 @@ export function step(input) {
|
|
|
302
335
|
}
|
|
303
336
|
}
|
|
304
337
|
}
|
|
338
|
+
// short-range particle-to-particle separation to prevent clumping
|
|
339
|
+
if (separation && separation > 0) {
|
|
340
|
+
const ns = env.neighbors(p, 12);
|
|
341
|
+
for (const n of ns) {
|
|
342
|
+
const dx = p.x - n.x;
|
|
343
|
+
const dy = p.y - n.y;
|
|
344
|
+
const dz = (p.z ?? 0) - (n.z ?? 0);
|
|
345
|
+
const dist = Math.hypot(dx, dy, dz) || 0.1;
|
|
346
|
+
if (dist < 12) {
|
|
347
|
+
const force = ((12 - dist) / 12) * separation * 0.12;
|
|
348
|
+
p.vx += (dx / dist) * force;
|
|
349
|
+
p.vy += (dy / dist) * force;
|
|
350
|
+
if (p.vz !== undefined)
|
|
351
|
+
p.vz += (dz / dist) * force;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
305
355
|
// global safety cap (§20.10): no token or composite may drive a free particle past
|
|
306
356
|
// c (the unit system's "speed of light"). The natural primitives self-clamp; this
|
|
307
357
|
// enforces it for *every* force. A non-finite velocity slips the `> c²` test — the
|