@modernrelay/orbit-engine-cosmos 0.13.6 → 0.15.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/README.md +33 -34
- package/dist/index.d.ts +83 -83
- package/dist/index.js +155 -56
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -5,10 +5,12 @@ Cosmos engine adapter for Orbit.
|
|
|
5
5
|
## Tested engine range
|
|
6
6
|
|
|
7
7
|
This adapter is developed and probed against exactly **`@cosmos.gl/graph@3.4.0`**
|
|
8
|
-
(exact pin, no range).
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
(exact pin, no range). In the repository, `pnpm cosmos:pin-check` verifies that
|
|
9
|
+
the adapter and `apps/spike` declare the same exact version. Any pin bump must
|
|
10
|
+
update both manifests, pass that check and `pnpm probe:node`, then be exercised
|
|
11
|
+
locally with the headful `pnpm probe` suite on a real GPU before the new version
|
|
12
|
+
is considered supported. The 3.3.0 → 3.4.0 bump was re-probed on 2026-08-10;
|
|
13
|
+
upstream on-demand rendering made the quiescence probe pass.
|
|
12
14
|
|
|
13
15
|
## Capability profile
|
|
14
16
|
|
|
@@ -18,29 +20,26 @@ Node-verifiable facts (measured by `pnpm probe:node`, re-checked in CI):
|
|
|
18
20
|
| --- | --- | --- |
|
|
19
21
|
| `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. |
|
|
20
22
|
| `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. Since v0.13 this is a REAL `EngineCapabilities` field, not just prose. |
|
|
21
|
-
| `idleFrames` | `'stops'` | cosmos ≥3.4 on-demand rendering idle-stops the engine loop, and the adapter's gated activity clock stops with it — measured 0 idle frames and 0 rAF registrations per 750 ms at rest
|
|
23
|
+
| `idleFrames` | `'stops'` | cosmos ≥3.4 on-demand rendering idle-stops the engine loop, and the adapter's gated activity clock stops with it — measured 0 idle frames and 0 rAF registrations per 750 ms at rest by the quiescence probe below. |
|
|
22
24
|
| `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. |
|
|
23
25
|
|
|
24
|
-
GPU-probed capabilities (measured 2026-
|
|
25
|
-
Chromium
|
|
26
|
+
GPU-probed capabilities (re-measured 2026-08-10 on Apple M5 Pro, ANGLE Metal, headful
|
|
27
|
+
Chromium):
|
|
26
28
|
|
|
27
29
|
| Capability | Result | Finding |
|
|
28
30
|
| --- | --- | --- |
|
|
29
31
|
| `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. |
|
|
30
32
|
| `point-picking` | ✅ pass | `onPointClick` delivers the exact index at `spaceToScreenPosition` coords; misses report `undefined`. |
|
|
31
|
-
| `link-picking` | ✅ pass | Native `onLinkClick`/`onLinkMouseOver` deliver correct link indices; ~4 px perpendicular tolerance on a 2 px link. **Flipped
|
|
33
|
+
| `link-picking` | ✅ pass | Native `onLinkClick`/`onLinkMouseOver` deliver correct link indices; ~4 px perpendicular tolerance on a 2 px link. **Flipped:** the adapter now reports `capabilities.linkPicking: true` and wires `onLinkClick`/`onLinkMouseOver`/`onLinkMouseOut` to the host events, backed by the headful probe. |
|
|
32
34
|
| `nan-tombstones` | ✅ pass | NaN'd points escape hit-testing, incident links vanish, `fitView` ignores tombstoned slots. |
|
|
33
35
|
| `native-pinning` | ✅ pass | `setPinnedPoints` excludes points from force integration; `[]`/repeat calls are idempotent. |
|
|
34
|
-
| `tracked-positions` | ⚠️ fail (budget) | Semantics correct (packing order, last-call-replaces), but each `getTrackedPointPositionsArray` read stalls ~8.7 ms p50 on 3.4.0 (was ~18 ms on 3.3) **regardless of k** — a sync pipeline flush, not payload cost. Per-frame tracked readback stays non-viable; DOM-label positioning reads at settle/idle cadence
|
|
36
|
+
| `tracked-positions` | ⚠️ fail (budget) | Semantics correct (packing order, last-call-replaces), but each `getTrackedPointPositionsArray` read stalls ~8.7 ms p50 on 3.4.0 (was ~18 ms on 3.3) **regardless of k** — a sync pipeline flush, not payload cost. Per-frame tracked readback stays non-viable; DOM-label positioning reads at settle/idle cadence. |
|
|
35
37
|
| `quiescence` | ✅ pass (3.4.0) | On-demand rendering: 0 idle frames AND 0 rAF registrations per 750 ms at rest (was 91 on 3.3.0). Settle lands ~5 s after `onSimulationEnd` (trailing transitions). Picking stays index-truthful at rest, drag release renders its final frame, and a `render()` + same-tick capture reads valid pixels (the stale-wake micro-cases). |
|
|
36
|
-
| `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
|
|
38
|
+
| `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. This validates the adapter's recreate-on-restore protocol: `CosmosEngine` rebuilds the Graph in the same container and the core replays the scene. |
|
|
37
39
|
|
|
38
|
-
|
|
39
|
-
`resource-estimates`) lives at [docs/m0-conformance.md](../../docs/m0-conformance.md).
|
|
40
|
+
## Interaction surface
|
|
40
41
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
Coordinate-space facts verified against the 3.3.0 dist (see the
|
|
42
|
+
Coordinate-space facts verified against the exact-pin 3.4.0 dist (see the
|
|
44
43
|
`CosmosEngine.ts` module header for the exact evidence):
|
|
45
44
|
|
|
46
45
|
- `pointsInPolygon(screenPolygon)` — cosmos' `findPointsInPolygon` takes
|
|
@@ -56,24 +55,24 @@ Coordinate-space facts verified against the 3.3.0 dist (see the
|
|
|
56
55
|
- `setPinnedIndices` maps to `setPinnedPoints` (full-set replace; `null`/`[]`
|
|
57
56
|
unpin all). Re-applying pins after context-loss recovery is core-owned.
|
|
58
57
|
|
|
59
|
-
## Overlay & toolbar surface
|
|
58
|
+
## Overlay & toolbar surface
|
|
60
59
|
|
|
61
60
|
| Capability | Behavior | Basis / degradation |
|
|
62
61
|
| --- | --- | --- |
|
|
63
|
-
| `onFrame` GATED activity clock | The adapter owns the single `requestAnimationFrame` loop, GATED to match cosmos ≥3.4's on-demand rendering
|
|
62
|
+
| `onFrame` GATED activity clock | The adapter owns the single `requestAnimationFrame` loop, GATED to match cosmos ≥3.4's on-demand rendering: it free-runs only under a held run reason (sim hot, drag gesture, cosmos transition) and otherwise burns a one-shot tick budget that every visual write re-arms (commits, highlight/focus/pin setters, camera moves, hover; animated camera moves self-sustain via per-step `onZoom` bursts). Every reason release grants ONE trailing tick (the final GPU write lands after the current draw pass). Stopped on context loss/`destroy()`, re-armed on recovery; the callback is skipped while `document.hidden`. **At rest: zero rAF registrations** (`idleFrames: 'stops'`). | **Documented degradation:** cosmos still exposes no draw/render-phase hook (`postDrawFrames: false`), so this is an *activity* clock, not a post-draw hook — DOM overlays may lag the canvas by one frame. Reported once at mount via `engine:overlay-activity-clock`; `showFPSMonitor` via the config escape hatch keeps cosmos rendering and is diagnosed once (`engine:fps-monitor-defeats-quiescence`). |
|
|
64
63
|
| `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. |
|
|
65
|
-
| `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
|
|
66
|
-
| `captureScreenshot()` | Resolves a PNG `Blob` of the current frame, or `null` when unsupported, not ready, lost, or on any capture failure. | The
|
|
64
|
+
| `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 exact-pin d.ts; the dist flips Y assuming ordered corners) — no conversion. |
|
|
65
|
+
| `captureScreenshot()` | Resolves a PNG `Blob` of the current frame, or `null` when unsupported, not ready, lost, or on any capture failure. | The 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. Under on-demand rendering an idle scene draws nothing (blank buffer), so the adapter first calls `graph.render()` — a visual mutator that schedules the frame the capture rAF then samples. |
|
|
67
66
|
|
|
68
|
-
## Styling channels
|
|
67
|
+
## Styling channels
|
|
69
68
|
|
|
70
|
-
Facts verified against the 3.
|
|
69
|
+
Facts verified against the exact-pin 3.4.0 dist typings (`config.d.ts` / `index.d.ts`):
|
|
71
70
|
|
|
72
71
|
| Capability | Value | Mechanism / basis |
|
|
73
72
|
| --- | --- | --- |
|
|
74
73
|
| `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). |
|
|
75
|
-
| `pointImages` | `true` | cosmos 3.
|
|
76
|
-
| `renderLinks` toggle | config-only | cosmos 3.
|
|
74
|
+
| `pointImages` | `true` | cosmos 3.4.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. |
|
|
75
|
+
| `renderLinks` toggle | config-only | cosmos 3.4.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. |
|
|
77
76
|
| Theme default colors | config-only | Commit `config.defaultPointColor`/`config.defaultLinkColor` map to cosmos' `pointDefaultColor`/`linkDefaultColor` config keys (CSS color strings accepted natively). |
|
|
78
77
|
|
|
79
78
|
While pre-mount, context-lost, or terminally failed, resources fold into the
|
|
@@ -81,11 +80,11 @@ pending-commit per-channel merge like every other channel: image upserts union
|
|
|
81
80
|
per slot (latest bitmap wins; a later remove drops an earlier pending upsert of
|
|
82
81
|
the same slot), and the latest `pointImageIndex` replaces wholesale.
|
|
83
82
|
|
|
84
|
-
## Cluster force
|
|
83
|
+
## Cluster force
|
|
85
84
|
|
|
86
85
|
| Capability | Value | Mechanism / basis |
|
|
87
86
|
| --- | --- | --- |
|
|
88
|
-
| `clusterForce` | `true` | **Dist investigation (3.
|
|
87
|
+
| `clusterForce` | `true` | **Dist investigation (3.4.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 still requires a headful probe (see the GPU evidence policy below).** |
|
|
89
88
|
|
|
90
89
|
Contract mapping applied inside the single atomic commit (staged after the
|
|
91
90
|
roster, before the one `render()`):
|
|
@@ -105,20 +104,20 @@ roster, before the one `render()`):
|
|
|
105
104
|
membership array of the **current roster length** plus empty cluster
|
|
106
105
|
positions, so the mapping length never lags the roster (I2).
|
|
107
106
|
|
|
108
|
-
## Camera surface
|
|
107
|
+
## Camera surface
|
|
109
108
|
|
|
110
109
|
| Capability | Behavior | Basis / formula |
|
|
111
110
|
| --- | --- | --- |
|
|
112
|
-
| `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.**
|
|
113
|
-
|
|
|
111
|
+
| `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.** The exact-pin API has no pan-to method, but `setZoomTransformByPointPositions(positions, duration, scale, padding)` is an exact center+zoom when `scale` is explicit — verified in the 3.4.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())`. |
|
|
112
|
+
| 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 earlier zoom-only restore behavior. |
|
|
114
113
|
|
|
115
114
|
## GPU evidence policy
|
|
116
115
|
|
|
117
116
|
GPU probe records are produced locally by running the probe suite headful on a
|
|
118
117
|
real GPU (`pnpm probe`, which sets `PROBE_HEADFUL=1`; set `PROBE_GPU` to
|
|
119
|
-
describe the hardware). CI
|
|
120
|
-
|
|
121
|
-
(`pnpm
|
|
122
|
-
software rasterizer are not accepted as
|
|
123
|
-
are permitted solely as a harness smoke test and their records
|
|
124
|
-
committed.
|
|
118
|
+
describe the hardware). CI verifies the exact dependency pins with
|
|
119
|
+
`pnpm cosmos:pin-check` and re-runs the Node-safe probes
|
|
120
|
+
(`pnpm probe:node`); it never produces GPU evidence. Results rendered through
|
|
121
|
+
SwiftShader or any other software rasterizer are not accepted as GPU evidence.
|
|
122
|
+
Headless runs are permitted solely as a harness smoke test and their records
|
|
123
|
+
must not be committed.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { GraphEngine, EngineCapabilities, EngineHostEvents, EngineCommit, FitViewOptions } from '@modernrelay/orbit-core/engine';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* CosmosEngine — GraphEngine adapter over @cosmos.gl/graph
|
|
4
|
+
* CosmosEngine — GraphEngine adapter over @cosmos.gl/graph.
|
|
5
5
|
*
|
|
6
|
-
* Node-safe module: cosmos is loaded lazily via `await import
|
|
7
|
-
* `mount
|
|
6
|
+
* Node-safe module: cosmos is loaded lazily via `await import` inside
|
|
7
|
+
* `mount`; module scope carries only type-only imports (erased at
|
|
8
8
|
* runtime) and never touches the DOM.
|
|
9
9
|
*
|
|
10
10
|
* Color scale note: the EngineCommit contract carries RGBA floats in [0,1],
|
|
@@ -13,47 +13,47 @@ import { GraphEngine, EngineCapabilities, EngineHostEvents, EngineCommit, FitVie
|
|
|
13
13
|
* input is fed to GPU buffers untouched) — so color buffers pass through.
|
|
14
14
|
*
|
|
15
15
|
* Interaction notes (verified against the 3.3.0 dist):
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
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 (the earlier 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 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
55
|
*
|
|
56
|
-
* GATED overlay/activity clock
|
|
56
|
+
* GATED overlay/activity clock:
|
|
57
57
|
* cosmos still exposes no draw/render-phase hook (`postDrawFrames: false`),
|
|
58
58
|
* so the adapter owns the single requestAnimationFrame loop that reports
|
|
59
59
|
* `onFrame(timeMs)` to the host — an ACTIVITY clock, not a post-draw hook:
|
|
@@ -65,29 +65,29 @@ import { GraphEngine, EngineCapabilities, EngineHostEvents, EngineCommit, FitVie
|
|
|
65
65
|
* cosmos transitions) and otherwise burns a small one-shot tick budget that
|
|
66
66
|
* every visual write re-arms (commits, highlight/focus/pin setters, camera
|
|
67
67
|
* moves, hover events; animated camera moves self-sustain through per-step
|
|
68
|
-
* onZoom bursts). Releasing a run reason always grants ONE trailing tick
|
|
68
|
+
* onZoom bursts). Releasing a run reason always grants ONE trailing tick
|
|
69
69
|
* the releasing activity's final GPU write lands after the current draw
|
|
70
70
|
* pass. At rest: zero rAF registrations from cosmos AND from this adapter
|
|
71
71
|
* (`capabilities.idleFrames: 'stops'`). The clock stops on context
|
|
72
72
|
* loss/destroy, re-arms on recovery, and skips the callback (keeping its
|
|
73
73
|
* schedule) while `document.hidden`.
|
|
74
74
|
*
|
|
75
|
-
* Screenshots use the
|
|
75
|
+
* Screenshots use the same-tick capture method (apps/spike/src/instrument.ts):
|
|
76
76
|
* cosmos renders with preserveDrawingBuffer:false, so the WebGL buffer is only
|
|
77
|
-
* readable via a synchronous drawImage inside the same rAF tick it was drawn
|
|
77
|
+
* readable via a synchronous drawImage inside the same rAF tick it was drawn
|
|
78
78
|
* `captureScreenshot` schedules one rAF, draws the cosmos canvas onto an
|
|
79
79
|
* offscreen 2D canvas inside that tick, and resolves the Blob (null on any
|
|
80
80
|
* failure or unusable lifecycle state).
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
81
|
+
* - The D3 drag events carry NO point index (the drag subject is a bare
|
|
82
|
+
* `{x, y}`); cosmos assigns `store.draggingPointIndex` immediately before
|
|
83
|
+
* invoking `onDragStart`, so the adapter reads it there (see
|
|
84
|
+
* `readDraggingIndex` for the public-callback fallback).
|
|
85
|
+
* - During a drag, cosmos' drag shader hard-pins the dragged point's
|
|
86
|
+
* position texture entry to the mouse's SPACE position every frame
|
|
87
|
+
* (dist `drag`: `pointPosition.rg = mousePos` where `mousePos` =
|
|
88
|
+
* screenToSpace(pointer)). So the point's final space position equals
|
|
89
|
+
* `screenToSpacePosition([event.x, event.y])` at drag end — an O(1) CPU
|
|
90
|
+
* transform, chosen over `getPointPositions` (full GPU readback).
|
|
91
91
|
*/
|
|
92
92
|
|
|
93
93
|
/** Derived from the contract to avoid importing the core root barrel. */
|
|
@@ -132,7 +132,7 @@ declare class CosmosEngine implements GraphEngine {
|
|
|
132
132
|
/** Sticky override from EngineConfigUpdate.seedRadius. */
|
|
133
133
|
private seedRadiusOverride;
|
|
134
134
|
/** Point count of the last structure-bearing commit — the roster length a
|
|
135
|
-
* `cluster: null` clear must write an all-unclustered array for
|
|
135
|
+
* `cluster: null` clear must write an all-unclustered array for. */
|
|
136
136
|
private lastPointCount;
|
|
137
137
|
/**
|
|
138
138
|
* cosmos fires `onClick` (index undefined) AND `onBackgroundClick` for the
|
|
@@ -143,7 +143,7 @@ declare class CosmosEngine implements GraphEngine {
|
|
|
143
143
|
private dragIndex;
|
|
144
144
|
/** Last hover reported by cosmos — fallback dragged-point source. */
|
|
145
145
|
private lastHoverIndex;
|
|
146
|
-
/** Live native link hover
|
|
146
|
+
/** Live native link hover gates the unified onClick's
|
|
147
147
|
* background interpretation — a hovered link owns the click). */
|
|
148
148
|
private hoveredLinkIndex;
|
|
149
149
|
/** rAF id of the pending activity-clock tick; null = clock not running. */
|
|
@@ -187,7 +187,7 @@ declare class CosmosEngine implements GraphEngine {
|
|
|
187
187
|
* zoom — `setViewport(p)` then `getViewport()` returns p, modulo cosmos'
|
|
188
188
|
* d3 scaleExtent clamp. A missing x or y is filled from the current
|
|
189
189
|
* viewport; zoom-only calls keep the `setZoomLevel` path (d3's scaleTo
|
|
190
|
-
* preserves the current center). Instant unless `durationMs` is given
|
|
190
|
+
* preserves the current center). Instant unless `durationMs` is given
|
|
191
191
|
* cosmos' own default duration is 250 ms, which would animate context-
|
|
192
192
|
* recovery replays.
|
|
193
193
|
*/
|
|
@@ -208,7 +208,7 @@ declare class CosmosEngine implements GraphEngine {
|
|
|
208
208
|
setPinnedIndices(indices: readonly number[] | null): void;
|
|
209
209
|
getPositions(): Float32Array | null;
|
|
210
210
|
/**
|
|
211
|
-
* Captures the cosmos canvas via the
|
|
211
|
+
* Captures the cosmos canvas via the same-tick method (see module
|
|
212
212
|
* header): one rAF is scheduled and, inside that tick, the WebGL canvas is
|
|
213
213
|
* drawn synchronously onto an offscreen 2D canvas (cosmos renders with
|
|
214
214
|
* preserveDrawingBuffer:false, so the buffer is only readable same-tick).
|
|
@@ -219,13 +219,13 @@ declare class CosmosEngine implements GraphEngine {
|
|
|
219
219
|
destroy(): void;
|
|
220
220
|
/**
|
|
221
221
|
* One rAF tick of the gated activity clock. Ordering is load-bearing:
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
222
|
+
* 1. the tick budget is consumed BEFORE the host callback runs, so work
|
|
223
|
+
* scheduled from inside onFrame re-arms a fresh tick instead of being
|
|
224
|
+
* coalesced away;
|
|
225
|
+
* 2. the reschedule/stop decision is made BEFORE the host callback, so a
|
|
226
|
+
* throwing host can never kill a should-keep-running clock (and a
|
|
227
|
+
* stopping clock stays stopped even if the callback wakes it — the
|
|
228
|
+
* wake starts a new loop via requestTicks/wake, not this handle).
|
|
229
229
|
* Skips the callback (but keeps its schedule) while the document is hidden.
|
|
230
230
|
*/
|
|
231
231
|
private readonly frameTick;
|
|
@@ -251,7 +251,7 @@ declare class CosmosEngine implements GraphEngine {
|
|
|
251
251
|
private get activeGraph();
|
|
252
252
|
/**
|
|
253
253
|
* cosmos owns its canvas; we can only wire webglcontext* listeners after
|
|
254
|
-
* init by querying it. A missing canvas downgrades to an info diagnostic
|
|
254
|
+
* init by querying it. A missing canvas downgrades to an info diagnostic
|
|
255
255
|
* the engine keeps working, just without context-loss recovery.
|
|
256
256
|
*/
|
|
257
257
|
private attachContextListeners;
|
|
@@ -310,48 +310,48 @@ declare class CosmosEngine implements GraphEngine {
|
|
|
310
310
|
private handleDragEnd;
|
|
311
311
|
private emitViewportChange;
|
|
312
312
|
/**
|
|
313
|
-
* One visibly atomic update
|
|
314
|
-
* exactly one render
|
|
313
|
+
* One visibly atomic update: all channels and config are staged, then
|
|
314
|
+
* exactly one render draws them; restart reheats after the render.
|
|
315
315
|
*/
|
|
316
316
|
private applyCommit;
|
|
317
317
|
/**
|
|
318
|
-
* Applies the
|
|
318
|
+
* Applies the stage-4 cluster force (capability `clusterForce`).
|
|
319
319
|
*
|
|
320
320
|
* Contract mapping, verified against the 3.3.0 dist (`index.d.ts`):
|
|
321
|
-
*
|
|
322
|
-
*
|
|
323
|
-
*
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
-
*
|
|
321
|
+
* - `pointClusters` (Float32Array, NaN = unclustered) →
|
|
322
|
+
* `setPointClusters((number | undefined)[])`, where cosmos' documented
|
|
323
|
+
* "does not belong to any cluster" value is `undefined`;
|
|
324
|
+
* - `centers` (Float32Array, `[x0,y0,x1,y1,…]`) →
|
|
325
|
+
* `setClusterPositions((number | undefined)[])`; a non-finite entry means
|
|
326
|
+
* "no position" and cosmos falls back to that cluster's centermass;
|
|
327
|
+
* - `null` clears: an all-`undefined` array of the CURRENT roster length
|
|
328
|
+
* (length must track the roster) plus empty cluster positions.
|
|
329
329
|
* `strength` is the scene-wide `simulationCluster` config coefficient
|
|
330
330
|
* applied in the config block, not the per-point
|
|
331
331
|
* `setPointClusterStrength` buffer (Orbit's strength is scene-wide).
|
|
332
332
|
*/
|
|
333
333
|
private applyClusterForce;
|
|
334
334
|
/**
|
|
335
|
-
* Applies the
|
|
335
|
+
* Applies the image-atlas channel: upserts convert ImageBitmap →
|
|
336
336
|
* ImageData through an offscreen 2D canvas into the slot mirror, removals
|
|
337
337
|
* blank their slot, and any atlas change re-uploads the FULL ImageData
|
|
338
338
|
* array (cosmos' setImageData is whole-array only, matching
|
|
339
339
|
* `rangeUpdates: []`). Environments without a usable 2D context (jsdom)
|
|
340
|
-
* no-op the channel and report `engine:image-channel-unavailable` once
|
|
340
|
+
* no-op the channel and report `engine:image-channel-unavailable` once
|
|
341
341
|
* never throw.
|
|
342
342
|
*/
|
|
343
343
|
private applyResources;
|
|
344
344
|
/**
|
|
345
345
|
* Reads an ImageData of the given size off an offscreen 2D canvas, drawing
|
|
346
346
|
* `bitmap` onto it first when provided (ImageBitmap → ImageData transcode;
|
|
347
|
-
* without a bitmap: a transparent blank). Returns null — never throws
|
|
347
|
+
* without a bitmap: a transparent blank). Returns null — never throws
|
|
348
348
|
* where no 2D context exists (jsdom without the canvas package).
|
|
349
349
|
*/
|
|
350
350
|
private canvasImageData;
|
|
351
351
|
/** Documented degradation, reported at most once per engine instance. */
|
|
352
352
|
private reportImageChannelUnavailable;
|
|
353
353
|
/**
|
|
354
|
-
* Replaces NaN pairs (= "no known position"
|
|
354
|
+
* Replaces NaN pairs (= "no known position") with random points on a
|
|
355
355
|
* ring of radius seedRadius around the space center. cosmos treats NaN
|
|
356
356
|
* positions as *absent* points, so they must never reach setPointPositions.
|
|
357
357
|
* Known positions pass through verbatim (same array when nothing to seed).
|