@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 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). Any pin bump requires re-running the M0 probe suite and
9
- regenerating the conformance matrix before the new version is considered
10
- supported. (3.3.0 3.4.0 re-probed 2026-08-10: all 11 rows green the
11
- quiescence row flipped to PASS on upstream on-demand rendering, ADR-005.)
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 (quiescence row below; ADR-005). |
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-07-21 on Apple M5 Pro, ANGLE Metal, headful
25
- Chromium; evidence in `docs/evidence/m0/`):
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 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)). |
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 (ADR-001 stands). |
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 (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. |
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
- The full, generated matrix (including deferred areas: `mobile`, `resize`,
39
- `resource-estimates`) lives at [docs/m0-conformance.md](../../docs/m0-conformance.md).
40
+ ## Interaction surface
40
41
 
41
- ## Interaction surface (S6)
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 (S7)
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 (ADR-005): 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`). |
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 3.3.0 d.ts; the dist flips Y assuming ordered corners) — no conversion. |
66
- | `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. 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 (ADR-005). |
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 (S10)
67
+ ## Styling channels
69
68
 
70
- Facts verified against the 3.3.0 dist typings (`config.d.ts` / `index.d.ts`):
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.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. |
76
- | `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. |
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 (S12, §16.3 stage 4)
83
+ ## Cluster force
85
84
 
86
85
  | Capability | Value | Mechanism / basis |
87
86
  | --- | --- | --- |
88
- | `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
+ | `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 (S11)
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.** 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())`. |
113
- | §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. |
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 never produces GPU evidence: it re-runs only the
120
- Node-safe probes (`pnpm probe:node`) and validates the committed matrix
121
- (`pnpm conformance:check`). Results rendered through SwiftShader or any other
122
- software rasterizer are not accepted as conformance evidence — headless runs
123
- are permitted solely as a harness smoke test and their records must not be
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 (spec §13).
4
+ * CosmosEngine — GraphEngine adapter over @cosmos.gl/graph.
5
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
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
- * - 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).
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 (ADR-005; M0 evidence docs/m0-conformance.md):
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 M0 same-tick capture method (apps/spike/src/instrument.ts):
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
- * - 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).
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 (§16.3). */
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 (review P1: gates the unified onClick's
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 M0 same-tick method (see module
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
- * 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).
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 (§13): all channels and config are staged, then
314
- * exactly one render() draws them; restart reheats after the 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 §16.3 stage-4 cluster force (capability `clusterForce`).
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
- * - `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.
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 §8 image-atlas channel: upserts convert ImageBitmap →
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", §7.3) with random points on a
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).