@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/dist/index.js CHANGED
@@ -67,21 +67,21 @@ var DEFAULT_SPACE_SIZE = 4096;
67
67
  var DEFAULT_RESTORE_DEADLINE_MS = 1e4;
68
68
  var CosmosEngine = class {
69
69
  capabilities = {
70
- // Evidence-backed S6 flip: the M0 probe measured native onLinkClick/
70
+ // Evidence-backed flip: the probe measured native onLinkClick/
71
71
  // onLinkMouseOver delivering correct link indices with ~4px perpendicular
72
- // tolerance (docs/m0-conformance.json, `link-picking`).
72
+ // tolerance.
73
73
  linkPicking: true,
74
74
  rangeUpdates: [],
75
75
  trackedPositions: true,
76
76
  simulation: true,
77
- // §16.12: cosmos 3.3.0 exposes the `linkDefaultArrows` config key
77
+ // cosmos 3.3.0 exposes the `linkDefaultArrows` config key
78
78
  // (config.d.ts) — instanced arrowheads toggle atomically via setConfigPartial.
79
79
  edgeArrows: true,
80
- // §8: cosmos 3.3.0 exposes setImageData(ImageData[]) +
80
+ // cosmos 3.3.0 exposes setImageData(ImageData[]) +
81
81
  // setPointImageIndices(Float32Array) (index.d.ts) — per-point sprites via
82
82
  // an adapter-maintained slot→ImageData atlas.
83
83
  pointImages: true,
84
- // §16.3 stage 4: cosmos 3.3.0 ships a real GPU cluster force
84
+ // stage 4: cosmos 3.3.0 ships a real GPU cluster force
85
85
  // `setPointClusters((number|undefined)[])`, `setClusterPositions(
86
86
  // (number|undefined)[])`, `setPointClusterStrength(Float32Array)`
87
87
  // (index.d.ts) plus the `simulationCluster` coefficient (config.d.ts,
@@ -89,14 +89,14 @@ var CosmosEngine = class {
89
89
  // `undefined` is cosmos' documented "not in any cluster" value, which the
90
90
  // adapter maps from the contract's NaN.
91
91
  clusterForce: true,
92
- // §17 stop-at-rest: cosmos 3.4.0 ships on-demand rendering — the M0
92
+ // stop-at-rest: cosmos 3.4.0 ships on-demand rendering — the M0
93
93
  // quiescence row measured 0 idle frames AND 0 rAF registrations per
94
- // 750ms at rest (docs/m0-conformance.md, was 91 on 3.3.0/ADR-002).
95
- // The adapter's own activity clock is gated to match (ADR-005).
94
+ // 750ms at rest.
95
+ // The adapter's own activity clock is gated to match.
96
96
  idleFrames: "stops",
97
97
  // onFrame is an activity clock, not an exact post-draw hook — cosmos
98
98
  // still exposes no public frame hook (probe `post-draw-frames`);
99
- // overlays may lag one sample (ADR-002).
99
+ // overlays may lag one sample.
100
100
  postDrawFrames: false
101
101
  };
102
102
  options;
@@ -118,7 +118,7 @@ var CosmosEngine = class {
118
118
  /** Sticky override from EngineConfigUpdate.seedRadius. */
119
119
  seedRadiusOverride;
120
120
  /** Point count of the last structure-bearing commit — the roster length a
121
- * `cluster: null` clear must write an all-unclustered array for (§16.3). */
121
+ * `cluster: null` clear must write an all-unclustered array for. */
122
122
  lastPointCount = 0;
123
123
  /**
124
124
  * cosmos fires `onClick` (index undefined) AND `onBackgroundClick` for the
@@ -129,7 +129,7 @@ var CosmosEngine = class {
129
129
  dragIndex = null;
130
130
  /** Last hover reported by cosmos — fallback dragged-point source. */
131
131
  lastHoverIndex = null;
132
- /** Live native link hover (review P1: gates the unified onClick's
132
+ /** Live native link hover gates the unified onClick's
133
133
  * background interpretation — a hovered link owns the click). */
134
134
  hoveredLinkIndex = null;
135
135
  // --- adapter-owned GATED overlay/activity clock (see module header) ---
@@ -143,7 +143,7 @@ var CosmosEngine = class {
143
143
  pendingTicks = 0;
144
144
  /** One-shot guard for the repulsionTheta deprecation diagnostic. */
145
145
  repulsionThetaWarned = false;
146
- // --- WebGL context-loss recovery state (§13.1) ---
146
+ // --- WebGL context-loss recovery state ---
147
147
  /** cosmos' canvas (queried post-mount) carrying the webglcontext* listeners. */
148
148
  canvas = null;
149
149
  contextLost = false;
@@ -152,7 +152,7 @@ var CosmosEngine = class {
152
152
  deadline = null;
153
153
  /** Graph constructor cached at mount so recovery never re-imports cosmos. */
154
154
  cosmosCtor = null;
155
- // --- §8 image atlas (capability pointImages) ---
155
+ // --- image atlas (capability pointImages) ---
156
156
  /**
157
157
  * slot → ImageData mirror of the cosmos image atlas. ImageData is CPU-side,
158
158
  * so the mirror survives context loss — any post-restore atlas commit
@@ -262,7 +262,7 @@ var CosmosEngine = class {
262
262
  * zoom — `setViewport(p)` then `getViewport()` returns p, modulo cosmos'
263
263
  * d3 scaleExtent clamp. A missing x or y is filled from the current
264
264
  * viewport; zoom-only calls keep the `setZoomLevel` path (d3's scaleTo
265
- * preserves the current center). Instant unless `durationMs` is given
265
+ * preserves the current center). Instant unless `durationMs` is given
266
266
  * cosmos' own default duration is 250 ms, which would animate context-
267
267
  * recovery replays.
268
268
  */
@@ -323,7 +323,7 @@ var CosmosEngine = class {
323
323
  });
324
324
  this.requestTicks(2);
325
325
  }
326
- // --- spatial queries & pinning (§13/§15) ---
326
+ // --- spatial queries & pinning ---
327
327
  pointsInPolygon(screenPolygon) {
328
328
  const graph = this.activeGraph;
329
329
  if (!graph) return [];
@@ -361,7 +361,7 @@ var CosmosEngine = class {
361
361
  return graph ? Float32Array.from(graph.getPointPositions()) : null;
362
362
  }
363
363
  /**
364
- * Captures the cosmos canvas via the M0 same-tick method (see module
364
+ * Captures the cosmos canvas via the same-tick method (see module
365
365
  * header): one rAF is scheduled and, inside that tick, the WebGL canvas is
366
366
  * drawn synchronously onto an offscreen 2D canvas (cosmos renders with
367
367
  * preserveDrawingBuffer:false, so the buffer is only readable same-tick).
@@ -428,13 +428,13 @@ var CosmosEngine = class {
428
428
  // --- gated overlay/activity clock (see module header) ---
429
429
  /**
430
430
  * One rAF tick of the gated activity clock. Ordering is load-bearing:
431
- * 1. the tick budget is consumed BEFORE the host callback runs, so work
432
- * scheduled from inside onFrame re-arms a fresh tick instead of being
433
- * coalesced away;
434
- * 2. the reschedule/stop decision is made BEFORE the host callback, so a
435
- * throwing host can never kill a should-keep-running clock (and a
436
- * stopping clock stays stopped even if the callback wakes it — the
437
- * wake starts a new loop via requestTicks/wake, not this handle).
431
+ * 1. the tick budget is consumed BEFORE the host callback runs, so work
432
+ * scheduled from inside onFrame re-arms a fresh tick instead of being
433
+ * coalesced away;
434
+ * 2. the reschedule/stop decision is made BEFORE the host callback, so a
435
+ * throwing host can never kill a should-keep-running clock (and a
436
+ * stopping clock stays stopped even if the callback wakes it — the
437
+ * wake starts a new loop via requestTicks/wake, not this handle).
438
438
  * Skips the callback (but keeps its schedule) while the document is hidden.
439
439
  */
440
440
  frameTick = (timeMs) => {
@@ -488,14 +488,14 @@ var CosmosEngine = class {
488
488
  this.runReasons.clear();
489
489
  this.pendingTicks = 0;
490
490
  }
491
- // --- WebGL context-loss recovery (§13.1) ---
491
+ // --- WebGL context-loss recovery ---
492
492
  /** The graph, unless it is unusable (context lost / terminally failed). */
493
493
  get activeGraph() {
494
494
  return this.contextLost || this.failed ? null : this.graph;
495
495
  }
496
496
  /**
497
497
  * cosmos owns its canvas; we can only wire webglcontext* listeners after
498
- * init by querying it. A missing canvas downgrades to an info diagnostic
498
+ * init by querying it. A missing canvas downgrades to an info diagnostic
499
499
  * the engine keeps working, just without context-loss recovery.
500
500
  */
501
501
  attachContextListeners() {
@@ -617,31 +617,38 @@ var CosmosEngine = class {
617
617
  * later explicit directive (including `false`) replaces it.
618
618
  */
619
619
  queueCommit(update) {
620
+ const ownedUpdate = cloneCommitForQueue(update);
620
621
  const previous = this.pendingCommit;
621
622
  if (previous === null) {
622
- this.pendingCommit = update;
623
+ this.pendingCommit = ownedUpdate;
623
624
  return;
624
625
  }
625
- const merged = { revision: update.revision };
626
- const structure = update.structure ?? previous.structure;
626
+ const merged = { revision: ownedUpdate.revision };
627
+ const structure = ownedUpdate.structure ?? previous.structure;
627
628
  if (structure !== void 0) merged.structure = structure;
628
- if (previous.buffers !== void 0 || update.buffers !== void 0) {
629
- merged.buffers = { ...previous.buffers, ...update.buffers };
629
+ if (previous.buffers !== void 0 || ownedUpdate.buffers !== void 0) {
630
+ merged.buffers = { ...previous.buffers, ...ownedUpdate.buffers };
630
631
  }
631
- if (previous.config !== void 0 || update.config !== void 0) {
632
- const config = { ...previous.config, ...update.config };
633
- if (previous.config?.simulation !== void 0 || update.config?.simulation !== void 0) {
632
+ if (previous.bufferPatches !== void 0 || ownedUpdate.bufferPatches !== void 0) {
633
+ merged.bufferPatches = {
634
+ ...previous.bufferPatches,
635
+ ...ownedUpdate.bufferPatches
636
+ };
637
+ }
638
+ if (previous.config !== void 0 || ownedUpdate.config !== void 0) {
639
+ const config = { ...previous.config, ...ownedUpdate.config };
640
+ if (previous.config?.simulation !== void 0 || ownedUpdate.config?.simulation !== void 0) {
634
641
  config.simulation = {
635
642
  ...previous.config?.simulation,
636
- ...update.config?.simulation
643
+ ...ownedUpdate.config?.simulation
637
644
  };
638
645
  }
639
646
  merged.config = config;
640
647
  }
641
- if (previous.resources !== void 0 || update.resources !== void 0) {
642
- merged.resources = mergeResources(previous.resources, update.resources);
648
+ if (previous.resources !== void 0 || ownedUpdate.resources !== void 0) {
649
+ merged.resources = mergeResources(previous.resources, ownedUpdate.resources);
643
650
  }
644
- const restart = update.restart !== void 0 && update.restart !== false ? update.restart : previous.restart;
651
+ const restart = ownedUpdate.restart !== void 0 && ownedUpdate.restart !== false ? ownedUpdate.restart : previous.restart;
645
652
  if (restart !== void 0) merged.restart = restart;
646
653
  this.pendingCommit = merged;
647
654
  }
@@ -669,10 +676,10 @@ var CosmosEngine = class {
669
676
  // cosmos 3.4 defaults this to true. Pinned OFF for the 3.4 upgrade:
670
677
  // occlusion culling changes which overlapping pixels draw, and the M5
671
678
  // pixel-diff thresholds (CHANGED_MIN/RESTORED_MAX) were tuned against
672
- // un-culled rendering. One variable at a time flipping it is a
673
- // registered follow-up with its own threshold re-validation.
679
+ // un-culled rendering. Keep one variable at a time; enabling culling
680
+ // requires its own threshold re-validation.
674
681
  pointOcclusionCulling: false,
675
- // Native point dragging; the core owns pin semantics on top (§16.3).
682
+ // Native point dragging; the core owns pin semantics on top.
676
683
  enableDrag: this.options.enableDrag ?? true,
677
684
  onPointClick: (index, _position, event) => {
678
685
  this.events?.onPointClick?.(index, clickModifiers(event));
@@ -733,7 +740,7 @@ var CosmosEngine = class {
733
740
  this.requestTicks(2);
734
741
  this.emitViewportChange();
735
742
  },
736
- // --- gated-clock run reasons (§17 stop-at-rest, ADR-005) ---
743
+ // --- gated-clock run reasons ---
737
744
  onSimulationStart: () => {
738
745
  this.wake("sim");
739
746
  },
@@ -833,8 +840,8 @@ var CosmosEngine = class {
833
840
  if (viewport) this.events?.onViewportChange?.(viewport);
834
841
  }
835
842
  /**
836
- * One visibly atomic update (§13): all channels and config are staged, then
837
- * exactly one render() draws them; restart reheats after the render.
843
+ * One visibly atomic update: all channels and config are staged, then
844
+ * exactly one render draws them; restart reheats after the render.
838
845
  */
839
846
  applyCommit(graph, update) {
840
847
  const { config, structure, buffers, resources, restart } = update;
@@ -909,17 +916,17 @@ var CosmosEngine = class {
909
916
  this.requestTicks(2);
910
917
  }
911
918
  /**
912
- * Applies the §16.3 stage-4 cluster force (capability `clusterForce`).
919
+ * Applies the stage-4 cluster force (capability `clusterForce`).
913
920
  *
914
921
  * Contract mapping, verified against the 3.3.0 dist (`index.d.ts`):
915
- * - `pointClusters` (Float32Array, NaN = unclustered) →
916
- * `setPointClusters((number | undefined)[])`, where cosmos' documented
917
- * "does not belong to any cluster" value is `undefined`;
918
- * - `centers` (Float32Array, `[x0,y0,x1,y1,…]`) →
919
- * `setClusterPositions((number | undefined)[])`; a non-finite entry means
920
- * "no position" and cosmos falls back to that cluster's centermass;
921
- * - `null` clears: an all-`undefined` array of the CURRENT roster length
922
- * (length must track the roster) plus empty cluster positions.
922
+ * - `pointClusters` (Float32Array, NaN = unclustered) →
923
+ * `setPointClusters((number | undefined)[])`, where cosmos' documented
924
+ * "does not belong to any cluster" value is `undefined`;
925
+ * - `centers` (Float32Array, `[x0,y0,x1,y1,…]`) →
926
+ * `setClusterPositions((number | undefined)[])`; a non-finite entry means
927
+ * "no position" and cosmos falls back to that cluster's centermass;
928
+ * - `null` clears: an all-`undefined` array of the CURRENT roster length
929
+ * (length must track the roster) plus empty cluster positions.
923
930
  * `strength` is the scene-wide `simulationCluster` config coefficient
924
931
  * applied in the config block, not the per-point
925
932
  * `setPointClusterStrength` buffer (Orbit's strength is scene-wide).
@@ -948,12 +955,12 @@ var CosmosEngine = class {
948
955
  }
949
956
  }
950
957
  /**
951
- * Applies the §8 image-atlas channel: upserts convert ImageBitmap →
958
+ * Applies the image-atlas channel: upserts convert ImageBitmap →
952
959
  * ImageData through an offscreen 2D canvas into the slot mirror, removals
953
960
  * blank their slot, and any atlas change re-uploads the FULL ImageData
954
961
  * array (cosmos' setImageData is whole-array only, matching
955
962
  * `rangeUpdates: []`). Environments without a usable 2D context (jsdom)
956
- * no-op the channel and report `engine:image-channel-unavailable` once
963
+ * no-op the channel and report `engine:image-channel-unavailable` once
957
964
  * never throw.
958
965
  */
959
966
  applyResources(graph, resources) {
@@ -999,7 +1006,7 @@ var CosmosEngine = class {
999
1006
  /**
1000
1007
  * Reads an ImageData of the given size off an offscreen 2D canvas, drawing
1001
1008
  * `bitmap` onto it first when provided (ImageBitmap → ImageData transcode;
1002
- * without a bitmap: a transparent blank). Returns null — never throws
1009
+ * without a bitmap: a transparent blank). Returns null — never throws
1003
1010
  * where no 2D context exists (jsdom without the canvas package).
1004
1011
  */
1005
1012
  canvasImageData(width, height, bitmap) {
@@ -1028,7 +1035,7 @@ var CosmosEngine = class {
1028
1035
  });
1029
1036
  }
1030
1037
  /**
1031
- * Replaces NaN pairs (= "no known position", §7.3) with random points on a
1038
+ * Replaces NaN pairs (= "no known position") with random points on a
1032
1039
  * ring of radius seedRadius around the space center. cosmos treats NaN
1033
1040
  * positions as *absent* points, so they must never reach setPointPositions.
1034
1041
  * Known positions pass through verbatim (same array when nothing to seed).
@@ -1055,6 +1062,98 @@ var CosmosEngine = class {
1055
1062
  return out;
1056
1063
  }
1057
1064
  };
1065
+ function cloneCommitForQueue(update) {
1066
+ const owned = { revision: update.revision };
1067
+ if (update.structure !== void 0) {
1068
+ owned.structure = {
1069
+ pointCount: update.structure.pointCount,
1070
+ positions: update.structure.positions.slice(),
1071
+ links: update.structure.links.slice()
1072
+ };
1073
+ }
1074
+ if (update.buffers !== void 0) {
1075
+ const buffers = {};
1076
+ if (update.buffers.pointColor !== void 0) {
1077
+ buffers.pointColor = update.buffers.pointColor.slice();
1078
+ }
1079
+ if (update.buffers.pointSize !== void 0) {
1080
+ buffers.pointSize = update.buffers.pointSize.slice();
1081
+ }
1082
+ if (update.buffers.linkColor !== void 0) {
1083
+ buffers.linkColor = update.buffers.linkColor.slice();
1084
+ }
1085
+ if (update.buffers.linkWidth !== void 0) {
1086
+ buffers.linkWidth = update.buffers.linkWidth.slice();
1087
+ }
1088
+ owned.buffers = buffers;
1089
+ }
1090
+ if (update.bufferPatches !== void 0) {
1091
+ const patches = {};
1092
+ if (update.bufferPatches.pointColor !== void 0) {
1093
+ patches.pointColor = update.bufferPatches.pointColor.map(({ start, data }) => ({
1094
+ start,
1095
+ data: data.slice()
1096
+ }));
1097
+ }
1098
+ if (update.bufferPatches.pointSize !== void 0) {
1099
+ patches.pointSize = update.bufferPatches.pointSize.map(({ start, data }) => ({
1100
+ start,
1101
+ data: data.slice()
1102
+ }));
1103
+ }
1104
+ if (update.bufferPatches.linkColor !== void 0) {
1105
+ patches.linkColor = update.bufferPatches.linkColor.map(({ start, data }) => ({
1106
+ start,
1107
+ data: data.slice()
1108
+ }));
1109
+ }
1110
+ if (update.bufferPatches.linkWidth !== void 0) {
1111
+ patches.linkWidth = update.bufferPatches.linkWidth.map(({ start, data }) => ({
1112
+ start,
1113
+ data: data.slice()
1114
+ }));
1115
+ }
1116
+ owned.bufferPatches = patches;
1117
+ }
1118
+ if (update.config !== void 0) {
1119
+ const config = { ...update.config };
1120
+ if (update.config.simulation !== void 0) {
1121
+ config.simulation = { ...update.config.simulation };
1122
+ }
1123
+ if (update.config.cluster !== void 0) {
1124
+ config.cluster = update.config.cluster === null ? null : {
1125
+ ...update.config.cluster,
1126
+ pointClusters: update.config.cluster.pointClusters.slice(),
1127
+ ...update.config.cluster.centers === void 0 ? {} : { centers: update.config.cluster.centers.slice() }
1128
+ };
1129
+ }
1130
+ owned.config = config;
1131
+ }
1132
+ if (update.resources !== void 0) {
1133
+ const resources = {};
1134
+ if (update.resources.imageAtlas !== void 0) {
1135
+ const imageAtlas = {};
1136
+ if (update.resources.imageAtlas.upserts !== void 0) {
1137
+ imageAtlas.upserts = update.resources.imageAtlas.upserts.map(({ slot, bitmap }) => ({
1138
+ slot,
1139
+ bitmap
1140
+ }));
1141
+ }
1142
+ if (update.resources.imageAtlas.removeSlots !== void 0) {
1143
+ imageAtlas.removeSlots = Array.from(update.resources.imageAtlas.removeSlots);
1144
+ }
1145
+ resources.imageAtlas = imageAtlas;
1146
+ }
1147
+ if (update.resources.pointImageIndex !== void 0) {
1148
+ resources.pointImageIndex = update.resources.pointImageIndex.slice();
1149
+ }
1150
+ owned.resources = resources;
1151
+ }
1152
+ if (update.restart !== void 0) {
1153
+ owned.restart = update.restart === false ? false : { alpha: update.restart.alpha };
1154
+ }
1155
+ return owned;
1156
+ }
1058
1157
  function mergeResources(previous, update) {
1059
1158
  const upsertBySlot = /* @__PURE__ */ new Map();
1060
1159
  const removes = /* @__PURE__ */ new Set();