@modernrelay/orbit-engine-cosmos 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/README.md +122 -0
- package/dist/index.d.ts +322 -0
- package/dist/index.js +947 -0
- package/dist/index.js.map +1 -0
- package/package.json +38 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ModernRelay
|
|
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,122 @@
|
|
|
1
|
+
# @modernrelay/orbit-engine-cosmos
|
|
2
|
+
|
|
3
|
+
Cosmos engine adapter for Orbit.
|
|
4
|
+
|
|
5
|
+
## Tested engine range
|
|
6
|
+
|
|
7
|
+
This adapter is developed and probed against exactly **`@cosmos.gl/graph@3.3.0`**
|
|
8
|
+
(exact pin, no range). Any pin bump requires re-running the M0 probe suite and
|
|
9
|
+
regenerating the conformance matrix before the new version is considered
|
|
10
|
+
supported.
|
|
11
|
+
|
|
12
|
+
## Capability profile
|
|
13
|
+
|
|
14
|
+
Node-verifiable facts (measured by `pnpm probe:node`, re-checked in CI):
|
|
15
|
+
|
|
16
|
+
| Capability | Value | Basis |
|
|
17
|
+
| --- | --- | --- |
|
|
18
|
+
| `rangeUpdates` | `[]` | Every buffer setter in `dist/index.d.ts` (`setPointPositions`, `setPointColors`, `setLinks`, …) takes a full array; no offset/count variants exist. Whole-buffer re-upload is the only update path. |
|
|
19
|
+
| `postDrawFrames` | `false` | The `config.d.ts` callback inventory has no draw/render-phase hook — only simulation (`onSimulationTick/Start/End/Pause/Unpause`), transition, zoom, drag and pointer callbacks. Post-draw work must be scheduled externally. |
|
|
20
|
+
| `node-import-safe` | `true` | `await import('@cosmos.gl/graph')` succeeds in plain Node (module scope guards `typeof window`), so SSR and test imports never crash. |
|
|
21
|
+
|
|
22
|
+
GPU-probed capabilities (measured 2026-07-21 on Apple M5 Pro, ANGLE Metal, headful
|
|
23
|
+
Chromium; evidence in `docs/evidence/m0/`):
|
|
24
|
+
|
|
25
|
+
| Capability | Result | Finding |
|
|
26
|
+
| --- | --- | --- |
|
|
27
|
+
| `atomic-commit` | ✅ pass | Multi-channel commit (positions+colors+links) applied mid-simulation shows zero mixed frames — a monotonic A→B switch one frame after commit. Visibly atomic commits are real. |
|
|
28
|
+
| `point-picking` | ✅ pass | `onPointClick` delivers the exact index at `spaceToScreenPosition` coords; misses report `undefined`. |
|
|
29
|
+
| `link-picking` | ✅ pass | Native `onLinkClick`/`onLinkMouseOver` deliver correct link indices; ~4 px perpendicular tolerance on a 2 px link. **Flipped in S6:** the adapter now reports `capabilities.linkPicking: true` and wires `onLinkClick`/`onLinkMouseOver`/`onLinkMouseOut` to the host events, backed by this record ([full matrix](../../docs/m0-conformance.md)). |
|
|
30
|
+
| `nan-tombstones` | ✅ pass | NaN'd points escape hit-testing, incident links vanish, `fitView` ignores tombstoned slots. |
|
|
31
|
+
| `native-pinning` | ✅ pass | `setPinnedPoints` excludes points from force integration; `[]`/repeat calls are idempotent. |
|
|
32
|
+
| `tracked-positions` | ⚠️ fail (budget) | Semantics correct (packing order, last-call-replaces), but each `getTrackedPointPositionsArray` read stalls ~18 ms p50 **regardless of k** (256→16384) — a sync pipeline flush, not payload cost. Per-frame tracked readback is not viable on 3.3.0; DOM-label positioning must read at settle/idle cadence. |
|
|
33
|
+
| `quiescence` | ⚠️ fail | The render loop free-runs at display refresh whenever data is present (`stopFrames` fires only on empty scene/destroy) — 91 frames in a 750 ms idle window. Orbit must impose stop-at-rest externally (plan S13-T06). |
|
|
34
|
+
| `context-loss` | ⚠️ fail (by design) | `lost`/`restored` events surface, but no combination of buffer/config replay revives rendering after `restoreContext()` — only a full `Graph` destroy + re-create recovers (match 1.0). This validates the adapter's recreate-on-restore protocol: `CosmosEngine` rebuilds the Graph in the same container and the core replays the scene. |
|
|
35
|
+
|
|
36
|
+
The full, generated matrix (including deferred areas: `mobile`, `resize`,
|
|
37
|
+
`resource-estimates`) lives at [docs/m0-conformance.md](../../docs/m0-conformance.md).
|
|
38
|
+
|
|
39
|
+
## Interaction surface (S6)
|
|
40
|
+
|
|
41
|
+
Coordinate-space facts verified against the 3.3.0 dist (see the
|
|
42
|
+
`CosmosEngine.ts` module header for the exact evidence):
|
|
43
|
+
|
|
44
|
+
- `pointsInPolygon(screenPolygon)` — cosmos' `findPointsInPolygon` takes
|
|
45
|
+
**screen** coordinates (0 to canvas width/height per its d.ts), so the
|
|
46
|
+
host's screen polygon passes through without conversion.
|
|
47
|
+
- `onDragEnd(index, x, y)` reports **space** coordinates: cosmos' drag shader
|
|
48
|
+
pins the dragged point to the mouse's space position every frame, so
|
|
49
|
+
`screenToSpacePosition([event.x, event.y])` at drag end is the point's exact
|
|
50
|
+
final position (O(1), no GPU readback).
|
|
51
|
+
- Native dragging is on by default (`CosmosEngineOptions.enableDrag`
|
|
52
|
+
overrides); setting any `onLink*` callback is what enables cosmos' native
|
|
53
|
+
link hit-testing.
|
|
54
|
+
- `setPinnedIndices` maps to `setPinnedPoints` (full-set replace; `null`/`[]`
|
|
55
|
+
unpin all). Re-applying pins after context-loss recovery is core-owned.
|
|
56
|
+
|
|
57
|
+
## Overlay & toolbar surface (S7)
|
|
58
|
+
|
|
59
|
+
| Capability | Behavior | Basis / degradation |
|
|
60
|
+
| --- | --- | --- |
|
|
61
|
+
| `onFrame` activity clock | The adapter owns a single `requestAnimationFrame` loop: started at mount, stopped on context loss and `destroy()`, resumed after recovery. Each tick is one `onFrame(timeMs)` call (the core skips all work when nothing subscribes); the callback is skipped while `document.hidden` (the clock keeps ticking). | **Documented degradation:** cosmos 3.3.0 exposes no draw/render-phase hook (`postDrawFrames: false`), so this is an *activity* clock, not a post-draw hook — DOM overlays (labels, tooltips) may lag the canvas by one frame. Reported once at mount via the `engine:overlay-activity-clock` info diagnostic. Stop-at-rest quiescence is S13 scope. |
|
|
62
|
+
| `onContextMenu` | cosmos' unified `onContextMenu(index \| undefined, pos, event)` config callback → host `onContextMenu(index \| null, [containerX, containerY])` (container-relative CSS px from the event's `clientX/Y`). Covers desktop right-click and touch long-press (cosmos synthesizes the latter). The native event is `preventDefault`-ed only when the host registered `onContextMenu` (Orbit's core always does — it owns the typed `contextMenu` event). | Only the unified callback is wired: the per-target `onPointContextMenu`/`onLinkContextMenu`/`onBackgroundContextMenu` callbacks fire *additionally* for the same gesture and would double-report. A context menu over a link arrives with `index === undefined` and is reported as background. |
|
|
63
|
+
| `pointsInRect(screenRect)` | `[x0, y0, x1, y1]` → cosmos `findPointsInRect([[left, top], [right, bottom]])` (corners normalized to min/max so any opposite-corner pair works). Lifecycle-guarded: `[]` pre-mount / while lost / after destroy. | Same **screen** coordinate space as `findPointsInPolygon` (0 to canvas width/height per the 3.3.0 d.ts; the dist flips Y assuming ordered corners) — no conversion. |
|
|
64
|
+
| `captureScreenshot()` | Resolves a PNG `Blob` of the current frame, or `null` when unsupported, not ready, lost, or on any capture failure. | The M0 same-tick capture method (`apps/spike/src/instrument.ts`): cosmos renders with a non-preserved drawing buffer, so the WebGL canvas is only readable via a synchronous `drawImage` onto an offscreen 2D canvas inside the same rAF tick — the adapter schedules one rAF and captures inside it. |
|
|
65
|
+
|
|
66
|
+
## Styling channels (S10)
|
|
67
|
+
|
|
68
|
+
Facts verified against the 3.3.0 dist typings (`config.d.ts` / `index.d.ts`):
|
|
69
|
+
|
|
70
|
+
| Capability | Value | Mechanism / basis |
|
|
71
|
+
| --- | --- | --- |
|
|
72
|
+
| `edgeArrows` | `true` | Commit `config.linkArrows` maps to cosmos' `linkDefaultArrows` config key (config.d.ts, default `false`) via `setConfigPartial` — instanced arrowheads toggle atomically within the commit's single `render()`. No per-link `setLinkArrows(boolean[])` buffer is used (Orbit's toggle is scene-wide). |
|
|
73
|
+
| `pointImages` | `true` | cosmos 3.3.0 exposes `setImageData(ImageData[])` + `setPointImageIndices(Float32Array)` (index.d.ts). The adapter maintains a slot→`ImageData` mirror of the atlas: commit upserts transcode each `ImageBitmap` through an offscreen 2D canvas (`drawImage` + `getImageData`), `removeSlots` blanks entries with a 1×1 transparent `ImageData` (slot indices stay stable), and any atlas change re-uploads the FULL array (cosmos has no partial image update — consistent with `rangeUpdates: []`). The mirror is CPU-side, so it survives context loss; post-restore atlas commits re-upload everything to the fresh graph. **Degradation:** where no 2D context exists (jsdom without the canvas package), the image channel no-ops — never throws — and reports the `engine:image-channel-unavailable` warning diagnostic exactly once. |
|
|
74
|
+
| `renderLinks` toggle | config-only | cosmos 3.3.0 has a first-class `renderLinks: boolean` config key (config.d.ts, default `true`), so commit `config.renderLinks` is a pure `setConfigPartial` toggle — **zero buffer setters**, no `linkOpacity` workaround, no link-buffer rebuild. |
|
|
75
|
+
| Theme default colors | config-only | Commit `config.defaultPointColor`/`config.defaultLinkColor` map to cosmos' `pointDefaultColor`/`linkDefaultColor` config keys (CSS color strings accepted natively). |
|
|
76
|
+
|
|
77
|
+
While pre-mount, context-lost, or terminally failed, resources fold into the
|
|
78
|
+
pending-commit per-channel merge like every other channel: image upserts union
|
|
79
|
+
per slot (latest bitmap wins; a later remove drops an earlier pending upsert of
|
|
80
|
+
the same slot), and the latest `pointImageIndex` replaces wholesale.
|
|
81
|
+
|
|
82
|
+
## Cluster force (S12, §16.3 stage 4)
|
|
83
|
+
|
|
84
|
+
| Capability | Value | Mechanism / basis |
|
|
85
|
+
| --- | --- | --- |
|
|
86
|
+
| `clusterForce` | `true` | **Dist investigation (3.3.0, the exact pin):** `dist/index.d.ts` exposes `setPointClusters((number \| undefined)[])`, `setClusterPositions((number \| undefined)[])`, `setPointClusterStrength(Float32Array)` and `getClusterPositions()`; `dist/config.d.ts` exposes the `simulationCluster` coefficient (default `0.1`); `dist/modules/Clusters/index.d.ts` is the GPU module implementing it (centermass FBO + force application pass). The capability is therefore declared honestly, not faked. **Evidence class: Node-verifiable (dist typing scan), like `rangeUpdates`/`postDrawFrames` — the API surface is present and wired, but the VISIBLE force behavior is not yet GPU-probed; a headful probe record is the S13 conformance follow-up (see the GPU evidence policy below).** |
|
|
87
|
+
|
|
88
|
+
Contract mapping applied inside the single atomic commit (staged after the
|
|
89
|
+
roster, before the one `render()`):
|
|
90
|
+
|
|
91
|
+
- `config.cluster.pointClusters` (`Float32Array`, **NaN = unclustered**) →
|
|
92
|
+
`setPointClusters`, converting NaN to cosmos' documented `undefined`
|
|
93
|
+
("does not belong to any cluster and will not be affected by cluster
|
|
94
|
+
forces").
|
|
95
|
+
- `config.cluster.centers` (`[x0,y0,x1,y1,…]`) → `setClusterPositions`; a
|
|
96
|
+
non-finite entry becomes `undefined`, which cosmos documents as "position
|
|
97
|
+
not defined → use centermass positioning instead".
|
|
98
|
+
- `config.cluster.strength` → the **scene-wide** `simulationCluster` config
|
|
99
|
+
key via `setConfigPartial`, not the per-point `setPointClusterStrength`
|
|
100
|
+
buffer (Orbit's strength is scene-wide by contract; the per-point buffer is
|
|
101
|
+
available for a future per-node strength channel).
|
|
102
|
+
- `config.cluster: null` (the D2 explicit clear) → an all-`undefined`
|
|
103
|
+
membership array of the **current roster length** plus empty cluster
|
|
104
|
+
positions, so the mapping length never lags the roster (I2).
|
|
105
|
+
|
|
106
|
+
## Camera surface (S11)
|
|
107
|
+
|
|
108
|
+
| Capability | Behavior | Basis / formula |
|
|
109
|
+
| --- | --- | --- |
|
|
110
|
+
| `setViewport({x, y, zoom})` — **real pan** | Centers space point `(x, y)` at exactly the requested (or current) zoom in one call; a follow-up `getViewport()` returns the same `{x, y, zoom}` (modulo cosmos' d3 `scaleExtent` clamp). A missing `x` or `y` is filled from the current viewport; zoom-only calls keep the `setZoomLevel` path (d3 `scaleTo` preserves the center). Instant unless `durationMs` is given. | **The former zoom-only limitation is LIFTED.** cosmos 3.3 has no pan-to API, but `setZoomTransformByPointPositions(positions, duration, scale, padding)` is an exact center+zoom when `scale` is explicit — verified in the 3.3.0 dist (`zoomInstance.getTransform`): `store.scaleX/scaleY` are linear slope-±1 space→screen maps, a single-point bbox is widened ±0.5 *symmetrically* (center preserved), and an explicit `scale` bypasses the fit math and `padding` entirely, yielding `translate(w/2 − scaleX(x)·k, h/2 − scaleY(y)·k).scale(k)` with `k = scale`. The adapter calls `setZoomTransformByPointPositions(Float32Array.of(x, y), durationMs ?? 0, zoom ?? getZoomLevel())`. |
|
|
111
|
+
| §13.1 viewport restore | Context-loss recovery now restores the full camera: the core replays the last stored `{x, y, zoom}` through `setViewport` after the scene commit, and the pan lands (previously only `zoom` was honored). | Supersedes the "viewport restore is zoom-only" caveat recorded in ADR-004 / spec §13.1 for cosmos 3.3. |
|
|
112
|
+
|
|
113
|
+
## GPU evidence policy
|
|
114
|
+
|
|
115
|
+
GPU probe records are produced locally by running the probe suite headful on a
|
|
116
|
+
real GPU (`pnpm probe`, which sets `PROBE_HEADFUL=1`; set `PROBE_GPU` to
|
|
117
|
+
describe the hardware). CI never produces GPU evidence: it re-runs only the
|
|
118
|
+
Node-safe probes (`pnpm probe:node`) and validates the committed matrix
|
|
119
|
+
(`pnpm conformance:check`). Results rendered through SwiftShader or any other
|
|
120
|
+
software rasterizer are not accepted as conformance evidence — headless runs
|
|
121
|
+
are permitted solely as a harness smoke test and their records must not be
|
|
122
|
+
committed.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { GraphEngine, EngineCapabilities, EngineHostEvents, EngineCommit, FitViewOptions } from '@modernrelay/orbit-core/engine';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CosmosEngine — GraphEngine adapter over @cosmos.gl/graph (spec §13).
|
|
5
|
+
*
|
|
6
|
+
* Node-safe module: cosmos is loaded lazily via `await import()` inside
|
|
7
|
+
* `mount()` (§18); module scope carries only type-only imports (erased at
|
|
8
|
+
* runtime) and never touches the DOM.
|
|
9
|
+
*
|
|
10
|
+
* Color scale note: the EngineCommit contract carries RGBA floats in [0,1],
|
|
11
|
+
* which is exactly cosmos' native scale (verified against the 3.3.0 dist:
|
|
12
|
+
* hex config colors are parsed via /255 into 0–1 floats and setPointColors
|
|
13
|
+
* input is fed to GPU buffers untouched) — so color buffers pass through.
|
|
14
|
+
*
|
|
15
|
+
* Interaction notes (verified against the 3.3.0 dist):
|
|
16
|
+
* - Link picking is native: setting any onLink* config callback flips
|
|
17
|
+
* cosmos' internal `isLinkHoveringEnabled` on, so wiring the callbacks in
|
|
18
|
+
* `buildInitialConfig` is sufficient (no extra config flag exists).
|
|
19
|
+
* - `findPointsInPolygon` takes SCREEN coordinates ("from 0 to the
|
|
20
|
+
* width/height of the canvas" per dist/index.d.ts), so `pointsInPolygon`
|
|
21
|
+
* passes the host's screen polygon through without conversion.
|
|
22
|
+
* - `findPointsInRect` uses the SAME screen space and expects ordered
|
|
23
|
+
* corners `[[left, top], [right, bottom]]` (the dist flips Y assuming that
|
|
24
|
+
* ordering), so `pointsInRect` normalizes the host's [x0,y0,x1,y1] rect to
|
|
25
|
+
* min/max corners and passes it through without conversion.
|
|
26
|
+
* - Camera pan (S11 — the zoom-only limitation is LIFTED): cosmos 3.3 has no
|
|
27
|
+
* pan-to API, but `setZoomTransformByPointPositions(positions, duration,
|
|
28
|
+
* scale, padding)` (dist/index.d.ts) is an exact center+zoom when `scale`
|
|
29
|
+
* is explicit. Derivation from the dist (`zoomInstance.getTransform`):
|
|
30
|
+
* space→screen-at-k=1 goes through `store.scaleX/scaleY`, which are LINEAR
|
|
31
|
+
* with slope ±1 (scaleX(x) = x + (w−S)/2; scaleY(y) = (S−y) + (h−S)/2,
|
|
32
|
+
* S = adjustedSpaceSize, w/h = canvas size); a single-point bbox is widened
|
|
33
|
+
* ±0.5 SYMMETRICALLY (center preserved); an explicit `scale` bypasses the
|
|
34
|
+
* fit math and `padding` entirely — k = clamp(scale, d3 scaleExtent) and
|
|
35
|
+
* the result is translate(w/2 − scaleX(x)·k, h/2 − scaleY(y)·k).scale(k),
|
|
36
|
+
* i.e. space point (x, y) lands exactly at the screen center with
|
|
37
|
+
* eventTransform.k = scale. Since `getZoomLevel()` returns eventTransform.k
|
|
38
|
+
* and `getViewport()` reads screenToSpacePosition(screen center),
|
|
39
|
+
* `setViewport({x, y, zoom})` → setZoomTransformByPointPositions(
|
|
40
|
+
* Float32Array.of(x, y), durationMs ?? 0, zoom ?? getZoomLevel()) makes a
|
|
41
|
+
* follow-up `getViewport()` return exactly {x, y, zoom} (modulo the
|
|
42
|
+
* scaleExtent clamp). This also completes §13.1 recovery: the core replays
|
|
43
|
+
* the full stored {x, y, zoom} through setViewport, so viewport restore is
|
|
44
|
+
* no longer zoom-only.
|
|
45
|
+
* - Context menu: cosmos fires the unified config `onContextMenu(index |
|
|
46
|
+
* undefined, pos, event)` exactly ONCE per gesture — desktop right-click
|
|
47
|
+
* (dist `onContextMenu(e)`) and touch/pen long-press (dist long-press timer
|
|
48
|
+
* → `fireContextMenu`) both route through it — while the per-target
|
|
49
|
+
* `onPointContextMenu`/`onLinkContextMenu`/`onBackgroundContextMenu`
|
|
50
|
+
* callbacks fire ADDITIONALLY for the same gesture. Wiring only the unified
|
|
51
|
+
* callback therefore avoids a double-report (same shape as the
|
|
52
|
+
* onClick/onBackgroundClick dedupe). A context menu over a LINK arrives
|
|
53
|
+
* with index undefined and is reported as background (the host event
|
|
54
|
+
* carries node-or-background only).
|
|
55
|
+
*
|
|
56
|
+
* Overlay/activity clock (M0 evidence, docs/m0-conformance.md): cosmos exposes
|
|
57
|
+
* no draw/render-phase hook (`postDrawFrames: false`), so the adapter owns a
|
|
58
|
+
* single requestAnimationFrame loop that reports `onFrame(timeMs)` to the host
|
|
59
|
+
* — an ACTIVITY clock, not a post-draw hook: overlays may lag the canvas by
|
|
60
|
+
* one sample. That degradation is reported once at mount via the
|
|
61
|
+
* `engine:overlay-activity-clock` info diagnostic. The loop is started at
|
|
62
|
+
* mount, stopped on context loss/destroy, resumed on recovery, and skips the
|
|
63
|
+
* callback while `document.hidden` (stop-at-rest quiescence is S13 scope).
|
|
64
|
+
*
|
|
65
|
+
* Screenshots use the M0 same-tick capture method (apps/spike/src/instrument.ts):
|
|
66
|
+
* cosmos renders with preserveDrawingBuffer:false, so the WebGL buffer is only
|
|
67
|
+
* readable via a synchronous drawImage inside the same rAF tick it was drawn —
|
|
68
|
+
* `captureScreenshot` schedules one rAF, draws the cosmos canvas onto an
|
|
69
|
+
* offscreen 2D canvas inside that tick, and resolves the Blob (null on any
|
|
70
|
+
* failure or unusable lifecycle state).
|
|
71
|
+
* - The D3 drag events carry NO point index (the drag subject is a bare
|
|
72
|
+
* `{x, y}`); cosmos assigns `store.draggingPointIndex` immediately before
|
|
73
|
+
* invoking `onDragStart`, so the adapter reads it there (see
|
|
74
|
+
* `readDraggingIndex` for the public-callback fallback).
|
|
75
|
+
* - During a drag, cosmos' drag shader hard-pins the dragged point's
|
|
76
|
+
* position texture entry to the mouse's SPACE position every frame
|
|
77
|
+
* (dist `drag()`: `pointPosition.rg = mousePos` where `mousePos` =
|
|
78
|
+
* screenToSpace(pointer)). So the point's final space position equals
|
|
79
|
+
* `screenToSpacePosition([event.x, event.y])` at drag end — an O(1) CPU
|
|
80
|
+
* transform, chosen over `getPointPositions()` (full GPU readback).
|
|
81
|
+
*/
|
|
82
|
+
|
|
83
|
+
/** Derived from the contract to avoid importing the core root barrel. */
|
|
84
|
+
type ViewportState = NonNullable<ReturnType<GraphEngine['getViewport']>>;
|
|
85
|
+
interface CosmosEngineOptions {
|
|
86
|
+
/** Simulation space size passed to cosmos (cosmos default: 4096). */
|
|
87
|
+
spaceSize?: number;
|
|
88
|
+
/** Ring radius for seeding unknown (NaN) positions. Default: spaceSize/4. */
|
|
89
|
+
seedRadius?: number;
|
|
90
|
+
/** Whether cosmos auto-fits the view on init. Default: false (the core drives the camera). */
|
|
91
|
+
fitViewOnInit?: boolean;
|
|
92
|
+
/** Native point dragging (cosmos `enableDrag`). Default: true. */
|
|
93
|
+
enableDrag?: boolean;
|
|
94
|
+
/** Escape hatch: shallow-merged last into the cosmos constructor config. */
|
|
95
|
+
initialConfig?: Record<string, unknown>;
|
|
96
|
+
/**
|
|
97
|
+
* Visible-time budget (ms) for the browser to restore a lost WebGL context
|
|
98
|
+
* before an `engine:context-restore-deadline` diagnostic is emitted. The
|
|
99
|
+
* deadline is observability only — a later restore still recovers.
|
|
100
|
+
* Default: 10_000.
|
|
101
|
+
*/
|
|
102
|
+
restoreDeadlineMs?: number;
|
|
103
|
+
}
|
|
104
|
+
declare class CosmosEngine implements GraphEngine {
|
|
105
|
+
readonly capabilities: EngineCapabilities;
|
|
106
|
+
private readonly options;
|
|
107
|
+
private graph;
|
|
108
|
+
private innerDiv;
|
|
109
|
+
private events;
|
|
110
|
+
/**
|
|
111
|
+
* Pre-mount/lost-context commits collapse per channel and are applied as one
|
|
112
|
+
* atomic commit once a usable graph exists.
|
|
113
|
+
*/
|
|
114
|
+
private pendingCommit;
|
|
115
|
+
private applied;
|
|
116
|
+
private destroyed;
|
|
117
|
+
private mounting;
|
|
118
|
+
/** Constructed graph whose `ready` promise has not settled yet. */
|
|
119
|
+
private graphAwaitingReady;
|
|
120
|
+
/** Guards every graph teardown, including async init/recovery races. */
|
|
121
|
+
private readonly destroyedGraphs;
|
|
122
|
+
/** Sticky override from EngineConfigUpdate.seedRadius. */
|
|
123
|
+
private seedRadiusOverride;
|
|
124
|
+
/** Point count of the last structure-bearing commit — the roster length a
|
|
125
|
+
* `cluster: null` clear must write an all-unclustered array for (§16.3). */
|
|
126
|
+
private lastPointCount;
|
|
127
|
+
/**
|
|
128
|
+
* cosmos fires `onClick` (index undefined) AND `onBackgroundClick` for the
|
|
129
|
+
* same background click; remembering the MouseEvent dedupes the null emit.
|
|
130
|
+
*/
|
|
131
|
+
private lastNullClickEvent;
|
|
132
|
+
/** Point latched at cosmos onDragStart; cleared when the gesture ends. */
|
|
133
|
+
private dragIndex;
|
|
134
|
+
/** Last hover reported by cosmos — fallback dragged-point source. */
|
|
135
|
+
private lastHoverIndex;
|
|
136
|
+
/** rAF id of the pending activity-clock tick; null = clock not running. */
|
|
137
|
+
private frameHandle;
|
|
138
|
+
/** Window driving the clock, cached so stop works after the div detaches. */
|
|
139
|
+
private frameWindow;
|
|
140
|
+
/** cosmos' canvas (queried post-mount) carrying the webglcontext* listeners. */
|
|
141
|
+
private canvas;
|
|
142
|
+
private contextLost;
|
|
143
|
+
/** Terminal: GL reinitialization failed; commits stash inertly forever. */
|
|
144
|
+
private failed;
|
|
145
|
+
private deadline;
|
|
146
|
+
/** Graph constructor cached at mount so recovery never re-imports cosmos. */
|
|
147
|
+
private cosmosCtor;
|
|
148
|
+
/**
|
|
149
|
+
* slot → ImageData mirror of the cosmos image atlas. ImageData is CPU-side,
|
|
150
|
+
* so the mirror survives context loss — any post-restore atlas commit
|
|
151
|
+
* re-uploads the FULL array to the fresh graph. `null` = removed (blank).
|
|
152
|
+
*/
|
|
153
|
+
private imageSlots;
|
|
154
|
+
/** Lazily created 1×1 transparent entry filling removed/hole slots. */
|
|
155
|
+
private blankImage;
|
|
156
|
+
/** One-shot guard for the `engine:image-channel-unavailable` diagnostic. */
|
|
157
|
+
private imageChannelUnavailable;
|
|
158
|
+
constructor(options?: CosmosEngineOptions);
|
|
159
|
+
mount(container: HTMLElement, events: EngineHostEvents): Promise<void>;
|
|
160
|
+
commit(update: EngineCommit): void;
|
|
161
|
+
appliedRevision(): number | null;
|
|
162
|
+
fitView(opts?: FitViewOptions): void;
|
|
163
|
+
zoom(factor: number, durationMs?: number): void;
|
|
164
|
+
/**
|
|
165
|
+
* REAL pan (the former zoom-only limitation is lifted — see module header):
|
|
166
|
+
* when a target center is provided, one `setZoomTransformByPointPositions`
|
|
167
|
+
* call centers space point (x, y) at EXACTLY the requested (or current)
|
|
168
|
+
* zoom — `setViewport(p)` then `getViewport()` returns p, modulo cosmos'
|
|
169
|
+
* d3 scaleExtent clamp. A missing x or y is filled from the current
|
|
170
|
+
* viewport; zoom-only calls keep the `setZoomLevel` path (d3's scaleTo
|
|
171
|
+
* preserves the current center). Instant unless `durationMs` is given —
|
|
172
|
+
* cosmos' own default duration is 250 ms, which would animate context-
|
|
173
|
+
* recovery replays.
|
|
174
|
+
*/
|
|
175
|
+
setViewport(v: Partial<ViewportState>, opts?: {
|
|
176
|
+
durationMs?: number;
|
|
177
|
+
}): void;
|
|
178
|
+
getViewport(): ViewportState | null;
|
|
179
|
+
zoomToIndex(index: number, durationMs?: number): void;
|
|
180
|
+
start(alpha?: number): void;
|
|
181
|
+
pause(): void;
|
|
182
|
+
setSelectedIndices(indices: readonly number[] | null): void;
|
|
183
|
+
setFocusedIndex(index: number | null): void;
|
|
184
|
+
pointsInPolygon(screenPolygon: readonly [number, number][]): number[];
|
|
185
|
+
pointsInRect(screenRect: readonly [number, number, number, number]): number[];
|
|
186
|
+
neighborIndices(index: number): number[];
|
|
187
|
+
screenToSpace(p: readonly [number, number]): [number, number] | null;
|
|
188
|
+
spaceToScreen(p: readonly [number, number]): [number, number] | null;
|
|
189
|
+
setPinnedIndices(indices: readonly number[] | null): void;
|
|
190
|
+
getPositions(): Float32Array | null;
|
|
191
|
+
/**
|
|
192
|
+
* Captures the cosmos canvas via the M0 same-tick method (see module
|
|
193
|
+
* header): one rAF is scheduled and, inside that tick, the WebGL canvas is
|
|
194
|
+
* drawn synchronously onto an offscreen 2D canvas (cosmos renders with
|
|
195
|
+
* preserveDrawingBuffer:false, so the buffer is only readable same-tick).
|
|
196
|
+
* Resolves null on any failure or unusable lifecycle state (pre-mount,
|
|
197
|
+
* context lost/failed, destroyed, no 2D context, toBlob failure).
|
|
198
|
+
*/
|
|
199
|
+
captureScreenshot(): Promise<Blob | null>;
|
|
200
|
+
destroy(): void;
|
|
201
|
+
/**
|
|
202
|
+
* One rAF tick of the activity clock. Reschedules FIRST so a throwing host
|
|
203
|
+
* callback can never kill the clock; skips the callback (but keeps ticking)
|
|
204
|
+
* while the document is hidden. Deliberately cheap: a single callback
|
|
205
|
+
* invocation — the core skips all work when nothing subscribes.
|
|
206
|
+
*/
|
|
207
|
+
private readonly frameTick;
|
|
208
|
+
private startFrameLoop;
|
|
209
|
+
private stopFrameLoop;
|
|
210
|
+
/** The graph, unless it is unusable (context lost / terminally failed). */
|
|
211
|
+
private get activeGraph();
|
|
212
|
+
/**
|
|
213
|
+
* cosmos owns its canvas; we can only wire webglcontext* listeners after
|
|
214
|
+
* init by querying it. A missing canvas downgrades to an info diagnostic —
|
|
215
|
+
* the engine keeps working, just without context-loss recovery.
|
|
216
|
+
*/
|
|
217
|
+
private attachContextListeners;
|
|
218
|
+
private detachContextListeners;
|
|
219
|
+
/** DOM listener: nothing may throw out of it. */
|
|
220
|
+
private readonly handleContextLost;
|
|
221
|
+
/** DOM listener: nothing may throw out of it (async work is caught below). */
|
|
222
|
+
private readonly handleContextRestored;
|
|
223
|
+
/**
|
|
224
|
+
* cosmos cannot reuse a restored context (its GPU resources are gone), so
|
|
225
|
+
* recovery = tear down the old Graph and build a fresh one in the same div,
|
|
226
|
+
* flush the stashed commit, and only then report `restored` — the core
|
|
227
|
+
* re-commits the full scene in response.
|
|
228
|
+
*/
|
|
229
|
+
private reinitializeAfterRestore;
|
|
230
|
+
private startRestoreDeadline;
|
|
231
|
+
/**
|
|
232
|
+
* Coalesces partial commits without discarding independent channels. The
|
|
233
|
+
* newest call supplies the visible revision; structure is one atomic
|
|
234
|
+
* channel, buffers/config merge per field, and restart persists until a
|
|
235
|
+
* later explicit directive (including `false`) replaces it.
|
|
236
|
+
*/
|
|
237
|
+
private queueCommit;
|
|
238
|
+
/** Invokes a graph's destructor at most once, even across async races. */
|
|
239
|
+
private destroyGraphOnce;
|
|
240
|
+
private get spaceSize();
|
|
241
|
+
private get seedRadius();
|
|
242
|
+
private buildInitialConfig;
|
|
243
|
+
/**
|
|
244
|
+
* Maps cosmos' unified context-menu callback to the host event: index
|
|
245
|
+
* undefined → null (background), MouseEvent client coords → container-
|
|
246
|
+
* relative CSS px (the inner div fills the host container exactly). The
|
|
247
|
+
* native event is preventDefault-ed so the browser menu never opens — but
|
|
248
|
+
* only when the host actually registered onContextMenu (it always does in
|
|
249
|
+
* Orbit: the core owns the typed 'contextMenu' event channel). cosmos'
|
|
250
|
+
* desktop `contextmenu` handler already prevents the event itself; repeating
|
|
251
|
+
* it is an idempotent no-op that also covers the touch long-press path
|
|
252
|
+
* (where cosmos forwards the originating pointerdown event instead).
|
|
253
|
+
*/
|
|
254
|
+
private handleContextMenu;
|
|
255
|
+
/** Emits onPointClick(null) once per originating click event. */
|
|
256
|
+
private emitNullPointClick;
|
|
257
|
+
/**
|
|
258
|
+
* The D3 drag event carries no point index; cosmos assigns
|
|
259
|
+
* `store.draggingPointIndex` right before invoking onDragStart (see module
|
|
260
|
+
* header). The public onPointMouseOver stream is the fallback — cosmos only
|
|
261
|
+
* starts a drag while a point is hovered.
|
|
262
|
+
*/
|
|
263
|
+
private readDraggingIndex;
|
|
264
|
+
private handleDragStart;
|
|
265
|
+
/**
|
|
266
|
+
* Reports the dragged point's final SPACE position. cosmos' drag shader
|
|
267
|
+
* pins the point to the mouse's space position every frame, so converting
|
|
268
|
+
* the event's screen coords is exact and O(1) (see module header).
|
|
269
|
+
*/
|
|
270
|
+
private handleDragEnd;
|
|
271
|
+
private emitViewportChange;
|
|
272
|
+
/**
|
|
273
|
+
* One visibly atomic update (§13): all channels and config are staged, then
|
|
274
|
+
* exactly one render() draws them; restart reheats after the render.
|
|
275
|
+
*/
|
|
276
|
+
private applyCommit;
|
|
277
|
+
/**
|
|
278
|
+
* Applies the §16.3 stage-4 cluster force (capability `clusterForce`).
|
|
279
|
+
*
|
|
280
|
+
* Contract mapping, verified against the 3.3.0 dist (`index.d.ts`):
|
|
281
|
+
* - `pointClusters` (Float32Array, NaN = unclustered) →
|
|
282
|
+
* `setPointClusters((number | undefined)[])`, where cosmos' documented
|
|
283
|
+
* "does not belong to any cluster" value is `undefined`;
|
|
284
|
+
* - `centers` (Float32Array, `[x0,y0,x1,y1,…]`) →
|
|
285
|
+
* `setClusterPositions((number | undefined)[])`; a non-finite entry means
|
|
286
|
+
* "no position" and cosmos falls back to that cluster's centermass;
|
|
287
|
+
* - `null` clears: an all-`undefined` array of the CURRENT roster length
|
|
288
|
+
* (length must track the roster) plus empty cluster positions.
|
|
289
|
+
* `strength` is the scene-wide `simulationCluster` config coefficient
|
|
290
|
+
* applied in the config block, not the per-point
|
|
291
|
+
* `setPointClusterStrength` buffer (Orbit's strength is scene-wide).
|
|
292
|
+
*/
|
|
293
|
+
private applyClusterForce;
|
|
294
|
+
/**
|
|
295
|
+
* Applies the §8 image-atlas channel: upserts convert ImageBitmap →
|
|
296
|
+
* ImageData through an offscreen 2D canvas into the slot mirror, removals
|
|
297
|
+
* blank their slot, and any atlas change re-uploads the FULL ImageData
|
|
298
|
+
* array (cosmos' setImageData is whole-array only, matching
|
|
299
|
+
* `rangeUpdates: []`). Environments without a usable 2D context (jsdom)
|
|
300
|
+
* no-op the channel and report `engine:image-channel-unavailable` once —
|
|
301
|
+
* never throw.
|
|
302
|
+
*/
|
|
303
|
+
private applyResources;
|
|
304
|
+
/**
|
|
305
|
+
* Reads an ImageData of the given size off an offscreen 2D canvas, drawing
|
|
306
|
+
* `bitmap` onto it first when provided (ImageBitmap → ImageData transcode;
|
|
307
|
+
* without a bitmap: a transparent blank). Returns null — never throws —
|
|
308
|
+
* where no 2D context exists (jsdom without the canvas package).
|
|
309
|
+
*/
|
|
310
|
+
private canvasImageData;
|
|
311
|
+
/** Documented degradation, reported at most once per engine instance. */
|
|
312
|
+
private reportImageChannelUnavailable;
|
|
313
|
+
/**
|
|
314
|
+
* Replaces NaN pairs (= "no known position", §7.3) with random points on a
|
|
315
|
+
* ring of radius seedRadius around the space center. cosmos treats NaN
|
|
316
|
+
* positions as *absent* points, so they must never reach setPointPositions.
|
|
317
|
+
* Known positions pass through verbatim (same array when nothing to seed).
|
|
318
|
+
*/
|
|
319
|
+
private withSeededPositions;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export { CosmosEngine, type CosmosEngineOptions };
|