@modernrelay/orbit-core 0.2.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 +21 -0
- package/dist/chunk-Z7FOEASL.js +141 -0
- package/dist/chunk-Z7FOEASL.js.map +1 -0
- package/dist/clusters-ExqnvobT.d.ts +123 -0
- package/dist/engine.d.ts +1 -0
- package/dist/engine.js +3 -0
- package/dist/engine.js.map +1 -0
- package/dist/index-BPjuELfY.d.ts +1198 -0
- package/dist/index.d.ts +2995 -0
- package/dist/index.js +11938 -0
- package/dist/index.js.map +1 -0
- package/dist/testing.d.ts +163 -0
- package/dist/testing.js +384 -0
- package/dist/testing.js.map +1 -0
- package/package.json +45 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { a3 as GraphEngine, a5 as EngineCapabilities, a4 as EngineCommit, az as EngineHostEvents, aA as FitViewOptions, V as ViewportState, aB as EngineDiagnostic } from './index-BPjuELfY.js';
|
|
2
|
+
export { b as clusterProbe, e as resetClusterProbe } from './clusters-ExqnvobT.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* FakeEngine — the public headless test seam (§18 /testing, §19).
|
|
6
|
+
*
|
|
7
|
+
* Implements the full GraphEngine adapter contract with zero DOM/WebGL access
|
|
8
|
+
* so consumer suites can run under node/jsdom. Every public method call is
|
|
9
|
+
* recorded to `calls` in order; commits and camera calls get typed convenience
|
|
10
|
+
* views. Test-only helpers (`stepFrame`, `nudgePositions`, `inject*`) let
|
|
11
|
+
* suites observe applied-revision lag, simulate position drift, and drive
|
|
12
|
+
* host events.
|
|
13
|
+
*
|
|
14
|
+
* The state MIRRORS (`pinnedIndices`, `selectedIndices`, `lastStructure`,
|
|
15
|
+
* `lastBuffer`) answer "what does the engine currently hold?", which the raw
|
|
16
|
+
* `calls`/`commits` logs cannot: pins, highlight, structure and each buffer
|
|
17
|
+
* channel are committed on independent schedules, so the newest entry is
|
|
18
|
+
* rarely the newest value for a given channel.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
interface FakeEngineOptions {
|
|
22
|
+
/** Overrides merged over the defaults (linkPicking/trackedPositions false, rangeUpdates [], simulation true). */
|
|
23
|
+
capabilities?: Partial<EngineCapabilities>;
|
|
24
|
+
/**
|
|
25
|
+
* When true, commits queue instead of applying immediately; `stepFrame()`
|
|
26
|
+
* applies the oldest queued commit. Lets tests observe the lag between the
|
|
27
|
+
* desired-render revision and `appliedRevision()`.
|
|
28
|
+
*/
|
|
29
|
+
manualFrames?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* When set, `mount()` records the call, does NOT store the host events, and
|
|
32
|
+
* rejects with this error — simulates an engine that cannot initialize.
|
|
33
|
+
*/
|
|
34
|
+
mountError?: Error;
|
|
35
|
+
/** Resolved by `captureScreenshot()`; default null (unsupported/not ready). */
|
|
36
|
+
screenshot?: Blob | null;
|
|
37
|
+
}
|
|
38
|
+
/** One recorded public method invocation. */
|
|
39
|
+
interface RecordedCall {
|
|
40
|
+
method: string;
|
|
41
|
+
args: readonly unknown[];
|
|
42
|
+
}
|
|
43
|
+
declare class FakeEngine implements GraphEngine {
|
|
44
|
+
readonly capabilities: EngineCapabilities;
|
|
45
|
+
/** Every public method call, in order. */
|
|
46
|
+
readonly calls: RecordedCall[];
|
|
47
|
+
/** Every commit passed to `commit()`, in call order (queued or applied). */
|
|
48
|
+
readonly commits: EngineCommit[];
|
|
49
|
+
/** The fitView/zoom/setViewport/zoomToIndex entries of `calls`, in order. */
|
|
50
|
+
readonly cameraCalls: RecordedCall[];
|
|
51
|
+
private readonly manualFrames;
|
|
52
|
+
private readonly mountError;
|
|
53
|
+
private readonly screenshot;
|
|
54
|
+
private events;
|
|
55
|
+
/** Auto-advancing activity clock for emitFrame()/stepFrame() (ms). */
|
|
56
|
+
private frameClock;
|
|
57
|
+
private destroyedFlag;
|
|
58
|
+
private viewport;
|
|
59
|
+
private applied;
|
|
60
|
+
private positions;
|
|
61
|
+
/** Links of the most recent structure-bearing commit (commit-time, even in
|
|
62
|
+
* manualFrames mode) — the adjacency source for neighborIndices(). */
|
|
63
|
+
private lastLinks;
|
|
64
|
+
private pinned;
|
|
65
|
+
private selected;
|
|
66
|
+
/** Commits awaiting stepFrame() in manualFrames mode (oldest first). */
|
|
67
|
+
private readonly pendingFrames;
|
|
68
|
+
constructor(options?: FakeEngineOptions);
|
|
69
|
+
/** Most recent commit, if any. */
|
|
70
|
+
get lastCommit(): EngineCommit | undefined;
|
|
71
|
+
get destroyed(): boolean;
|
|
72
|
+
/** Last setPinnedIndices() value (null = unpinned / never pinned). */
|
|
73
|
+
get pinnedIndices(): readonly number[] | null;
|
|
74
|
+
/** Last setSelectedIndices() value (null = cleared / never selected) — the
|
|
75
|
+
* §9.1 highlight mirror of {@link pinnedIndices}. */
|
|
76
|
+
get selectedIndices(): readonly number[] | null;
|
|
77
|
+
/**
|
|
78
|
+
* Structure of the most recent structure-bearing commit — the geometry the
|
|
79
|
+
* engine currently holds. Buffer channels commit independently of structure
|
|
80
|
+
* (§9.1 mask fast path), so the newest commit often carries none.
|
|
81
|
+
*/
|
|
82
|
+
get lastStructure(): NonNullable<EngineCommit['structure']> | undefined;
|
|
83
|
+
/**
|
|
84
|
+
* Most recently committed value of ONE buffer channel — the engine's current
|
|
85
|
+
* content for it. Channels are committed independently (a mask drain may
|
|
86
|
+
* carry `pointColor` alone), so scanning back per channel is the only way to
|
|
87
|
+
* read composed state; the newest commit alone is not the whole picture.
|
|
88
|
+
*/
|
|
89
|
+
lastBuffer(channel: keyof NonNullable<EngineCommit['buffers']>): Float32Array | undefined;
|
|
90
|
+
/** Headless: never touches `container` — a dummy cast works under node. */
|
|
91
|
+
mount(container: HTMLElement, events: EngineHostEvents): Promise<void>;
|
|
92
|
+
commit(update: EngineCommit): void;
|
|
93
|
+
appliedRevision(): number | null;
|
|
94
|
+
fitView(opts?: FitViewOptions): void;
|
|
95
|
+
zoom(factor: number, durationMs?: number): void;
|
|
96
|
+
setViewport(v: Partial<ViewportState>, opts?: {
|
|
97
|
+
durationMs?: number;
|
|
98
|
+
}): void;
|
|
99
|
+
getViewport(): ViewportState | null;
|
|
100
|
+
zoomToIndex(index: number, durationMs?: number): void;
|
|
101
|
+
start(alpha?: number): void;
|
|
102
|
+
pause(): void;
|
|
103
|
+
setSelectedIndices(indices: readonly number[] | null): void;
|
|
104
|
+
setFocusedIndex(index: number | null): void;
|
|
105
|
+
/** Screen == space in the FakeEngine: ray-cast over current positions,
|
|
106
|
+
* skipping NaN pairs (not-yet-applied structure in manualFrames mode). */
|
|
107
|
+
pointsInPolygon(screenPolygon: readonly [number, number][]): number[];
|
|
108
|
+
/** Screen == space: point indices inside `[x0, y0, x1, y1]` (bounds
|
|
109
|
+
* normalized), skipping NaN pairs (not-yet-applied structure). */
|
|
110
|
+
pointsInRect(screenRect: readonly [number, number, number, number]): number[];
|
|
111
|
+
/** Resolves the constructor's `screenshot` option (null by default). */
|
|
112
|
+
captureScreenshot(): Promise<Blob | null>;
|
|
113
|
+
/** 1-hop adjacency from the last committed links (self-loops excluded). */
|
|
114
|
+
neighborIndices(index: number): number[];
|
|
115
|
+
/** Identity transform: FakeEngine screen coordinates ARE space coordinates. */
|
|
116
|
+
screenToSpace(p: readonly [number, number]): [number, number] | null;
|
|
117
|
+
/** Identity transform: FakeEngine screen coordinates ARE space coordinates. */
|
|
118
|
+
spaceToScreen(p: readonly [number, number]): [number, number] | null;
|
|
119
|
+
setPinnedIndices(indices: readonly number[] | null): void;
|
|
120
|
+
getPositions(): Float32Array | null;
|
|
121
|
+
destroy(): void;
|
|
122
|
+
/**
|
|
123
|
+
* manualFrames mode: apply the oldest queued commit, making its revision
|
|
124
|
+
* visible via appliedRevision(). No-op on the queue when empty. A drawn
|
|
125
|
+
* frame is also an activity-clock sample, so a mounted engine gets an
|
|
126
|
+
* `onFrame` tick (§13 overlay/activity clock).
|
|
127
|
+
*/
|
|
128
|
+
stepFrame(): void;
|
|
129
|
+
/**
|
|
130
|
+
* Drive the host `onFrame` activity clock. `timeMs` sets the clock
|
|
131
|
+
* explicitly; omitted, the clock auto-advances by 16ms per call.
|
|
132
|
+
*/
|
|
133
|
+
emitFrame(timeMs?: number): void;
|
|
134
|
+
/** Simulate simulation drift: shift every current position by (dx, dy). */
|
|
135
|
+
nudgePositions(dx: number, dy: number): void;
|
|
136
|
+
injectPointClick(index: number | null, modifiers?: {
|
|
137
|
+
metaKey: boolean;
|
|
138
|
+
shiftKey: boolean;
|
|
139
|
+
}): void;
|
|
140
|
+
injectPointHover(index: number | null): void;
|
|
141
|
+
injectLinkClick(linkIndex: number): void;
|
|
142
|
+
injectLinkHover(linkIndex: number | null): void;
|
|
143
|
+
injectDragStart(index: number): void;
|
|
144
|
+
injectDragEnd(index: number, x: number, y: number): void;
|
|
145
|
+
injectContextMenu(index: number | null, screen: readonly [number, number]): void;
|
|
146
|
+
injectViewportChange(v: ViewportState): void;
|
|
147
|
+
injectSimulationEnd(): void;
|
|
148
|
+
injectError(error: Error): void;
|
|
149
|
+
injectContextLost(): void;
|
|
150
|
+
injectContextRestored(): void;
|
|
151
|
+
injectContextFailed(error: Error): void;
|
|
152
|
+
injectEngineDiagnostic(d: EngineDiagnostic): void;
|
|
153
|
+
private record;
|
|
154
|
+
private requireMounted;
|
|
155
|
+
/**
|
|
156
|
+
* Make a commit visible: rebuild positions when structure is present (NaN
|
|
157
|
+
* pairs seeded deterministically, known coordinates kept verbatim) and
|
|
158
|
+
* advance the applied revision.
|
|
159
|
+
*/
|
|
160
|
+
private apply;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export { EngineDiagnostic, FakeEngine, type FakeEngineOptions, type RecordedCall };
|
package/dist/testing.js
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
export { clusterProbe, resetClusterProbe } from './chunk-Z7FOEASL.js';
|
|
2
|
+
|
|
3
|
+
// src/testing/FakeEngine.ts
|
|
4
|
+
var DEFAULT_CAPABILITIES = {
|
|
5
|
+
linkPicking: false,
|
|
6
|
+
rangeUpdates: [],
|
|
7
|
+
trackedPositions: false,
|
|
8
|
+
simulation: true
|
|
9
|
+
};
|
|
10
|
+
var CAMERA_METHODS = /* @__PURE__ */ new Set([
|
|
11
|
+
"fitView",
|
|
12
|
+
"zoom",
|
|
13
|
+
"setViewport",
|
|
14
|
+
"zoomToIndex"
|
|
15
|
+
]);
|
|
16
|
+
function seedPosition(index) {
|
|
17
|
+
return [index % 100 * 10, Math.floor(index / 100) * 10];
|
|
18
|
+
}
|
|
19
|
+
function pointInPolygon(x, y, polygon) {
|
|
20
|
+
let inside = false;
|
|
21
|
+
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
|
|
22
|
+
const [xi, yi] = polygon[i];
|
|
23
|
+
const [xj, yj] = polygon[j];
|
|
24
|
+
const crosses = yi > y !== yj > y && x < (xj - xi) * (y - yi) / (yj - yi) + xi;
|
|
25
|
+
if (crosses) inside = !inside;
|
|
26
|
+
}
|
|
27
|
+
return inside;
|
|
28
|
+
}
|
|
29
|
+
var FakeEngine = class {
|
|
30
|
+
capabilities;
|
|
31
|
+
/** Every public method call, in order. */
|
|
32
|
+
calls = [];
|
|
33
|
+
/** Every commit passed to `commit()`, in call order (queued or applied). */
|
|
34
|
+
commits = [];
|
|
35
|
+
/** The fitView/zoom/setViewport/zoomToIndex entries of `calls`, in order. */
|
|
36
|
+
cameraCalls = [];
|
|
37
|
+
manualFrames;
|
|
38
|
+
mountError;
|
|
39
|
+
screenshot;
|
|
40
|
+
events = null;
|
|
41
|
+
/** Auto-advancing activity clock for emitFrame()/stepFrame() (ms). */
|
|
42
|
+
frameClock = 0;
|
|
43
|
+
destroyedFlag = false;
|
|
44
|
+
viewport = { x: 0, y: 0, zoom: 1 };
|
|
45
|
+
applied = null;
|
|
46
|
+
positions = null;
|
|
47
|
+
/** Links of the most recent structure-bearing commit (commit-time, even in
|
|
48
|
+
* manualFrames mode) — the adjacency source for neighborIndices(). */
|
|
49
|
+
lastLinks = null;
|
|
50
|
+
pinned = null;
|
|
51
|
+
selected = null;
|
|
52
|
+
/** Commits awaiting stepFrame() in manualFrames mode (oldest first). */
|
|
53
|
+
pendingFrames = [];
|
|
54
|
+
constructor(options = {}) {
|
|
55
|
+
this.capabilities = { ...DEFAULT_CAPABILITIES, ...options.capabilities };
|
|
56
|
+
this.manualFrames = options.manualFrames ?? false;
|
|
57
|
+
this.mountError = options.mountError ?? null;
|
|
58
|
+
this.screenshot = options.screenshot ?? null;
|
|
59
|
+
}
|
|
60
|
+
/** Most recent commit, if any. */
|
|
61
|
+
get lastCommit() {
|
|
62
|
+
return this.commits[this.commits.length - 1];
|
|
63
|
+
}
|
|
64
|
+
get destroyed() {
|
|
65
|
+
return this.destroyedFlag;
|
|
66
|
+
}
|
|
67
|
+
/** Last setPinnedIndices() value (null = unpinned / never pinned). */
|
|
68
|
+
get pinnedIndices() {
|
|
69
|
+
return this.pinned;
|
|
70
|
+
}
|
|
71
|
+
/** Last setSelectedIndices() value (null = cleared / never selected) — the
|
|
72
|
+
* §9.1 highlight mirror of {@link pinnedIndices}. */
|
|
73
|
+
get selectedIndices() {
|
|
74
|
+
return this.selected;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Structure of the most recent structure-bearing commit — the geometry the
|
|
78
|
+
* engine currently holds. Buffer channels commit independently of structure
|
|
79
|
+
* (§9.1 mask fast path), so the newest commit often carries none.
|
|
80
|
+
*/
|
|
81
|
+
get lastStructure() {
|
|
82
|
+
for (let i = this.commits.length - 1; i >= 0; i--) {
|
|
83
|
+
const structure = this.commits[i].structure;
|
|
84
|
+
if (structure !== void 0) return structure;
|
|
85
|
+
}
|
|
86
|
+
return void 0;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Most recently committed value of ONE buffer channel — the engine's current
|
|
90
|
+
* content for it. Channels are committed independently (a mask drain may
|
|
91
|
+
* carry `pointColor` alone), so scanning back per channel is the only way to
|
|
92
|
+
* read composed state; the newest commit alone is not the whole picture.
|
|
93
|
+
*/
|
|
94
|
+
lastBuffer(channel) {
|
|
95
|
+
for (let i = this.commits.length - 1; i >= 0; i--) {
|
|
96
|
+
const buffers = this.commits[i].buffers;
|
|
97
|
+
const value = buffers?.[channel];
|
|
98
|
+
if (value !== void 0) return value;
|
|
99
|
+
}
|
|
100
|
+
return void 0;
|
|
101
|
+
}
|
|
102
|
+
// --- GraphEngine contract ---
|
|
103
|
+
/** Headless: never touches `container` — a dummy cast works under node. */
|
|
104
|
+
mount(container, events) {
|
|
105
|
+
this.record("mount", [container, events]);
|
|
106
|
+
if (this.mountError !== null) return Promise.reject(this.mountError);
|
|
107
|
+
this.events = events;
|
|
108
|
+
return Promise.resolve();
|
|
109
|
+
}
|
|
110
|
+
commit(update) {
|
|
111
|
+
if (this.destroyedFlag) {
|
|
112
|
+
throw new Error("FakeEngine: commit() called after destroy()");
|
|
113
|
+
}
|
|
114
|
+
this.record("commit", [update]);
|
|
115
|
+
this.commits.push(update);
|
|
116
|
+
if (update.structure !== void 0) this.lastLinks = update.structure.links;
|
|
117
|
+
if (this.manualFrames) {
|
|
118
|
+
this.pendingFrames.push(update);
|
|
119
|
+
} else {
|
|
120
|
+
this.apply(update);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
appliedRevision() {
|
|
124
|
+
this.record("appliedRevision", []);
|
|
125
|
+
return this.applied;
|
|
126
|
+
}
|
|
127
|
+
fitView(opts) {
|
|
128
|
+
this.record("fitView", [opts]);
|
|
129
|
+
}
|
|
130
|
+
zoom(factor, durationMs) {
|
|
131
|
+
this.record("zoom", [factor, durationMs]);
|
|
132
|
+
}
|
|
133
|
+
setViewport(v, opts) {
|
|
134
|
+
this.record("setViewport", [v, opts]);
|
|
135
|
+
this.viewport = { ...this.viewport, ...v };
|
|
136
|
+
}
|
|
137
|
+
getViewport() {
|
|
138
|
+
this.record("getViewport", []);
|
|
139
|
+
return { ...this.viewport };
|
|
140
|
+
}
|
|
141
|
+
zoomToIndex(index, durationMs) {
|
|
142
|
+
this.record("zoomToIndex", [index, durationMs]);
|
|
143
|
+
}
|
|
144
|
+
start(alpha) {
|
|
145
|
+
this.record("start", [alpha]);
|
|
146
|
+
}
|
|
147
|
+
pause() {
|
|
148
|
+
this.record("pause", []);
|
|
149
|
+
}
|
|
150
|
+
setSelectedIndices(indices) {
|
|
151
|
+
this.record("setSelectedIndices", [indices]);
|
|
152
|
+
this.selected = indices === null ? null : [...indices];
|
|
153
|
+
}
|
|
154
|
+
setFocusedIndex(index) {
|
|
155
|
+
this.record("setFocusedIndex", [index]);
|
|
156
|
+
}
|
|
157
|
+
/** Screen == space in the FakeEngine: ray-cast over current positions,
|
|
158
|
+
* skipping NaN pairs (not-yet-applied structure in manualFrames mode). */
|
|
159
|
+
pointsInPolygon(screenPolygon) {
|
|
160
|
+
this.record("pointsInPolygon", [screenPolygon]);
|
|
161
|
+
const pos = this.positions;
|
|
162
|
+
const out = [];
|
|
163
|
+
if (pos === null || screenPolygon.length < 3) return out;
|
|
164
|
+
for (let i = 0; i < pos.length / 2; i++) {
|
|
165
|
+
const x = pos[2 * i];
|
|
166
|
+
const y = pos[2 * i + 1];
|
|
167
|
+
if (Number.isNaN(x) || Number.isNaN(y)) continue;
|
|
168
|
+
if (pointInPolygon(x, y, screenPolygon)) out.push(i);
|
|
169
|
+
}
|
|
170
|
+
return out;
|
|
171
|
+
}
|
|
172
|
+
/** Screen == space: point indices inside `[x0, y0, x1, y1]` (bounds
|
|
173
|
+
* normalized), skipping NaN pairs (not-yet-applied structure). */
|
|
174
|
+
pointsInRect(screenRect) {
|
|
175
|
+
this.record("pointsInRect", [screenRect]);
|
|
176
|
+
const pos = this.positions;
|
|
177
|
+
const out = [];
|
|
178
|
+
if (pos === null) return out;
|
|
179
|
+
const minX = Math.min(screenRect[0], screenRect[2]);
|
|
180
|
+
const maxX = Math.max(screenRect[0], screenRect[2]);
|
|
181
|
+
const minY = Math.min(screenRect[1], screenRect[3]);
|
|
182
|
+
const maxY = Math.max(screenRect[1], screenRect[3]);
|
|
183
|
+
for (let i = 0; i < pos.length / 2; i++) {
|
|
184
|
+
const x = pos[2 * i];
|
|
185
|
+
const y = pos[2 * i + 1];
|
|
186
|
+
if (Number.isNaN(x) || Number.isNaN(y)) continue;
|
|
187
|
+
if (x >= minX && x <= maxX && y >= minY && y <= maxY) out.push(i);
|
|
188
|
+
}
|
|
189
|
+
return out;
|
|
190
|
+
}
|
|
191
|
+
/** Resolves the constructor's `screenshot` option (null by default). */
|
|
192
|
+
captureScreenshot() {
|
|
193
|
+
this.record("captureScreenshot", []);
|
|
194
|
+
return Promise.resolve(this.screenshot);
|
|
195
|
+
}
|
|
196
|
+
/** 1-hop adjacency from the last committed links (self-loops excluded). */
|
|
197
|
+
neighborIndices(index) {
|
|
198
|
+
this.record("neighborIndices", [index]);
|
|
199
|
+
const links = this.lastLinks;
|
|
200
|
+
if (links === null) return [];
|
|
201
|
+
const out = /* @__PURE__ */ new Set();
|
|
202
|
+
for (let k = 0; k < links.length; k += 2) {
|
|
203
|
+
const s = links[k];
|
|
204
|
+
const t = links[k + 1];
|
|
205
|
+
if (s === index && t !== index) out.add(t);
|
|
206
|
+
if (t === index && s !== index) out.add(s);
|
|
207
|
+
}
|
|
208
|
+
return [...out];
|
|
209
|
+
}
|
|
210
|
+
/** Identity transform: FakeEngine screen coordinates ARE space coordinates. */
|
|
211
|
+
screenToSpace(p) {
|
|
212
|
+
this.record("screenToSpace", [p]);
|
|
213
|
+
return [p[0], p[1]];
|
|
214
|
+
}
|
|
215
|
+
/** Identity transform: FakeEngine screen coordinates ARE space coordinates. */
|
|
216
|
+
spaceToScreen(p) {
|
|
217
|
+
this.record("spaceToScreen", [p]);
|
|
218
|
+
return [p[0], p[1]];
|
|
219
|
+
}
|
|
220
|
+
setPinnedIndices(indices) {
|
|
221
|
+
this.record("setPinnedIndices", [indices]);
|
|
222
|
+
this.pinned = indices === null ? null : [...indices];
|
|
223
|
+
}
|
|
224
|
+
getPositions() {
|
|
225
|
+
this.record("getPositions", []);
|
|
226
|
+
return this.positions === null ? null : new Float32Array(this.positions);
|
|
227
|
+
}
|
|
228
|
+
destroy() {
|
|
229
|
+
this.record("destroy", []);
|
|
230
|
+
this.destroyedFlag = true;
|
|
231
|
+
this.events = null;
|
|
232
|
+
}
|
|
233
|
+
// --- test helpers (not part of GraphEngine) ---
|
|
234
|
+
/**
|
|
235
|
+
* manualFrames mode: apply the oldest queued commit, making its revision
|
|
236
|
+
* visible via appliedRevision(). No-op on the queue when empty. A drawn
|
|
237
|
+
* frame is also an activity-clock sample, so a mounted engine gets an
|
|
238
|
+
* `onFrame` tick (§13 overlay/activity clock).
|
|
239
|
+
*/
|
|
240
|
+
stepFrame() {
|
|
241
|
+
this.record("stepFrame", []);
|
|
242
|
+
const next = this.pendingFrames.shift();
|
|
243
|
+
if (next !== void 0) this.apply(next);
|
|
244
|
+
if (this.events !== null) {
|
|
245
|
+
this.frameClock += 16;
|
|
246
|
+
this.events.onFrame?.(this.frameClock);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Drive the host `onFrame` activity clock. `timeMs` sets the clock
|
|
251
|
+
* explicitly; omitted, the clock auto-advances by 16ms per call.
|
|
252
|
+
*/
|
|
253
|
+
emitFrame(timeMs) {
|
|
254
|
+
const events = this.requireMounted("emitFrame");
|
|
255
|
+
this.frameClock = timeMs ?? this.frameClock + 16;
|
|
256
|
+
this.record("emitFrame", [this.frameClock]);
|
|
257
|
+
events.onFrame?.(this.frameClock);
|
|
258
|
+
}
|
|
259
|
+
/** Simulate simulation drift: shift every current position by (dx, dy). */
|
|
260
|
+
nudgePositions(dx, dy) {
|
|
261
|
+
this.record("nudgePositions", [dx, dy]);
|
|
262
|
+
const pos = this.positions;
|
|
263
|
+
if (pos === null) return;
|
|
264
|
+
for (let i = 0; i < pos.length; i += 2) {
|
|
265
|
+
pos[i] = pos[i] + dx;
|
|
266
|
+
pos[i + 1] = pos[i + 1] + dy;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
injectPointClick(index, modifiers) {
|
|
270
|
+
const events = this.requireMounted("injectPointClick");
|
|
271
|
+
this.record("injectPointClick", [index, modifiers]);
|
|
272
|
+
events.onPointClick?.(index, modifiers);
|
|
273
|
+
}
|
|
274
|
+
injectPointHover(index) {
|
|
275
|
+
const events = this.requireMounted("injectPointHover");
|
|
276
|
+
this.record("injectPointHover", [index]);
|
|
277
|
+
events.onPointHover?.(index);
|
|
278
|
+
}
|
|
279
|
+
injectLinkClick(linkIndex) {
|
|
280
|
+
const events = this.requireMounted("injectLinkClick");
|
|
281
|
+
this.record("injectLinkClick", [linkIndex]);
|
|
282
|
+
events.onLinkClick?.(linkIndex);
|
|
283
|
+
}
|
|
284
|
+
injectLinkHover(linkIndex) {
|
|
285
|
+
const events = this.requireMounted("injectLinkHover");
|
|
286
|
+
this.record("injectLinkHover", [linkIndex]);
|
|
287
|
+
events.onLinkHover?.(linkIndex);
|
|
288
|
+
}
|
|
289
|
+
injectDragStart(index) {
|
|
290
|
+
const events = this.requireMounted("injectDragStart");
|
|
291
|
+
this.record("injectDragStart", [index]);
|
|
292
|
+
events.onDragStart?.(index);
|
|
293
|
+
}
|
|
294
|
+
injectDragEnd(index, x, y) {
|
|
295
|
+
const events = this.requireMounted("injectDragEnd");
|
|
296
|
+
this.record("injectDragEnd", [index, x, y]);
|
|
297
|
+
events.onDragEnd?.(index, x, y);
|
|
298
|
+
}
|
|
299
|
+
injectContextMenu(index, screen) {
|
|
300
|
+
const events = this.requireMounted("injectContextMenu");
|
|
301
|
+
this.record("injectContextMenu", [index, screen]);
|
|
302
|
+
events.onContextMenu?.(index, screen);
|
|
303
|
+
}
|
|
304
|
+
injectViewportChange(v) {
|
|
305
|
+
const events = this.requireMounted("injectViewportChange");
|
|
306
|
+
this.record("injectViewportChange", [v]);
|
|
307
|
+
this.viewport = { ...v };
|
|
308
|
+
events.onViewportChange?.(v);
|
|
309
|
+
}
|
|
310
|
+
injectSimulationEnd() {
|
|
311
|
+
const events = this.requireMounted("injectSimulationEnd");
|
|
312
|
+
this.record("injectSimulationEnd", []);
|
|
313
|
+
events.onSimulationEnd?.();
|
|
314
|
+
}
|
|
315
|
+
injectError(error) {
|
|
316
|
+
const events = this.requireMounted("injectError");
|
|
317
|
+
this.record("injectError", [error]);
|
|
318
|
+
events.onError?.(error);
|
|
319
|
+
}
|
|
320
|
+
injectContextLost() {
|
|
321
|
+
const events = this.requireMounted("injectContextLost");
|
|
322
|
+
this.record("injectContextLost", []);
|
|
323
|
+
events.onContextEvent?.({ type: "lost" });
|
|
324
|
+
}
|
|
325
|
+
injectContextRestored() {
|
|
326
|
+
const events = this.requireMounted("injectContextRestored");
|
|
327
|
+
this.record("injectContextRestored", []);
|
|
328
|
+
events.onContextEvent?.({ type: "restored" });
|
|
329
|
+
}
|
|
330
|
+
injectContextFailed(error) {
|
|
331
|
+
const events = this.requireMounted("injectContextFailed");
|
|
332
|
+
this.record("injectContextFailed", [error]);
|
|
333
|
+
events.onContextEvent?.({ type: "failed", error });
|
|
334
|
+
}
|
|
335
|
+
injectEngineDiagnostic(d) {
|
|
336
|
+
const events = this.requireMounted("injectEngineDiagnostic");
|
|
337
|
+
this.record("injectEngineDiagnostic", [d]);
|
|
338
|
+
events.onDiagnostic?.(d);
|
|
339
|
+
}
|
|
340
|
+
// --- internals ---
|
|
341
|
+
record(method, args) {
|
|
342
|
+
while (args.length > 0 && args[args.length - 1] === void 0) args.pop();
|
|
343
|
+
const call = { method, args };
|
|
344
|
+
this.calls.push(call);
|
|
345
|
+
if (CAMERA_METHODS.has(method)) this.cameraCalls.push(call);
|
|
346
|
+
}
|
|
347
|
+
requireMounted(helper) {
|
|
348
|
+
const events = this.events;
|
|
349
|
+
if (events === null) {
|
|
350
|
+
throw new Error(`FakeEngine: ${helper}() requires a mounted engine \u2014 call mount() first`);
|
|
351
|
+
}
|
|
352
|
+
return events;
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Make a commit visible: rebuild positions when structure is present (NaN
|
|
356
|
+
* pairs seeded deterministically, known coordinates kept verbatim) and
|
|
357
|
+
* advance the applied revision.
|
|
358
|
+
*/
|
|
359
|
+
apply(update) {
|
|
360
|
+
const structure = update.structure;
|
|
361
|
+
if (structure !== void 0) {
|
|
362
|
+
const { pointCount, positions } = structure;
|
|
363
|
+
const next = new Float32Array(2 * pointCount);
|
|
364
|
+
for (let i = 0; i < pointCount; i++) {
|
|
365
|
+
const x = positions[2 * i];
|
|
366
|
+
const y = positions[2 * i + 1];
|
|
367
|
+
if (Number.isNaN(x) || Number.isNaN(y)) {
|
|
368
|
+
const [sx, sy] = seedPosition(i);
|
|
369
|
+
next[2 * i] = sx;
|
|
370
|
+
next[2 * i + 1] = sy;
|
|
371
|
+
} else {
|
|
372
|
+
next[2 * i] = x;
|
|
373
|
+
next[2 * i + 1] = y;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
this.positions = next;
|
|
377
|
+
}
|
|
378
|
+
this.applied = update.revision;
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
export { FakeEngine };
|
|
383
|
+
//# sourceMappingURL=testing.js.map
|
|
384
|
+
//# sourceMappingURL=testing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/testing/FakeEngine.ts"],"names":[],"mappings":"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","file":"testing.js","sourcesContent":["/**\n * FakeEngine — the public headless test seam (§18 /testing, §19).\n *\n * Implements the full GraphEngine adapter contract with zero DOM/WebGL access\n * so consumer suites can run under node/jsdom. Every public method call is\n * recorded to `calls` in order; commits and camera calls get typed convenience\n * views. Test-only helpers (`stepFrame`, `nudgePositions`, `inject*`) let\n * suites observe applied-revision lag, simulate position drift, and drive\n * host events.\n *\n * The state MIRRORS (`pinnedIndices`, `selectedIndices`, `lastStructure`,\n * `lastBuffer`) answer \"what does the engine currently hold?\", which the raw\n * `calls`/`commits` logs cannot: pins, highlight, structure and each buffer\n * channel are committed on independent schedules, so the newest entry is\n * rarely the newest value for a given channel.\n */\n\nimport type {\n EngineCapabilities,\n EngineCommit,\n EngineDiagnostic,\n EngineHostEvents,\n FitViewOptions,\n GraphEngine,\n} from '../engine/index';\nimport type { ViewportState } from '../types';\n\nexport interface FakeEngineOptions {\n /** Overrides merged over the defaults (linkPicking/trackedPositions false, rangeUpdates [], simulation true). */\n capabilities?: Partial<EngineCapabilities>;\n /**\n * When true, commits queue instead of applying immediately; `stepFrame()`\n * applies the oldest queued commit. Lets tests observe the lag between the\n * desired-render revision and `appliedRevision()`.\n */\n manualFrames?: boolean;\n /**\n * When set, `mount()` records the call, does NOT store the host events, and\n * rejects with this error — simulates an engine that cannot initialize.\n */\n mountError?: Error;\n /** Resolved by `captureScreenshot()`; default null (unsupported/not ready). */\n screenshot?: Blob | null;\n}\n\n/** One recorded public method invocation. */\nexport interface RecordedCall {\n method: string;\n args: readonly unknown[];\n}\n\nconst DEFAULT_CAPABILITIES: EngineCapabilities = {\n linkPicking: false,\n rangeUpdates: [],\n trackedPositions: false,\n simulation: true,\n};\n\nconst CAMERA_METHODS: ReadonlySet<string> = new Set([\n 'fitView',\n 'zoom',\n 'setViewport',\n 'zoomToIndex',\n]);\n\n/** Deterministic seed for an unknown (NaN) position: a 10px grid, 100 per row. */\nfunction seedPosition(index: number): readonly [number, number] {\n return [(index % 100) * 10, Math.floor(index / 100) * 10];\n}\n\n/** Standard even-odd ray cast (screen == space in the FakeEngine). */\nfunction pointInPolygon(x: number, y: number, polygon: readonly [number, number][]): boolean {\n let inside = false;\n for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {\n const [xi, yi] = polygon[i]!;\n const [xj, yj] = polygon[j]!;\n const crosses = yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi;\n if (crosses) inside = !inside;\n }\n return inside;\n}\n\nexport class FakeEngine implements GraphEngine {\n readonly capabilities: EngineCapabilities;\n\n /** Every public method call, in order. */\n readonly calls: RecordedCall[] = [];\n /** Every commit passed to `commit()`, in call order (queued or applied). */\n readonly commits: EngineCommit[] = [];\n /** The fitView/zoom/setViewport/zoomToIndex entries of `calls`, in order. */\n readonly cameraCalls: RecordedCall[] = [];\n\n private readonly manualFrames: boolean;\n private readonly mountError: Error | null;\n private readonly screenshot: Blob | null;\n private events: EngineHostEvents | null = null;\n /** Auto-advancing activity clock for emitFrame()/stepFrame() (ms). */\n private frameClock = 0;\n private destroyedFlag = false;\n private viewport: ViewportState = { x: 0, y: 0, zoom: 1 };\n private applied: number | null = null;\n private positions: Float32Array | null = null;\n /** Links of the most recent structure-bearing commit (commit-time, even in\n * manualFrames mode) — the adjacency source for neighborIndices(). */\n private lastLinks: Uint32Array | null = null;\n private pinned: readonly number[] | null = null;\n private selected: readonly number[] | null = null;\n /** Commits awaiting stepFrame() in manualFrames mode (oldest first). */\n private readonly pendingFrames: EngineCommit[] = [];\n\n constructor(options: FakeEngineOptions = {}) {\n this.capabilities = { ...DEFAULT_CAPABILITIES, ...options.capabilities };\n this.manualFrames = options.manualFrames ?? false;\n this.mountError = options.mountError ?? null;\n this.screenshot = options.screenshot ?? null;\n }\n\n /** Most recent commit, if any. */\n get lastCommit(): EngineCommit | undefined {\n return this.commits[this.commits.length - 1];\n }\n\n get destroyed(): boolean {\n return this.destroyedFlag;\n }\n\n /** Last setPinnedIndices() value (null = unpinned / never pinned). */\n get pinnedIndices(): readonly number[] | null {\n return this.pinned;\n }\n\n /** Last setSelectedIndices() value (null = cleared / never selected) — the\n * §9.1 highlight mirror of {@link pinnedIndices}. */\n get selectedIndices(): readonly number[] | null {\n return this.selected;\n }\n\n /**\n * Structure of the most recent structure-bearing commit — the geometry the\n * engine currently holds. Buffer channels commit independently of structure\n * (§9.1 mask fast path), so the newest commit often carries none.\n */\n get lastStructure(): NonNullable<EngineCommit['structure']> | undefined {\n for (let i = this.commits.length - 1; i >= 0; i--) {\n const structure = this.commits[i]!.structure;\n if (structure !== undefined) return structure;\n }\n return undefined;\n }\n\n /**\n * Most recently committed value of ONE buffer channel — the engine's current\n * content for it. Channels are committed independently (a mask drain may\n * carry `pointColor` alone), so scanning back per channel is the only way to\n * read composed state; the newest commit alone is not the whole picture.\n */\n lastBuffer(channel: keyof NonNullable<EngineCommit['buffers']>): Float32Array | undefined {\n for (let i = this.commits.length - 1; i >= 0; i--) {\n const buffers = this.commits[i]!.buffers;\n const value = buffers?.[channel];\n if (value !== undefined) return value;\n }\n return undefined;\n }\n\n // --- GraphEngine contract ---\n\n /** Headless: never touches `container` — a dummy cast works under node. */\n mount(container: HTMLElement, events: EngineHostEvents): Promise<void> {\n this.record('mount', [container, events]);\n if (this.mountError !== null) return Promise.reject(this.mountError);\n this.events = events;\n return Promise.resolve();\n }\n\n commit(update: EngineCommit): void {\n if (this.destroyedFlag) {\n throw new Error('FakeEngine: commit() called after destroy()');\n }\n this.record('commit', [update]);\n this.commits.push(update);\n if (update.structure !== undefined) this.lastLinks = update.structure.links;\n if (this.manualFrames) {\n this.pendingFrames.push(update);\n } else {\n this.apply(update);\n }\n }\n\n appliedRevision(): number | null {\n this.record('appliedRevision', []);\n return this.applied;\n }\n\n fitView(opts?: FitViewOptions): void {\n this.record('fitView', [opts]);\n }\n\n zoom(factor: number, durationMs?: number): void {\n this.record('zoom', [factor, durationMs]);\n }\n\n setViewport(v: Partial<ViewportState>, opts?: { durationMs?: number }): void {\n this.record('setViewport', [v, opts]);\n this.viewport = { ...this.viewport, ...v };\n }\n\n getViewport(): ViewportState | null {\n this.record('getViewport', []);\n return { ...this.viewport };\n }\n\n zoomToIndex(index: number, durationMs?: number): void {\n this.record('zoomToIndex', [index, durationMs]);\n }\n\n start(alpha?: number): void {\n this.record('start', [alpha]);\n }\n\n pause(): void {\n this.record('pause', []);\n }\n\n setSelectedIndices(indices: readonly number[] | null): void {\n this.record('setSelectedIndices', [indices]);\n this.selected = indices === null ? null : [...indices];\n }\n\n setFocusedIndex(index: number | null): void {\n this.record('setFocusedIndex', [index]);\n }\n\n /** Screen == space in the FakeEngine: ray-cast over current positions,\n * skipping NaN pairs (not-yet-applied structure in manualFrames mode). */\n pointsInPolygon(screenPolygon: readonly [number, number][]): number[] {\n this.record('pointsInPolygon', [screenPolygon]);\n const pos = this.positions;\n const out: number[] = [];\n if (pos === null || screenPolygon.length < 3) return out;\n for (let i = 0; i < pos.length / 2; i++) {\n const x = pos[2 * i]!;\n const y = pos[2 * i + 1]!;\n if (Number.isNaN(x) || Number.isNaN(y)) continue;\n if (pointInPolygon(x, y, screenPolygon)) out.push(i);\n }\n return out;\n }\n\n /** Screen == space: point indices inside `[x0, y0, x1, y1]` (bounds\n * normalized), skipping NaN pairs (not-yet-applied structure). */\n pointsInRect(screenRect: readonly [number, number, number, number]): number[] {\n this.record('pointsInRect', [screenRect]);\n const pos = this.positions;\n const out: number[] = [];\n if (pos === null) return out;\n const minX = Math.min(screenRect[0], screenRect[2]);\n const maxX = Math.max(screenRect[0], screenRect[2]);\n const minY = Math.min(screenRect[1], screenRect[3]);\n const maxY = Math.max(screenRect[1], screenRect[3]);\n for (let i = 0; i < pos.length / 2; i++) {\n const x = pos[2 * i]!;\n const y = pos[2 * i + 1]!;\n if (Number.isNaN(x) || Number.isNaN(y)) continue;\n if (x >= minX && x <= maxX && y >= minY && y <= maxY) out.push(i);\n }\n return out;\n }\n\n /** Resolves the constructor's `screenshot` option (null by default). */\n captureScreenshot(): Promise<Blob | null> {\n this.record('captureScreenshot', []);\n return Promise.resolve(this.screenshot);\n }\n\n /** 1-hop adjacency from the last committed links (self-loops excluded). */\n neighborIndices(index: number): number[] {\n this.record('neighborIndices', [index]);\n const links = this.lastLinks;\n if (links === null) return [];\n const out = new Set<number>();\n for (let k = 0; k < links.length; k += 2) {\n const s = links[k]!;\n const t = links[k + 1]!;\n if (s === index && t !== index) out.add(t);\n if (t === index && s !== index) out.add(s);\n }\n return [...out];\n }\n\n /** Identity transform: FakeEngine screen coordinates ARE space coordinates. */\n screenToSpace(p: readonly [number, number]): [number, number] | null {\n this.record('screenToSpace', [p]);\n return [p[0], p[1]];\n }\n\n /** Identity transform: FakeEngine screen coordinates ARE space coordinates. */\n spaceToScreen(p: readonly [number, number]): [number, number] | null {\n this.record('spaceToScreen', [p]);\n return [p[0], p[1]];\n }\n\n setPinnedIndices(indices: readonly number[] | null): void {\n this.record('setPinnedIndices', [indices]);\n this.pinned = indices === null ? null : [...indices];\n }\n\n getPositions(): Float32Array | null {\n this.record('getPositions', []);\n return this.positions === null ? null : new Float32Array(this.positions);\n }\n\n destroy(): void {\n this.record('destroy', []);\n this.destroyedFlag = true;\n this.events = null;\n }\n\n // --- test helpers (not part of GraphEngine) ---\n\n /**\n * manualFrames mode: apply the oldest queued commit, making its revision\n * visible via appliedRevision(). No-op on the queue when empty. A drawn\n * frame is also an activity-clock sample, so a mounted engine gets an\n * `onFrame` tick (§13 overlay/activity clock).\n */\n stepFrame(): void {\n this.record('stepFrame', []);\n const next = this.pendingFrames.shift();\n if (next !== undefined) this.apply(next);\n if (this.events !== null) {\n this.frameClock += 16;\n this.events.onFrame?.(this.frameClock);\n }\n }\n\n /**\n * Drive the host `onFrame` activity clock. `timeMs` sets the clock\n * explicitly; omitted, the clock auto-advances by 16ms per call.\n */\n emitFrame(timeMs?: number): void {\n const events = this.requireMounted('emitFrame');\n this.frameClock = timeMs ?? this.frameClock + 16;\n this.record('emitFrame', [this.frameClock]);\n events.onFrame?.(this.frameClock);\n }\n\n /** Simulate simulation drift: shift every current position by (dx, dy). */\n nudgePositions(dx: number, dy: number): void {\n this.record('nudgePositions', [dx, dy]);\n const pos = this.positions;\n if (pos === null) return;\n for (let i = 0; i < pos.length; i += 2) {\n pos[i] = pos[i]! + dx;\n pos[i + 1] = pos[i + 1]! + dy;\n }\n }\n\n injectPointClick(index: number | null, modifiers?: { metaKey: boolean; shiftKey: boolean }): void {\n const events = this.requireMounted('injectPointClick');\n this.record('injectPointClick', [index, modifiers]);\n events.onPointClick?.(index, modifiers);\n }\n\n injectPointHover(index: number | null): void {\n const events = this.requireMounted('injectPointHover');\n this.record('injectPointHover', [index]);\n events.onPointHover?.(index);\n }\n\n injectLinkClick(linkIndex: number): void {\n const events = this.requireMounted('injectLinkClick');\n this.record('injectLinkClick', [linkIndex]);\n events.onLinkClick?.(linkIndex);\n }\n\n injectLinkHover(linkIndex: number | null): void {\n const events = this.requireMounted('injectLinkHover');\n this.record('injectLinkHover', [linkIndex]);\n events.onLinkHover?.(linkIndex);\n }\n\n injectDragStart(index: number): void {\n const events = this.requireMounted('injectDragStart');\n this.record('injectDragStart', [index]);\n events.onDragStart?.(index);\n }\n\n injectDragEnd(index: number, x: number, y: number): void {\n const events = this.requireMounted('injectDragEnd');\n this.record('injectDragEnd', [index, x, y]);\n events.onDragEnd?.(index, x, y);\n }\n\n injectContextMenu(index: number | null, screen: readonly [number, number]): void {\n const events = this.requireMounted('injectContextMenu');\n this.record('injectContextMenu', [index, screen]);\n events.onContextMenu?.(index, screen);\n }\n\n injectViewportChange(v: ViewportState): void {\n const events = this.requireMounted('injectViewportChange');\n this.record('injectViewportChange', [v]);\n this.viewport = { ...v };\n events.onViewportChange?.(v);\n }\n\n injectSimulationEnd(): void {\n const events = this.requireMounted('injectSimulationEnd');\n this.record('injectSimulationEnd', []);\n events.onSimulationEnd?.();\n }\n\n injectError(error: Error): void {\n const events = this.requireMounted('injectError');\n this.record('injectError', [error]);\n events.onError?.(error);\n }\n\n injectContextLost(): void {\n const events = this.requireMounted('injectContextLost');\n this.record('injectContextLost', []);\n events.onContextEvent?.({ type: 'lost' });\n }\n\n injectContextRestored(): void {\n const events = this.requireMounted('injectContextRestored');\n this.record('injectContextRestored', []);\n events.onContextEvent?.({ type: 'restored' });\n }\n\n injectContextFailed(error: Error): void {\n const events = this.requireMounted('injectContextFailed');\n this.record('injectContextFailed', [error]);\n events.onContextEvent?.({ type: 'failed', error });\n }\n\n injectEngineDiagnostic(d: EngineDiagnostic): void {\n const events = this.requireMounted('injectEngineDiagnostic');\n this.record('injectEngineDiagnostic', [d]);\n events.onDiagnostic?.(d);\n }\n\n // --- internals ---\n\n private record(method: string, args: unknown[]): void {\n // Trim trailing omitted optionals so `args` matches the caller's shape.\n while (args.length > 0 && args[args.length - 1] === undefined) args.pop();\n const call: RecordedCall = { method, args };\n this.calls.push(call);\n if (CAMERA_METHODS.has(method)) this.cameraCalls.push(call);\n }\n\n private requireMounted(helper: string): EngineHostEvents {\n const events = this.events;\n if (events === null) {\n throw new Error(`FakeEngine: ${helper}() requires a mounted engine — call mount() first`);\n }\n return events;\n }\n\n /**\n * Make a commit visible: rebuild positions when structure is present (NaN\n * pairs seeded deterministically, known coordinates kept verbatim) and\n * advance the applied revision.\n */\n private apply(update: EngineCommit): void {\n const structure = update.structure;\n if (structure !== undefined) {\n const { pointCount, positions } = structure;\n const next = new Float32Array(2 * pointCount);\n for (let i = 0; i < pointCount; i++) {\n const x = positions[2 * i]!;\n const y = positions[2 * i + 1]!;\n if (Number.isNaN(x) || Number.isNaN(y)) {\n const [sx, sy] = seedPosition(i);\n next[2 * i] = sx;\n next[2 * i + 1] = sy;\n } else {\n next[2 * i] = x;\n next[2 * i + 1] = y;\n }\n }\n this.positions = next;\n }\n this.applied = update.revision;\n }\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@modernrelay/orbit-core",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Headless graph-visualization core: typed declarative snapshots reconciled into atomic engine commits. No React, no DOM, no engine imports.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/ModernRelay/orbit.git",
|
|
9
|
+
"directory": "packages/core"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"sideEffects": false,
|
|
13
|
+
"files": [
|
|
14
|
+
"dist"
|
|
15
|
+
],
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"default": "./dist/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./engine": {
|
|
22
|
+
"types": "./dist/engine.d.ts",
|
|
23
|
+
"default": "./dist/engine.js"
|
|
24
|
+
},
|
|
25
|
+
"./testing": {
|
|
26
|
+
"types": "./dist/testing.d.ts",
|
|
27
|
+
"default": "./dist/testing.js"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"zustand": "^5.0.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"typescript": "^5.6.0",
|
|
38
|
+
"vitest": "^3.0.0"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsup",
|
|
42
|
+
"test": "vitest run",
|
|
43
|
+
"typecheck": "tsc --noEmit"
|
|
44
|
+
}
|
|
45
|
+
}
|