@rybosome/tspice 0.0.3 → 0.0.8

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.
Files changed (221) hide show
  1. package/README.md +145 -84
  2. package/backend-contract/dist/.tsbuildinfo +1 -1
  3. package/backend-contract/dist/domains/cells-windows.d.ts +94 -0
  4. package/backend-contract/dist/domains/cells-windows.js +10 -0
  5. package/backend-contract/dist/domains/coords-vectors.d.ts +53 -3
  6. package/backend-contract/dist/domains/dsk.d.ts +49 -0
  7. package/backend-contract/dist/domains/dsk.js +2 -0
  8. package/backend-contract/dist/domains/ek.d.ts +186 -0
  9. package/backend-contract/dist/domains/ek.js +8 -0
  10. package/backend-contract/dist/domains/ephemeris.d.ts +141 -3
  11. package/backend-contract/dist/domains/error.d.ts +42 -0
  12. package/backend-contract/dist/domains/error.js +33 -0
  13. package/backend-contract/dist/domains/file-io.d.ts +114 -0
  14. package/backend-contract/dist/domains/file-io.js +8 -0
  15. package/backend-contract/dist/domains/frames.d.ts +44 -4
  16. package/backend-contract/dist/domains/geometry-gf.d.ts +44 -0
  17. package/backend-contract/dist/domains/geometry-gf.js +14 -0
  18. package/backend-contract/dist/domains/geometry.d.ts +21 -1
  19. package/backend-contract/dist/domains/ids-names-normalize.d.ts +3 -0
  20. package/backend-contract/dist/domains/ids-names-normalize.js +74 -0
  21. package/backend-contract/dist/domains/ids-names.d.ts +37 -0
  22. package/backend-contract/dist/domains/kernel-pool.d.ts +134 -0
  23. package/backend-contract/dist/domains/kernel-pool.js +2 -0
  24. package/backend-contract/dist/domains/kernels-utils.d.ts +44 -0
  25. package/backend-contract/dist/domains/kernels-utils.js +265 -0
  26. package/backend-contract/dist/domains/kernels.d.ts +39 -3
  27. package/backend-contract/dist/domains/time.d.ts +102 -0
  28. package/backend-contract/dist/index.d.ts +34 -15
  29. package/backend-contract/dist/index.js +15 -1
  30. package/backend-contract/dist/shared/errors.d.ts +6 -0
  31. package/backend-contract/dist/shared/errors.js +8 -0
  32. package/backend-contract/dist/shared/mat3.d.ts +52 -0
  33. package/backend-contract/dist/shared/mat3.js +150 -0
  34. package/backend-contract/dist/shared/mat6.d.ts +34 -0
  35. package/backend-contract/dist/shared/mat6.js +116 -0
  36. package/backend-contract/dist/shared/spice-handles.d.ts +20 -0
  37. package/backend-contract/dist/shared/spice-handles.js +82 -0
  38. package/backend-contract/dist/shared/spice-int.d.ts +32 -0
  39. package/backend-contract/dist/shared/spice-int.js +41 -0
  40. package/backend-contract/dist/shared/types.d.ts +136 -5
  41. package/backend-contract/dist/shared/types.js +1 -1
  42. package/backend-contract/dist/shared/vec.d.ts +54 -0
  43. package/backend-contract/dist/shared/vec.js +162 -0
  44. package/backend-fake/dist/.tsbuildinfo +1 -1
  45. package/backend-fake/dist/index.d.ts +21 -1
  46. package/backend-fake/dist/index.js +1112 -33
  47. package/backend-node/dist/.tsbuildinfo +1 -1
  48. package/backend-node/dist/codec/arrays.d.ts +9 -0
  49. package/backend-node/dist/codec/arrays.js +36 -0
  50. package/backend-node/dist/codec/errors.d.ts +6 -6
  51. package/backend-node/dist/codec/errors.js +6 -6
  52. package/backend-node/dist/domains/cells-windows.d.ts +5 -0
  53. package/backend-node/dist/domains/cells-windows.js +112 -0
  54. package/backend-node/dist/domains/coords-vectors.d.ts +1 -0
  55. package/backend-node/dist/domains/coords-vectors.js +66 -0
  56. package/backend-node/dist/domains/dsk.d.ts +6 -0
  57. package/backend-node/dist/domains/dsk.js +108 -0
  58. package/backend-node/dist/domains/ek.d.ts +10 -0
  59. package/backend-node/dist/domains/ek.js +100 -0
  60. package/backend-node/dist/domains/ephemeris.d.ts +5 -1
  61. package/backend-node/dist/domains/ephemeris.js +150 -1
  62. package/backend-node/dist/domains/error.d.ts +5 -0
  63. package/backend-node/dist/domains/error.js +34 -0
  64. package/backend-node/dist/domains/file-io.d.ts +7 -0
  65. package/backend-node/dist/domains/file-io.js +105 -0
  66. package/backend-node/dist/domains/frames.d.ts +1 -0
  67. package/backend-node/dist/domains/frames.js +58 -6
  68. package/backend-node/dist/domains/geometry-gf.d.ts +5 -0
  69. package/backend-node/dist/domains/geometry-gf.js +74 -0
  70. package/backend-node/dist/domains/geometry.d.ts +1 -0
  71. package/backend-node/dist/domains/geometry.js +62 -0
  72. package/backend-node/dist/domains/ids-names.d.ts +2 -1
  73. package/backend-node/dist/domains/ids-names.js +30 -0
  74. package/backend-node/dist/domains/kernel-pool.d.ts +5 -0
  75. package/backend-node/dist/domains/kernel-pool.js +74 -0
  76. package/backend-node/dist/domains/kernels.d.ts +1 -0
  77. package/backend-node/dist/domains/kernels.js +100 -13
  78. package/backend-node/dist/domains/time.d.ts +1 -0
  79. package/backend-node/dist/domains/time.js +75 -1
  80. package/backend-node/dist/index.d.ts +5 -1
  81. package/backend-node/dist/index.js +62 -1
  82. package/backend-node/dist/lowlevel/binding.d.ts +3 -0
  83. package/backend-node/dist/lowlevel/binding.js +115 -0
  84. package/backend-node/dist/runtime/addon.d.ts +273 -2
  85. package/backend-node/dist/runtime/addon.js +3 -0
  86. package/backend-node/dist/runtime/kernel-staging.d.ts +17 -0
  87. package/backend-node/dist/runtime/kernel-staging.js +80 -7
  88. package/backend-node/dist/runtime/spice-handles.d.ts +3 -0
  89. package/backend-node/dist/runtime/spice-handles.js +2 -0
  90. package/backend-node/dist/runtime/virtual-output-staging.d.ts +16 -0
  91. package/backend-node/dist/runtime/virtual-output-staging.js +148 -0
  92. package/backend-wasm/dist/.tsbuildinfo +1 -1
  93. package/backend-wasm/dist/codec/alloc.d.ts +19 -0
  94. package/backend-wasm/dist/codec/alloc.js +64 -0
  95. package/backend-wasm/dist/codec/calls.d.ts +2 -0
  96. package/backend-wasm/dist/codec/calls.js +13 -24
  97. package/backend-wasm/dist/codec/errors.d.ts +6 -0
  98. package/backend-wasm/dist/codec/errors.js +34 -2
  99. package/backend-wasm/dist/codec/found.d.ts +2 -0
  100. package/backend-wasm/dist/codec/found.js +20 -43
  101. package/backend-wasm/dist/codec/strings.d.ts +31 -1
  102. package/backend-wasm/dist/codec/strings.js +93 -6
  103. package/backend-wasm/dist/domains/cells-windows.d.ts +9 -0
  104. package/backend-wasm/dist/domains/cells-windows.js +392 -0
  105. package/backend-wasm/dist/domains/coords-vectors.d.ts +1 -0
  106. package/backend-wasm/dist/domains/coords-vectors.js +377 -184
  107. package/backend-wasm/dist/domains/dsk.d.ts +6 -0
  108. package/backend-wasm/dist/domains/dsk.js +179 -0
  109. package/backend-wasm/dist/domains/ek.d.ts +6 -0
  110. package/backend-wasm/dist/domains/ek.js +543 -0
  111. package/backend-wasm/dist/domains/ephemeris.d.ts +4 -1
  112. package/backend-wasm/dist/domains/ephemeris.js +405 -46
  113. package/backend-wasm/dist/domains/error.d.ts +5 -0
  114. package/backend-wasm/dist/domains/error.js +109 -0
  115. package/backend-wasm/dist/domains/file-io.d.ts +7 -0
  116. package/backend-wasm/dist/domains/file-io.js +462 -0
  117. package/backend-wasm/dist/domains/frames.d.ts +1 -0
  118. package/backend-wasm/dist/domains/frames.js +139 -6
  119. package/backend-wasm/dist/domains/geometry-gf.d.ts +5 -0
  120. package/backend-wasm/dist/domains/geometry-gf.js +178 -0
  121. package/backend-wasm/dist/domains/geometry.d.ts +1 -0
  122. package/backend-wasm/dist/domains/geometry.js +210 -0
  123. package/backend-wasm/dist/domains/ids-names.d.ts +2 -1
  124. package/backend-wasm/dist/domains/ids-names.js +89 -0
  125. package/backend-wasm/dist/domains/kernel-pool.d.ts +5 -0
  126. package/backend-wasm/dist/domains/kernel-pool.js +357 -0
  127. package/backend-wasm/dist/domains/kernels.d.ts +1 -0
  128. package/backend-wasm/dist/domains/kernels.js +108 -4
  129. package/backend-wasm/dist/domains/time.d.ts +2 -0
  130. package/backend-wasm/dist/domains/time.js +235 -133
  131. package/backend-wasm/dist/index.d.ts +4 -2
  132. package/backend-wasm/dist/lowlevel/exports.d.ts +215 -1
  133. package/backend-wasm/dist/lowlevel/exports.js +217 -38
  134. package/backend-wasm/dist/runtime/create-backend-options.d.ts +21 -0
  135. package/backend-wasm/dist/runtime/create-backend.node.d.ts +11 -2
  136. package/backend-wasm/dist/runtime/create-backend.node.js +283 -14
  137. package/backend-wasm/dist/runtime/create-backend.web.d.ts +5 -2
  138. package/backend-wasm/dist/runtime/create-backend.web.js +40 -6
  139. package/backend-wasm/dist/runtime/fs.d.ts +6 -0
  140. package/backend-wasm/dist/runtime/fs.js +29 -3
  141. package/backend-wasm/dist/runtime/spice-handles.d.ts +3 -0
  142. package/backend-wasm/dist/runtime/spice-handles.js +2 -0
  143. package/backend-wasm/dist/runtime/virtual-outputs.d.ts +16 -0
  144. package/backend-wasm/dist/runtime/virtual-outputs.js +35 -0
  145. package/backend-wasm/dist/tspice_backend_wasm.node.js +3 -3
  146. package/backend-wasm/dist/tspice_backend_wasm.wasm +0 -0
  147. package/backend-wasm/dist/tspice_backend_wasm.web.js +1 -1
  148. package/core/dist/.tsbuildinfo +1 -1
  149. package/core/dist/index.d.ts +21 -0
  150. package/core/dist/index.js +57 -0
  151. package/dist/.tsbuildinfo +1 -1
  152. package/dist/backend.d.ts +15 -6
  153. package/dist/backend.js +3 -6
  154. package/dist/clients/createSpiceAsyncFromTransport.d.ts +5 -0
  155. package/dist/clients/createSpiceAsyncFromTransport.js +90 -0
  156. package/dist/clients/createSpiceSyncFromTransport.d.ts +5 -0
  157. package/dist/clients/createSpiceSyncFromTransport.js +88 -0
  158. package/dist/clients/spiceClients.d.ts +59 -0
  159. package/dist/clients/spiceClients.js +292 -0
  160. package/dist/errors.d.ts +4 -0
  161. package/dist/errors.js +4 -0
  162. package/dist/index.d.ts +12 -7
  163. package/dist/index.js +5 -2
  164. package/dist/kernels/defaultKernelPathFromUrl.d.ts +8 -0
  165. package/dist/kernels/defaultKernelPathFromUrl.js +32 -0
  166. package/dist/kernels/kernelPack.d.ts +88 -0
  167. package/dist/kernels/kernelPack.js +122 -0
  168. package/dist/kernels/kernels.d.ts +98 -0
  169. package/dist/kernels/kernels.js +217 -0
  170. package/dist/kernels/naifKernelId.d.ts +2 -0
  171. package/dist/kernels/naifKernelId.js +2 -0
  172. package/dist/kit/index.d.ts +4 -0
  173. package/dist/kit/index.js +3 -0
  174. package/dist/kit/math/mat3.d.ts +31 -0
  175. package/dist/kit/math/mat3.js +82 -0
  176. package/dist/kit/spice/create-kit.d.ts +12 -0
  177. package/dist/kit/spice/create-kit.js +23 -0
  178. package/dist/kit/spice/frames.d.ts +8 -0
  179. package/dist/kit/spice/frames.js +16 -0
  180. package/dist/kit/spice/kernels.d.ts +14 -0
  181. package/dist/kit/spice/kernels.js +39 -0
  182. package/dist/kit/spice/state.d.ts +7 -0
  183. package/dist/kit/spice/state.js +36 -0
  184. package/dist/kit/spice/time.d.ts +9 -0
  185. package/dist/kit/spice/time.js +31 -0
  186. package/dist/kit/types/spice-types.d.ts +51 -0
  187. package/dist/spice.d.ts +10 -1
  188. package/dist/spice.js +84 -72
  189. package/dist/transport/caching/policy.d.ts +16 -0
  190. package/dist/transport/caching/policy.js +77 -0
  191. package/dist/transport/caching/withCaching.d.ts +125 -0
  192. package/dist/transport/caching/withCaching.js +335 -0
  193. package/dist/transport/caching/withCachingSync.d.ts +24 -0
  194. package/dist/transport/caching/withCachingSync.js +161 -0
  195. package/dist/transport/rpc/protocol.d.ts +35 -0
  196. package/dist/transport/rpc/protocol.js +56 -0
  197. package/dist/transport/rpc/taskScheduling.d.ts +20 -0
  198. package/dist/transport/rpc/taskScheduling.js +98 -0
  199. package/dist/transport/rpc/valueCodec.d.ts +5 -0
  200. package/dist/transport/rpc/valueCodec.js +106 -0
  201. package/dist/transport/types.d.ts +7 -0
  202. package/dist/transport/types.js +2 -0
  203. package/dist/types.d.ts +8 -17
  204. package/dist/types.js +2 -1
  205. package/dist/worker/browser/createSpiceWorker.d.ts +22 -0
  206. package/dist/worker/browser/createSpiceWorker.js +41 -0
  207. package/dist/worker/browser/createSpiceWorkerClient.d.ts +40 -0
  208. package/dist/worker/browser/createSpiceWorkerClient.js +99 -0
  209. package/dist/worker/browser/spiceWorkerEntry.d.ts +2 -0
  210. package/dist/worker/browser/spiceWorkerEntry.js +129 -0
  211. package/dist/worker/browser/spiceWorkerInlineSource.d.ts +2 -0
  212. package/dist/worker/browser/spiceWorkerInlineSource.js +4 -0
  213. package/dist/worker/index.d.ts +10 -0
  214. package/dist/worker/index.js +7 -0
  215. package/dist/worker/transport/createWorkerTransport.d.ts +69 -0
  216. package/dist/worker/transport/createWorkerTransport.js +398 -0
  217. package/dist/worker/transport/exposeTransportToWorker.d.ts +51 -0
  218. package/dist/worker/transport/exposeTransportToWorker.js +196 -0
  219. package/package.json +4 -4
  220. package/dist/spice-types.d.ts +0 -36
  221. /package/dist/{spice-types.js → kit/types/spice-types.js} +0 -0
@@ -1,3 +1,4 @@
1
+ import { assertGetmsgWhich, assertSpiceInt32, brandMat3RowMajor, kxtrctJs, matchesKernelKind, normalizeBodItem, normalizeKindInput, } from "#backend-contract";
1
2
  /**
2
3
  * A deterministic, pure-TS "toy" backend.
3
4
  *
@@ -128,13 +129,27 @@ function angleBetween(a, b) {
128
129
  const c = clamp(vdot(a, b) / (na * nb), -1, 1);
129
130
  return Math.acos(c);
130
131
  }
132
+ function ellipsoidSurfaceNormal(spoint, radii) {
133
+ const [x, y, z] = spoint;
134
+ const [a, b, c] = radii;
135
+ // Gradient of x^2/a^2 + y^2/b^2 + z^2/c^2 = 1 is [x/a^2, y/b^2, z/c^2]
136
+ if (!Number.isFinite(a) || !Number.isFinite(b) || !Number.isFinite(c) || a === 0 || b === 0 || c === 0) {
137
+ // If we don't have radii, fall back to the sphere assumption.
138
+ return vhat(spoint);
139
+ }
140
+ return vhat([
141
+ x / (a * a),
142
+ y / (b * b),
143
+ z / (c * c),
144
+ ]);
145
+ }
131
146
  function canonicalizeZero(n) {
132
147
  return Object.is(n, -0) ? 0 : n;
133
148
  }
134
149
  function rotZRowMajor(theta) {
135
150
  const c = Math.cos(theta);
136
151
  const s = Math.sin(theta);
137
- return [
152
+ return brandMat3RowMajor([
138
153
  canonicalizeZero(c),
139
154
  canonicalizeZero(-s),
140
155
  0,
@@ -144,13 +159,13 @@ function rotZRowMajor(theta) {
144
159
  0,
145
160
  0,
146
161
  1,
147
- ];
162
+ ], { label: "fake.rotZRowMajor" });
148
163
  }
149
164
  function drotZRowMajor(theta, w) {
150
165
  // d/dt rotZ(theta) = w * d/dtheta rotZ(theta)
151
166
  const c = Math.cos(theta);
152
167
  const s = Math.sin(theta);
153
- return [
168
+ return brandMat3RowMajor([
154
169
  canonicalizeZero(-w * s),
155
170
  canonicalizeZero(-w * c),
156
171
  0,
@@ -160,7 +175,7 @@ function drotZRowMajor(theta, w) {
160
175
  0,
161
176
  0,
162
177
  0,
163
- ];
178
+ ], { label: "fake.drotZRowMajor" });
164
179
  }
165
180
  function mmul3(a, b) {
166
181
  // Row-major 3x3 multiply: out = a*b
@@ -174,20 +189,147 @@ function mmul3(a, b) {
174
189
  out[r * 3 + c] = sum;
175
190
  }
176
191
  }
177
- return out;
192
+ // Return a real tuple (and brand) instead of exposing a mutable `number[]`.
193
+ return brandMat3RowMajor([out[0], out[1], out[2], out[3], out[4], out[5], out[6], out[7], out[8]], {
194
+ label: "fake.mmul3",
195
+ });
196
+ }
197
+ function rotateRowMajor(angle, axis) {
198
+ // CSPICE reduces `iaxis` mod 3 (treating 0 as 3). Mirror that for parity.
199
+ if (!Number.isFinite(axis) || !Number.isInteger(axis)) {
200
+ throw new Error(`Fake backend: rotate(): invalid axis: ${axis} (expected a finite integer)`);
201
+ }
202
+ const iaxis = axis;
203
+ const reduced = ((iaxis % 3) + 3) % 3;
204
+ const spiceAxis = (reduced === 0 ? 3 : reduced);
205
+ const c = Math.cos(angle);
206
+ const s = Math.sin(angle);
207
+ switch (spiceAxis) {
208
+ case 1:
209
+ return brandMat3RowMajor([
210
+ 1,
211
+ 0,
212
+ 0,
213
+ 0,
214
+ canonicalizeZero(c),
215
+ canonicalizeZero(-s),
216
+ 0,
217
+ canonicalizeZero(s),
218
+ canonicalizeZero(c),
219
+ ], { label: "fake.rotate" });
220
+ case 2:
221
+ return brandMat3RowMajor([
222
+ canonicalizeZero(c),
223
+ 0,
224
+ canonicalizeZero(s),
225
+ 0,
226
+ 1,
227
+ 0,
228
+ canonicalizeZero(-s),
229
+ 0,
230
+ canonicalizeZero(c),
231
+ ], { label: "fake.rotate" });
232
+ case 3:
233
+ return brandMat3RowMajor([
234
+ canonicalizeZero(c),
235
+ canonicalizeZero(-s),
236
+ 0,
237
+ canonicalizeZero(s),
238
+ canonicalizeZero(c),
239
+ 0,
240
+ 0,
241
+ 0,
242
+ 1,
243
+ ], { label: "fake.rotate" });
244
+ default: {
245
+ const _exhaustive = spiceAxis;
246
+ throw new Error(`Fake backend: rotate(): unreachable axis: ${String(_exhaustive)}`);
247
+ }
248
+ }
249
+ }
250
+ function axisAngleToRotationRowMajor(axis, angle) {
251
+ const u = vhat(axis);
252
+ const ux = u[0];
253
+ const uy = u[1];
254
+ const uz = u[2];
255
+ // Parity with CSPICE axisar_c: if axis is zero, return identity rotation.
256
+ if (ux === 0 && uy === 0 && uz === 0) {
257
+ return brandMat3RowMajor([1, 0, 0, 0, 1, 0, 0, 0, 1], { label: "fake.axisar" });
258
+ }
259
+ const c = Math.cos(angle);
260
+ const s = Math.sin(angle);
261
+ const t = 1 - c;
262
+ return brandMat3RowMajor([
263
+ canonicalizeZero(t * ux * ux + c),
264
+ canonicalizeZero(t * ux * uy - s * uz),
265
+ canonicalizeZero(t * ux * uz + s * uy),
266
+ canonicalizeZero(t * ux * uy + s * uz),
267
+ canonicalizeZero(t * uy * uy + c),
268
+ canonicalizeZero(t * uy * uz - s * ux),
269
+ canonicalizeZero(t * ux * uz - s * uy),
270
+ canonicalizeZero(t * uy * uz + s * ux),
271
+ canonicalizeZero(t * uz * uz + c),
272
+ ], { label: "fake.axisar" });
273
+ }
274
+ function georec(lon, lat, alt, re, f) {
275
+ // Match CSPICE validation semantics where feasible.
276
+ if (!Number.isFinite(re) || re <= 0) {
277
+ throw new Error(`Fake backend: georec(): invalid re: ${re} (expected > 0)`);
278
+ }
279
+ if (!Number.isFinite(f) || f >= 1) {
280
+ throw new Error(`Fake backend: georec(): invalid f: ${f} (expected < 1)`);
281
+ }
282
+ // Standard geodetic-to-rectangular conversion.
283
+ const rp = re * (1 - f);
284
+ const e2 = 1 - (rp * rp) / (re * re);
285
+ const slat = Math.sin(lat);
286
+ const clat = Math.cos(lat);
287
+ const slon = Math.sin(lon);
288
+ const clon = Math.cos(lon);
289
+ const n = re / Math.sqrt(1 - e2 * slat * slat);
290
+ const x = (n + alt) * clat * clon;
291
+ const y = (n + alt) * clat * slon;
292
+ const z = (n * (1 - e2) + alt) * slat;
293
+ return [x, y, z];
294
+ }
295
+ function recgeo(rect, re, f) {
296
+ // Match CSPICE validation semantics where feasible.
297
+ if (!Number.isFinite(re) || re <= 0) {
298
+ throw new Error(`Fake backend: recgeo(): invalid re: ${re} (expected > 0)`);
299
+ }
300
+ if (!Number.isFinite(f) || f >= 1) {
301
+ throw new Error(`Fake backend: recgeo(): invalid f: ${f} (expected < 1)`);
302
+ }
303
+ // Bowring's method (non-iterative) for rectangular to geodetic.
304
+ const x = rect[0];
305
+ const y = rect[1];
306
+ const z = rect[2];
307
+ const rp = re * (1 - f);
308
+ const e2 = 1 - (rp * rp) / (re * re);
309
+ const ep2 = (re * re - rp * rp) / (rp * rp);
310
+ const lon = Math.atan2(y, x);
311
+ const p = Math.sqrt(x * x + y * y);
312
+ // Handle poles / near-pole numerical stability.
313
+ // Avoid the general-case `alt = p / cos(lat) - n` path when `p` is tiny.
314
+ // (Near-pole cases are extremely sensitive as `cos(lat) -> 0`.)
315
+ const poleTol = 1e-14 * re;
316
+ if (p <= poleTol) {
317
+ const poleLon = 0;
318
+ const poleLat = z >= 0 ? Math.PI / 2 : -Math.PI / 2;
319
+ const poleAlt = Math.abs(z) - rp;
320
+ return { lon: poleLon, lat: poleLat, alt: poleAlt };
321
+ }
322
+ const theta = Math.atan2(z * re, p * rp);
323
+ const st = Math.sin(theta);
324
+ const ct = Math.cos(theta);
325
+ const lat = Math.atan2(z + ep2 * rp * st * st * st, p - e2 * re * ct * ct * ct);
326
+ const slat = Math.sin(lat);
327
+ const n = re / Math.sqrt(1 - e2 * slat * slat);
328
+ const alt = p / Math.cos(lat) - n;
329
+ return { lon, lat, alt };
178
330
  }
179
331
  function mtx3(m) {
180
- return [
181
- m[0],
182
- m[3],
183
- m[6],
184
- m[1],
185
- m[4],
186
- m[7],
187
- m[2],
188
- m[5],
189
- m[8],
190
- ];
332
+ return brandMat3RowMajor([m[0], m[3], m[6], m[1], m[4], m[7], m[2], m[5], m[8]], { label: "fake.mtx3" });
191
333
  }
192
334
  function mxv(m, v) {
193
335
  return [
@@ -309,14 +451,18 @@ function formatUtcFromMs(ms, prec) {
309
451
  const padded = (frac + "000000000000").slice(0, clampedPrec);
310
452
  return `${head}.${padded}Z`;
311
453
  }
312
- function guessKernelKind(path) {
454
+ function guessKernelKind(path, unknownExtension) {
313
455
  const lower = path.toLowerCase();
314
456
  if (lower.endsWith(".bsp"))
315
457
  return "SPK";
316
458
  if (lower.endsWith(".bc"))
317
459
  return "CK";
318
- if (lower.endsWith(".tpc") || lower.endsWith(".pck"))
460
+ if (lower.endsWith(".bpc"))
319
461
  return "PCK";
462
+ if (lower.endsWith(".bds") || lower.endsWith(".dsk"))
463
+ return "DSK";
464
+ if (lower.endsWith(".tpc") || lower.endsWith(".pck"))
465
+ return "TEXT";
320
466
  if (lower.endsWith(".tls") || lower.endsWith(".lsk"))
321
467
  return "LSK";
322
468
  if (lower.endsWith(".tf") || lower.endsWith(".fk"))
@@ -325,9 +471,18 @@ function guessKernelKind(path) {
325
471
  return "IK";
326
472
  if (lower.endsWith(".tsc") || lower.endsWith(".sclk"))
327
473
  return "SCLK";
474
+ if (lower.endsWith(".ek"))
475
+ return "EK";
328
476
  if (lower.endsWith(".tm") || lower.endsWith(".meta"))
329
477
  return "META";
330
- return "ALL";
478
+ // "ALL" is a query token, not a per-kernel kind.
479
+ if (unknownExtension === "assume-text") {
480
+ return "TEXT";
481
+ }
482
+ throw new RangeError(`Unsupported kernel extension (fake backend): ${path}`);
483
+ }
484
+ function assertNever(x, msg) {
485
+ throw new Error(msg);
331
486
  }
332
487
  function kernelFiltyp(kind) {
333
488
  // Keep this close to NAIF-style strings, but it doesn't need to be exact.
@@ -335,33 +490,234 @@ function kernelFiltyp(kind) {
335
490
  case "SPK":
336
491
  case "CK":
337
492
  case "PCK":
493
+ case "DSK":
494
+ case "EK":
495
+ case "META":
496
+ case "TEXT":
497
+ return kind;
338
498
  case "LSK":
339
499
  case "FK":
340
500
  case "IK":
341
501
  case "SCLK":
342
- case "EK":
343
- case "META":
344
- return kind;
345
- case "ALL":
346
- default:
347
- return "ALL";
502
+ return "TEXT";
348
503
  }
504
+ // Compile-time exhaustiveness check: if a new KernelKind is added, TypeScript
505
+ // forces us to intentionally map it.
506
+ return assertNever(kind, `Unmapped KernelKind: ${kind}`);
349
507
  }
350
- export function createFakeBackend() {
508
+ function normalizeVirtualKernelIdOrNull(input) {
509
+ // Keep lookup semantics aligned with the WASM backend's virtual-id
510
+ // normalization (see core's `normalizeVirtualKernelPath`).
511
+ //
512
+ // Unlike the core helper, this is non-throwing: invalid inputs just don't
513
+ // match.
514
+ const raw = input.replace(/\\/g, "/").trim();
515
+ if (!raw) {
516
+ return null;
517
+ }
518
+ // Strip leading slashes so `/kernels/foo.tls` behaves like `kernels/foo.tls`.
519
+ let rel = raw.replace(/^\/+/, "");
520
+ // Strip leading `./` segments.
521
+ while (rel.startsWith("./")) {
522
+ rel = rel.slice(2);
523
+ }
524
+ // Strip a leading `kernels/` directory to keep user input flexible.
525
+ // Treat a bare `kernels` segment as equivalent to `kernels/`.
526
+ if (rel === "kernels") {
527
+ rel = "";
528
+ }
529
+ while (rel.startsWith("kernels/")) {
530
+ rel = rel.replace(/^kernels\/+/, "");
531
+ }
532
+ const segments = rel.split("/");
533
+ const out = [];
534
+ for (const seg of segments) {
535
+ if (!seg || seg === ".") {
536
+ continue;
537
+ }
538
+ if (seg === "..") {
539
+ return null;
540
+ }
541
+ out.push(seg);
542
+ }
543
+ if (out.length === 0) {
544
+ return null;
545
+ }
546
+ return out.join("/");
547
+ }
548
+ function assertPoolRange(fn, start, room) {
549
+ if (!Number.isFinite(start) || !Number.isInteger(start) || start < 0) {
550
+ throw new RangeError(`${fn}(): start must be an integer >= 0`);
551
+ }
552
+ if (!Number.isFinite(room) || !Number.isInteger(room) || room <= 0) {
553
+ throw new RangeError(`${fn}(): room must be an integer > 0`);
554
+ }
555
+ }
556
+ function assertNonEmptyString(fn, field, value) {
557
+ if (value.trim().length === 0) {
558
+ throw new RangeError(`${fn}(): ${field} must be a non-empty string`);
559
+ }
560
+ }
561
+ /**
562
+ * Create an in-memory fake backend implementing the {@link SpiceBackend} contract.
563
+ *
564
+ * Intended for tests and UI development without native/WASM backends.
565
+ */
566
+ export function createFakeBackend(options = {}) {
351
567
  let nextHandle = 1;
568
+ let spiceFailed = false;
569
+ let spiceShort = "";
570
+ let spiceLong = "";
571
+ const traceStack = [];
352
572
  const kernels = [];
573
+ const unknownExtension = options.unknownExtension ?? "throw";
574
+ const kernelPool = new Map();
575
+ // boddef() mappings (CSPICE uses process-global state; keep it per-backend here).
576
+ const customNameToBodyCode = new Map();
577
+ const customBodyCodeToName = new Map();
578
+ // swpool/cvpool "agent" state.
579
+ const kernelPoolWatches = new Map();
580
+ const kernelPoolWatchesByName = new Map();
581
+ const unindexKernelPoolWatch = (agent, names) => {
582
+ for (const name of names) {
583
+ const agents = kernelPoolWatchesByName.get(name);
584
+ if (!agents)
585
+ continue;
586
+ agents.delete(agent);
587
+ if (agents.size === 0) {
588
+ kernelPoolWatchesByName.delete(name);
589
+ }
590
+ }
591
+ };
592
+ const indexKernelPoolWatch = (agent, names) => {
593
+ for (const name of names) {
594
+ let agents = kernelPoolWatchesByName.get(name);
595
+ if (!agents) {
596
+ agents = new Set();
597
+ kernelPoolWatchesByName.set(name, agents);
598
+ }
599
+ agents.add(agent);
600
+ }
601
+ };
602
+ const markKernelPoolUpdated = (name) => {
603
+ // Avoid an O(watches * names) scan by maintaining a reverse index.
604
+ const agents = kernelPoolWatchesByName.get(name);
605
+ if (!agents)
606
+ return;
607
+ for (const agent of agents) {
608
+ const watch = kernelPoolWatches.get(agent);
609
+ if (watch) {
610
+ watch.dirty = true;
611
+ }
612
+ }
613
+ };
614
+ const spiceCellUnsupported = "Fake backend does not support SpiceCell/SpiceWindow APIs (use wasm/node backend).";
353
615
  const getKernelsOfKind = (kind) => {
354
- if (kind === "ALL")
355
- return kernels;
356
- return kernels.filter((k) => k.kind === kind);
616
+ const requested = new Set(normalizeKindInput(kind));
617
+ return kernels.filter((k) => matchesKernelKind(requested, k));
618
+ };
619
+ // Minimal state for timdef() GET/SET.
620
+ //
621
+ // The fake backend treats time systems deterministically and does not model
622
+ // leap seconds, so these defaults are mostly for parity with the contract
623
+ // and to support callers that expect timdef() to exist.
624
+ const DEFAULT_TIME_DEFAULTS = [
625
+ ["SYSTEM", "UTC"],
626
+ ["CALENDAR", "GREGORIAN"],
627
+ ["ZONE", "UTC"],
628
+ ];
629
+ const timeDefaults = new Map(DEFAULT_TIME_DEFAULTS);
630
+ function resetTimeDefaults() {
631
+ timeDefaults.clear();
632
+ for (const [k, v] of DEFAULT_TIME_DEFAULTS) {
633
+ timeDefaults.set(k, v);
634
+ }
635
+ }
636
+ function timdef(action, item, value) {
637
+ assertNonEmptyString("timdef", "item", item);
638
+ if (action === "GET") {
639
+ return timeDefaults.get(item) ?? "";
640
+ }
641
+ if (typeof value !== "string") {
642
+ throw new TypeError("timdef(SET) requires a string value");
643
+ }
644
+ // Match CSPICE `timdef_c`: empty (length 0) strings are invalid, but
645
+ // whitespace-only strings are allowed (e.g. ZONE can be "blank").
646
+ if (value.length === 0) {
647
+ throw new RangeError("timdef(SET): value must be a non-empty string");
648
+ }
649
+ timeDefaults.set(item, value);
650
+ }
651
+ const getBodyRadiiKm = (bodyId) => {
652
+ const key = `BODY${bodyId}_RADII`;
653
+ const entry = kernelPool.get(key);
654
+ if (entry?.type === "N" && entry.values.length >= 3) {
655
+ const a = entry.values[0];
656
+ const b = entry.values[1];
657
+ const c = entry.values[2];
658
+ if (typeof a === "number" &&
659
+ typeof b === "number" &&
660
+ typeof c === "number" &&
661
+ Number.isFinite(a) &&
662
+ Number.isFinite(b) &&
663
+ Number.isFinite(c)) {
664
+ return [a, b, c];
665
+ }
666
+ }
667
+ // Fall back to a spherical body when RADII isn't available.
668
+ const r = getBodyRadiusKm(bodyId);
669
+ return [r, r, r];
357
670
  };
358
- return {
671
+ const backend = {
359
672
  kind: "fake",
360
673
  spiceVersion: () => FAKE_SPICE_VERSION,
674
+ failed: () => spiceFailed,
675
+ reset: () => {
676
+ spiceFailed = false;
677
+ spiceShort = "";
678
+ spiceLong = "";
679
+ traceStack.length = 0;
680
+ resetTimeDefaults();
681
+ },
682
+ getmsg: (which) => {
683
+ assertGetmsgWhich(which);
684
+ if (which === "SHORT")
685
+ return spiceShort;
686
+ if (which === "LONG")
687
+ return spiceLong;
688
+ // EXPLAIN
689
+ // CSPICE convention is typically:
690
+ // setmsg(long)
691
+ // sigerr(short)
692
+ // so we avoid overwriting the long message when signaling.
693
+ const trace = traceStack.length > 0 ? traceStack.join(" -> ") : "";
694
+ if (!spiceLong && !trace)
695
+ return "";
696
+ if (spiceLong && !trace)
697
+ return spiceLong;
698
+ if (!spiceLong && trace)
699
+ return `Trace: ${trace}`;
700
+ return `${spiceLong}\n\nTrace: ${trace}`;
701
+ },
702
+ setmsg: (message) => {
703
+ spiceLong = message;
704
+ },
705
+ sigerr: (short) => {
706
+ spiceFailed = true;
707
+ spiceShort = short;
708
+ },
709
+ chkin: (name) => {
710
+ traceStack.push(name);
711
+ },
712
+ chkout: (name) => {
713
+ const idx = traceStack.lastIndexOf(name);
714
+ if (idx >= 0)
715
+ traceStack.splice(idx, 1);
716
+ },
361
717
  furnsh: (kernel) => {
362
718
  const file = typeof kernel === "string" ? kernel : kernel.path;
363
719
  const source = typeof kernel === "string" ? file : "bytes";
364
- const kind = guessKernelKind(file);
720
+ const kind = guessKernelKind(file, unknownExtension);
365
721
  const handle = nextHandle++;
366
722
  kernels.push({
367
723
  file,
@@ -372,13 +728,42 @@ export function createFakeBackend() {
372
728
  });
373
729
  },
374
730
  unload: (path) => {
375
- const idx = kernels.findIndex((k) => k.file === path);
731
+ const needle = normalizeVirtualKernelIdOrNull(path);
732
+ if (needle == null) {
733
+ return;
734
+ }
735
+ const idx = kernels.findIndex((k) => normalizeVirtualKernelIdOrNull(k.file) === needle);
376
736
  if (idx >= 0) {
377
737
  kernels.splice(idx, 1);
378
738
  }
379
739
  },
380
740
  kclear: () => {
381
741
  kernels.length = 0;
742
+ kernelPool.clear();
743
+ kernelPoolWatches.clear();
744
+ kernelPoolWatchesByName.clear();
745
+ },
746
+ kinfo: (path) => {
747
+ const needle = normalizeVirtualKernelIdOrNull(path);
748
+ if (needle == null) {
749
+ return { found: false };
750
+ }
751
+ const k = kernels.find((k) => normalizeVirtualKernelIdOrNull(k.file) === needle);
752
+ if (!k) {
753
+ return { found: false };
754
+ }
755
+ return {
756
+ found: true,
757
+ filtyp: k.filtyp,
758
+ source: k.source,
759
+ handle: k.handle,
760
+ };
761
+ },
762
+ kxtrct: (keywd, terms, wordsq) => {
763
+ return kxtrctJs(keywd, terms, wordsq);
764
+ },
765
+ kplfrm: (_frmcls, _idset) => {
766
+ throw new Error(spiceCellUnsupported);
382
767
  },
383
768
  ktotal: (kind = "ALL") => {
384
769
  return getKernelsOfKind(kind).length;
@@ -396,6 +781,163 @@ export function createFakeBackend() {
396
781
  handle: k.handle,
397
782
  };
398
783
  },
784
+ gdpool: (name, start, room) => {
785
+ assertNonEmptyString("gdpool", "name", name);
786
+ assertPoolRange("gdpool", start, room);
787
+ const start0 = start;
788
+ const room0 = room;
789
+ const entry = kernelPool.get(name);
790
+ if (!entry)
791
+ return { found: false };
792
+ if (entry.type !== "N") {
793
+ throw new Error(`Fake backend: gdpool only supports numeric variables (got ${entry.type})`);
794
+ }
795
+ return {
796
+ found: true,
797
+ values: entry.values.slice(start0, start0 + room0),
798
+ };
799
+ },
800
+ gipool: (name, start, room) => {
801
+ assertNonEmptyString("gipool", "name", name);
802
+ assertPoolRange("gipool", start, room);
803
+ const start0 = start;
804
+ const room0 = room;
805
+ const entry = kernelPool.get(name);
806
+ if (!entry)
807
+ return { found: false };
808
+ if (entry.type !== "N") {
809
+ throw new Error(`Fake backend: gipool only supports numeric variables (got ${entry.type})`);
810
+ }
811
+ return {
812
+ found: true,
813
+ values: entry.values.slice(start0, start0 + room0).map((v, i) => {
814
+ assertSpiceInt32(v, `gipool(): values[${start0 + i}]`);
815
+ return v;
816
+ }),
817
+ };
818
+ },
819
+ gcpool: (name, start, room) => {
820
+ assertNonEmptyString("gcpool", "name", name);
821
+ assertPoolRange("gcpool", start, room);
822
+ const start0 = start;
823
+ const room0 = room;
824
+ const entry = kernelPool.get(name);
825
+ if (!entry)
826
+ return { found: false };
827
+ if (entry.type !== "C") {
828
+ throw new Error(`Fake backend: gcpool only supports character variables (got ${entry.type})`);
829
+ }
830
+ return {
831
+ found: true,
832
+ values: entry.values.slice(start0, start0 + room0),
833
+ };
834
+ },
835
+ gnpool: (template, start, room) => {
836
+ assertNonEmptyString("gnpool", "template", template);
837
+ assertPoolRange("gnpool", start, room);
838
+ const start0 = start;
839
+ const room0 = room;
840
+ const escapeRegex = (s) => s.replace(/[.*+?^${}()|[\[\]\\]/g, "\\$&");
841
+ // Support escaping wildcard characters via backslash:
842
+ // - `\*` matches a literal `*`
843
+ // - `\%` matches a literal `%`
844
+ let reSrc = "^";
845
+ for (let i = 0; i < template.length; i++) {
846
+ const ch = template[i];
847
+ if (ch === "\\") {
848
+ const next = template[i + 1];
849
+ if (next !== undefined) {
850
+ reSrc += escapeRegex(next);
851
+ i++;
852
+ }
853
+ else {
854
+ reSrc += "\\\\";
855
+ }
856
+ continue;
857
+ }
858
+ if (ch === "*") {
859
+ reSrc += ".*";
860
+ continue;
861
+ }
862
+ if (ch === "%") {
863
+ reSrc += ".";
864
+ continue;
865
+ }
866
+ reSrc += escapeRegex(ch);
867
+ }
868
+ reSrc += "$";
869
+ const re = new RegExp(reSrc);
870
+ const matches = Array.from(kernelPool.keys()).filter((k) => re.test(k)).sort();
871
+ if (matches.length === 0) {
872
+ return { found: false };
873
+ }
874
+ return {
875
+ found: true,
876
+ values: matches.slice(start0, start0 + room0),
877
+ };
878
+ },
879
+ dtpool: (name) => {
880
+ assertNonEmptyString("dtpool", "name", name);
881
+ const entry = kernelPool.get(name);
882
+ if (!entry)
883
+ return { found: false };
884
+ return {
885
+ found: true,
886
+ n: entry.values.length,
887
+ type: entry.type,
888
+ };
889
+ },
890
+ pdpool: (name, values) => {
891
+ assertNonEmptyString("pdpool", "name", name);
892
+ for (let i = 0; i < values.length; i++) {
893
+ const v = values[i];
894
+ if (!Number.isFinite(v)) {
895
+ throw new RangeError(`pdpool(): values[${i}] must be a finite number (got ${String(v)})`);
896
+ }
897
+ }
898
+ kernelPool.set(name, { type: "N", values: [...values] });
899
+ markKernelPoolUpdated(name);
900
+ },
901
+ pipool: (name, values) => {
902
+ assertNonEmptyString("pipool", "name", name);
903
+ for (let i = 0; i < values.length; i++) {
904
+ assertSpiceInt32(values[i], `pipool(): values[${i}]`);
905
+ }
906
+ kernelPool.set(name, { type: "N", values: [...values] });
907
+ markKernelPoolUpdated(name);
908
+ },
909
+ pcpool: (name, values) => {
910
+ assertNonEmptyString("pcpool", "name", name);
911
+ kernelPool.set(name, { type: "C", values: [...values] });
912
+ markKernelPoolUpdated(name);
913
+ },
914
+ swpool: (agent, names) => {
915
+ assertNonEmptyString("swpool", "agent", agent);
916
+ for (let i = 0; i < names.length; i++) {
917
+ assertNonEmptyString("swpool", `names[${i}]`, names[i]);
918
+ }
919
+ // CSPICE guarantees the next cvpool(agent) returns true.
920
+ const prev = kernelPoolWatches.get(agent);
921
+ if (prev) {
922
+ unindexKernelPoolWatch(agent, prev.names);
923
+ }
924
+ kernelPoolWatches.set(agent, { names: [...names], dirty: true });
925
+ indexKernelPoolWatch(agent, names);
926
+ },
927
+ cvpool: (agent) => {
928
+ assertNonEmptyString("cvpool", "agent", agent);
929
+ const watch = kernelPoolWatches.get(agent);
930
+ if (!watch)
931
+ return false;
932
+ const dirty = watch.dirty;
933
+ watch.dirty = false;
934
+ return dirty;
935
+ },
936
+ expool: (name) => {
937
+ assertNonEmptyString("expool", "name", name);
938
+ const entry = kernelPool.get(name);
939
+ return entry?.type === "N";
940
+ },
399
941
  tkvrsn: (item) => {
400
942
  if (item !== "TOOLKIT") {
401
943
  throw new Error(`Fake backend: unsupported tkvrsn item: ${String(item)}`);
@@ -421,19 +963,102 @@ export function createFakeBackend() {
421
963
  const ms = J2000_UTC_MS + et * 1000;
422
964
  return formatUtcFromMs(ms, 3);
423
965
  },
966
+ deltet: (_epoch, _eptype) => {
967
+ // Deterministic stub: the fake backend ignores leap seconds, so ET and
968
+ // UTC are treated as identical.
969
+ return 0;
970
+ },
971
+ unitim: (epoch, _insys, _outsys) => {
972
+ // Deterministic stub: treat all input/output systems as identical.
973
+ return epoch;
974
+ },
975
+ tparse: (timstr) => {
976
+ // Deterministic stub: match `str2et` semantics in the fake backend.
977
+ if (!isIso8601OrRfc3339Utcish(timstr)) {
978
+ throw new Error(`Fake backend: tparse() only supports ISO-8601/RFC3339 timestamps (got ${JSON.stringify(timstr)})`);
979
+ }
980
+ const ms = Date.parse(timstr);
981
+ if (!Number.isFinite(ms)) {
982
+ throw new Error(`Fake backend: failed to parse time: ${JSON.stringify(timstr)}`);
983
+ }
984
+ return (ms - J2000_UTC_MS) / 1000;
985
+ },
986
+ tpictr: (sample, pictur) => {
987
+ if (sample.length === 0) {
988
+ throw new RangeError("tpictr(): sample must be a non-empty string");
989
+ }
990
+ if (pictur.length === 0) {
991
+ throw new RangeError("tpictr(): pictur must be a non-empty string");
992
+ }
993
+ // Picture transformation is out of scope for the fake backend.
994
+ return pictur;
995
+ },
996
+ timdef,
424
997
  bodn2c: (name) => {
425
998
  const trimmed = normalizeName(name);
999
+ const custom = customNameToBodyCode.get(trimmed) ??
1000
+ customNameToBodyCode.get(trimmed.toLowerCase());
1001
+ if (custom !== undefined)
1002
+ return { found: true, code: custom };
426
1003
  const id = NAME_TO_ID.get(trimmed) ?? NAME_TO_ID.get(trimmed.toLowerCase());
427
1004
  if (id === undefined)
428
1005
  return { found: false };
429
1006
  return { found: true, code: id };
430
1007
  },
431
1008
  bodc2n: (code) => {
1009
+ const custom = customBodyCodeToName.get(code);
1010
+ if (custom !== undefined)
1011
+ return { found: true, name: custom };
432
1012
  const meta = ID_TO_BODY.get(code);
433
1013
  if (!meta)
434
1014
  return { found: false };
435
1015
  return { found: true, name: meta.name };
436
1016
  },
1017
+ bodc2s: (code) => {
1018
+ const custom = customBodyCodeToName.get(code);
1019
+ if (custom !== undefined)
1020
+ return custom;
1021
+ const meta = ID_TO_BODY.get(code);
1022
+ if (meta)
1023
+ return meta.name;
1024
+ return String(code);
1025
+ },
1026
+ bods2c: (name) => {
1027
+ const trimmed = normalizeName(name);
1028
+ // Accept numeric IDs as strings.
1029
+ if (/^-?\d+$/.test(trimmed)) {
1030
+ return { found: true, code: Number(trimmed) };
1031
+ }
1032
+ const custom = customNameToBodyCode.get(trimmed) ??
1033
+ customNameToBodyCode.get(trimmed.toLowerCase());
1034
+ if (custom !== undefined)
1035
+ return { found: true, code: custom };
1036
+ const id = NAME_TO_ID.get(trimmed) ?? NAME_TO_ID.get(trimmed.toLowerCase());
1037
+ if (id === undefined)
1038
+ return { found: false };
1039
+ return { found: true, code: id };
1040
+ },
1041
+ boddef: (name, code) => {
1042
+ const trimmed = normalizeName(name);
1043
+ customNameToBodyCode.set(trimmed, code);
1044
+ customNameToBodyCode.set(trimmed.toLowerCase(), code);
1045
+ customBodyCodeToName.set(code, trimmed);
1046
+ },
1047
+ bodfnd: (body, item) => {
1048
+ const key = `BODY${body}_${normalizeBodItem(item)}`;
1049
+ const entry = kernelPool.get(key);
1050
+ return entry !== undefined;
1051
+ },
1052
+ bodvar: (body, item) => {
1053
+ const key = `BODY${body}_${normalizeBodItem(item)}`;
1054
+ const entry = kernelPool.get(key);
1055
+ if (!entry || entry.type !== "N") {
1056
+ // Align with the backend contract: missing / non-numeric pool vars are a normal miss.
1057
+ // Callers that need strict presence checks can use `bodfnd()`.
1058
+ return [];
1059
+ }
1060
+ return [...entry.values];
1061
+ },
437
1062
  namfrm: (name) => {
438
1063
  const trimmed = normalizeName(name);
439
1064
  const code = FRAME_NAME_TO_CODE.get(trimmed) ?? FRAME_NAME_TO_CODE.get(trimmed.toLowerCase());
@@ -466,6 +1091,21 @@ export function createFakeBackend() {
466
1091
  ? { found: true, frcode: FRAME_CODES.IAU_MOON, frname: "IAU_MOON" }
467
1092
  : { found: false });
468
1093
  },
1094
+ frinfo: (frameId) => {
1095
+ if (frameId === FRAME_CODES.J2000) {
1096
+ return { found: true, center: 0, frameClass: 1, classId: frameId };
1097
+ }
1098
+ return { found: false };
1099
+ },
1100
+ ccifrm: (frameClass, classId) => {
1101
+ if (frameClass === 1) {
1102
+ const frname = FRAME_CODE_TO_NAME.get(classId);
1103
+ if (!frname)
1104
+ return { found: false };
1105
+ return { found: true, frcode: classId, frname, center: 0 };
1106
+ }
1107
+ return { found: false };
1108
+ },
469
1109
  scs2e: (_sc, sclkch) => {
470
1110
  // Minimal deterministic stub: treat the string as a number of seconds.
471
1111
  const n = Number(sclkch);
@@ -475,12 +1115,41 @@ export function createFakeBackend() {
475
1115
  // Minimal deterministic stub.
476
1116
  return String(et);
477
1117
  },
1118
+ scencd: (_sc, sclkch) => {
1119
+ // Minimal deterministic stub: treat the string as a number of ticks.
1120
+ const n = Number(sclkch);
1121
+ return Number.isFinite(n) ? n : 0;
1122
+ },
1123
+ scdecd: (_sc, sclkdp) => {
1124
+ // Minimal deterministic stub.
1125
+ return String(sclkdp);
1126
+ },
1127
+ sct2e: (_sc, sclkdp) => {
1128
+ // Minimal deterministic stub.
1129
+ return sclkdp;
1130
+ },
1131
+ sce2c: (_sc, et) => {
1132
+ // Minimal deterministic stub.
1133
+ return et;
1134
+ },
478
1135
  ckgp: (_inst, _sclkdp, _tol, _ref) => {
479
1136
  return { found: false };
480
1137
  },
481
1138
  ckgpav: (_inst, _sclkdp, _tol, _ref) => {
482
1139
  return { found: false };
483
1140
  },
1141
+ cklpf: (_ck) => {
1142
+ throw new Error("Fake backend: cklpf() is not implemented");
1143
+ },
1144
+ ckupf: (_handle) => {
1145
+ throw new Error("Fake backend: ckupf() is not implemented");
1146
+ },
1147
+ ckobj: (_ck, _ids) => {
1148
+ throw new Error("Fake backend: ckobj() is not implemented");
1149
+ },
1150
+ ckcov: (_ck, _idcode, _needav, _level, _tol, _timsys, _cover) => {
1151
+ throw new Error("Fake backend: ckcov() is not implemented");
1152
+ },
484
1153
  pxform: (from, to, et) => {
485
1154
  const f = parseFrameName(from);
486
1155
  const t = parseFrameName(to);
@@ -519,6 +1188,197 @@ export function createFakeBackend() {
519
1188
  lt: 0,
520
1189
  };
521
1190
  },
1191
+ spkez: (target, et, ref, abcorr, observer) => {
1192
+ void abcorr;
1193
+ assertSpiceInt32(target, "spkez(target)");
1194
+ assertSpiceInt32(observer, "spkez(observer)");
1195
+ const stateJ2000 = getRelativeStateInJ2000(String(target), String(observer), et);
1196
+ const outFrame = parseFrameName(ref);
1197
+ const state = outFrame === "J2000" ? stateJ2000 : applyStateTransform("J2000", outFrame, et, stateJ2000);
1198
+ return { state, lt: 0 };
1199
+ },
1200
+ spkezp: (target, et, ref, abcorr, observer) => {
1201
+ void abcorr;
1202
+ assertSpiceInt32(target, "spkezp(target)");
1203
+ assertSpiceInt32(observer, "spkezp(observer)");
1204
+ const stateJ2000 = getRelativeStateInJ2000(String(target), String(observer), et);
1205
+ const outFrame = parseFrameName(ref);
1206
+ const state = outFrame === "J2000" ? stateJ2000 : applyStateTransform("J2000", outFrame, et, stateJ2000);
1207
+ return {
1208
+ pos: [state[0], state[1], state[2]],
1209
+ lt: 0,
1210
+ };
1211
+ },
1212
+ spkgeo: (target, et, ref, observer) => {
1213
+ assertSpiceInt32(target, "spkgeo(target)");
1214
+ assertSpiceInt32(observer, "spkgeo(observer)");
1215
+ const stateJ2000 = getRelativeStateInJ2000(String(target), String(observer), et);
1216
+ const outFrame = parseFrameName(ref);
1217
+ const state = outFrame === "J2000" ? stateJ2000 : applyStateTransform("J2000", outFrame, et, stateJ2000);
1218
+ return { state, lt: 0 };
1219
+ },
1220
+ spkgps: (target, et, ref, observer) => {
1221
+ assertSpiceInt32(target, "spkgps(target)");
1222
+ assertSpiceInt32(observer, "spkgps(observer)");
1223
+ const stateJ2000 = getRelativeStateInJ2000(String(target), String(observer), et);
1224
+ const outFrame = parseFrameName(ref);
1225
+ const state = outFrame === "J2000" ? stateJ2000 : applyStateTransform("J2000", outFrame, et, stateJ2000);
1226
+ return {
1227
+ pos: [state[0], state[1], state[2]],
1228
+ lt: 0,
1229
+ };
1230
+ },
1231
+ spkssb: (target, et, ref) => {
1232
+ assertSpiceInt32(target, "spkssb(target)");
1233
+ const abs = getAbsoluteStateInJ2000(target, et);
1234
+ const stateJ2000 = [
1235
+ abs.posKm[0],
1236
+ abs.posKm[1],
1237
+ abs.posKm[2],
1238
+ abs.velKmPerSec[0],
1239
+ abs.velKmPerSec[1],
1240
+ abs.velKmPerSec[2],
1241
+ ];
1242
+ const outFrame = parseFrameName(ref);
1243
+ return outFrame === "J2000" ? stateJ2000 : applyStateTransform("J2000", outFrame, et, stateJ2000);
1244
+ },
1245
+ spkcov: (_spk, idcode, _cover) => {
1246
+ assertSpiceInt32(idcode, "spkcov(idcode)");
1247
+ throw new Error(spiceCellUnsupported);
1248
+ },
1249
+ spkobj: (_spk, _ids) => {
1250
+ throw new Error(spiceCellUnsupported);
1251
+ },
1252
+ illumg: (_method, target, ilusrc, et, fixref, abcorr, observer, spoint) => {
1253
+ void abcorr;
1254
+ const frame = parseFrameName(fixref);
1255
+ const inv = rotZRowMajor(FRAME_SPIN_RATE_RAD_PER_SEC[frame] * et);
1256
+ const spointJ = frame === "J2000" ? spoint : mxv(inv, spoint);
1257
+ const srcState = getRelativeStateInJ2000(ilusrc, target, et);
1258
+ const srcPosJ = [srcState[0], srcState[1], srcState[2]];
1259
+ const obsState = getRelativeStateInJ2000(observer, target, et);
1260
+ const obsPosJ = [obsState[0], obsState[1], obsState[2]];
1261
+ // Vectors from surface point.
1262
+ const srfToSrcJ = vsub(srcPosJ, spointJ);
1263
+ const srfToObsJ = vsub(obsPosJ, spointJ);
1264
+ const targetId = parseBodyRef(target);
1265
+ const normalF = ellipsoidSurfaceNormal(spoint, getBodyRadiiKm(targetId));
1266
+ const normalJ = frame === "J2000" ? normalF : mxv(inv, normalF);
1267
+ const phase = angleBetween(srfToSrcJ, srfToObsJ);
1268
+ const incdnc = angleBetween(normalJ, srfToSrcJ);
1269
+ const emissn = angleBetween(normalJ, srfToObsJ);
1270
+ const srfvecJ = vsub(spointJ, obsPosJ);
1271
+ const srfvec = frame === "J2000" ? srfvecJ : mxv(rotZRowMajor(-FRAME_SPIN_RATE_RAD_PER_SEC[frame] * et), srfvecJ);
1272
+ return {
1273
+ trgepc: et,
1274
+ srfvec,
1275
+ phase,
1276
+ incdnc,
1277
+ emissn,
1278
+ };
1279
+ },
1280
+ illumf: (_method, target, ilusrc, et, fixref, abcorr, observer, spoint) => {
1281
+ void abcorr;
1282
+ const frame = parseFrameName(fixref);
1283
+ const inv = rotZRowMajor(FRAME_SPIN_RATE_RAD_PER_SEC[frame] * et);
1284
+ const spointJ = frame === "J2000" ? spoint : mxv(inv, spoint);
1285
+ const srcState = getRelativeStateInJ2000(ilusrc, target, et);
1286
+ const srcPosJ = [srcState[0], srcState[1], srcState[2]];
1287
+ const obsState = getRelativeStateInJ2000(observer, target, et);
1288
+ const obsPosJ = [obsState[0], obsState[1], obsState[2]];
1289
+ // Vectors from surface point.
1290
+ const srfToSrcJ = vsub(srcPosJ, spointJ);
1291
+ const srfToObsJ = vsub(obsPosJ, spointJ);
1292
+ const targetId = parseBodyRef(target);
1293
+ const normalF = ellipsoidSurfaceNormal(spoint, getBodyRadiiKm(targetId));
1294
+ const normalJ = frame === "J2000" ? normalF : mxv(inv, normalF);
1295
+ const phase = angleBetween(srfToSrcJ, srfToObsJ);
1296
+ const incdnc = angleBetween(normalJ, srfToSrcJ);
1297
+ const emissn = angleBetween(normalJ, srfToObsJ);
1298
+ const srfvecJ = vsub(spointJ, obsPosJ);
1299
+ const srfvec = frame === "J2000" ? srfvecJ : mxv(rotZRowMajor(-FRAME_SPIN_RATE_RAD_PER_SEC[frame] * et), srfvecJ);
1300
+ const out = {
1301
+ trgepc: et,
1302
+ srfvec,
1303
+ phase,
1304
+ incdnc,
1305
+ emissn,
1306
+ };
1307
+ // NOTE: This fake backend defines:
1308
+ // - visibl: point is visible if emission angle is <= 90deg
1309
+ // - lit: point is lit if incidence angle is <= 90deg
1310
+ const visibl = out.emissn <= Math.PI / 2;
1311
+ const lit = out.incdnc <= Math.PI / 2;
1312
+ return { ...out, visibl, lit };
1313
+ },
1314
+ spksfs: (body, et) => {
1315
+ assertSpiceInt32(body, "spksfs(body)");
1316
+ void et;
1317
+ return { found: false };
1318
+ },
1319
+ spkpds: (_body, _center, _frame, _type, _first, _last) => {
1320
+ throw new Error("Fake backend: spkpds() is not implemented");
1321
+ },
1322
+ spkuds: (_descr) => {
1323
+ throw new Error("Fake backend: spkuds() is not implemented");
1324
+ },
1325
+ nvc2pl: (normal, konst) => {
1326
+ if (!Array.isArray(normal) || normal.length !== 3) {
1327
+ throw new TypeError("nvc2pl(normal, konst): normal must be a length-3 number[]");
1328
+ }
1329
+ for (let i = 0; i < 3; i++) {
1330
+ const v = normal[i];
1331
+ if (typeof v !== "number") {
1332
+ throw new TypeError(`nvc2pl(normal, konst): normal[${i}] must be a number`);
1333
+ }
1334
+ if (!Number.isFinite(v)) {
1335
+ throw new RangeError(`nvc2pl(normal, konst): normal[${i}] must be a finite number`);
1336
+ }
1337
+ }
1338
+ if (typeof konst !== "number") {
1339
+ throw new TypeError("nvc2pl(normal, konst): konst must be a number");
1340
+ }
1341
+ if (!Number.isFinite(konst)) {
1342
+ throw new RangeError("nvc2pl(normal, konst): konst must be a finite number");
1343
+ }
1344
+ return [normal[0], normal[1], normal[2], konst];
1345
+ },
1346
+ pl2nvc: (plane) => {
1347
+ if (!Array.isArray(plane) || plane.length !== 4) {
1348
+ throw new TypeError("pl2nvc(plane): plane must be a length-4 number[]");
1349
+ }
1350
+ for (let i = 0; i < 4; i++) {
1351
+ const v = plane[i];
1352
+ if (typeof v !== "number") {
1353
+ throw new TypeError(`pl2nvc(plane): plane[${i}] must be a number`);
1354
+ }
1355
+ if (!Number.isFinite(v)) {
1356
+ throw new RangeError(`pl2nvc(plane): plane[${i}] must be a finite number`);
1357
+ }
1358
+ }
1359
+ const n = [plane[0], plane[1], plane[2]];
1360
+ const mag = vnorm(n);
1361
+ if (mag === 0) {
1362
+ throw new RangeError("pl2nvc(plane): plane is degenerate (normal must be non-zero)");
1363
+ }
1364
+ return {
1365
+ normal: vscale(1 / mag, n),
1366
+ konst: plane[3] / mag,
1367
+ };
1368
+ },
1369
+ // --- SPK writers (not implemented in fake backend) ---
1370
+ spkopn: (_file, _ifname, _ncomch) => {
1371
+ throw new Error("Fake backend: spkopn() is not implemented");
1372
+ },
1373
+ spkopa: (_file) => {
1374
+ throw new Error("Fake backend: spkopa() is not implemented");
1375
+ },
1376
+ spkcls: (_handle) => {
1377
+ throw new Error("Fake backend: spkcls() is not implemented");
1378
+ },
1379
+ spkw08: (_handle, _body, _center, _frame, _first, _last, _segid, _degree, _states, _epoch1, _step) => {
1380
+ throw new Error("Fake backend: spkw08() is not implemented");
1381
+ },
522
1382
  subpnt: (_method, target, et, fixref, abcorr, observer) => {
523
1383
  void abcorr;
524
1384
  const targetId = parseBodyRef(target);
@@ -562,7 +1422,9 @@ export function createFakeBackend() {
562
1422
  // Vectors from surface point.
563
1423
  const srfToSunJ = vsub(sunPosJ, spointJ);
564
1424
  const srfToObsJ = vsub(obsPosJ, spointJ);
565
- const normalJ = vhat(spointJ);
1425
+ const targetId = parseBodyRef(target);
1426
+ const normalF = ellipsoidSurfaceNormal(spoint, getBodyRadiiKm(targetId));
1427
+ const normalJ = frame === "J2000" ? normalF : mxv(inv, normalF);
566
1428
  const phase = angleBetween(srfToSunJ, srfToObsJ);
567
1429
  const incdnc = angleBetween(normalJ, srfToSunJ);
568
1430
  const emissn = angleBetween(normalJ, srfToObsJ);
@@ -581,6 +1443,211 @@ export function createFakeBackend() {
581
1443
  // Deterministic stub: 0 => "no occultation".
582
1444
  return 0;
583
1445
  },
1446
+ // --- GF (Geometry Finder) (not implemented in fake backend) ---
1447
+ gfsstp: (_step) => {
1448
+ throw new Error("Fake backend: gfsstp() is not implemented");
1449
+ },
1450
+ gfstep: (_time) => {
1451
+ throw new Error("Fake backend: gfstep() is not implemented");
1452
+ },
1453
+ gfstol: (_value) => {
1454
+ throw new Error("Fake backend: gfstol() is not implemented");
1455
+ },
1456
+ gfrefn: (_t1, _t2, _s1, _s2) => {
1457
+ throw new Error("Fake backend: gfrefn() is not implemented");
1458
+ },
1459
+ gfrepi: (_window, _begmss, _endmss) => {
1460
+ throw new Error("Fake backend: gfrepi() is not implemented");
1461
+ },
1462
+ gfrepf: () => {
1463
+ throw new Error("Fake backend: gfrepf() is not implemented");
1464
+ },
1465
+ gfsep: (_targ1, _shape1, _frame1, _targ2, _shape2, _frame2, _abcorr, _obsrvr, _relate, _refval, _adjust, _step, _nintvls, _cnfine, _result) => {
1466
+ throw new Error("Fake backend: gfsep() is not implemented");
1467
+ },
1468
+ gfdist: (_target, _abcorr, _obsrvr, _relate, _refval, _adjust, _step, _nintvls, _cnfine, _result) => {
1469
+ throw new Error("Fake backend: gfdist() is not implemented");
1470
+ },
1471
+ // --- file i/o primitives (not implemented in fake backend) ---
1472
+ exists: (_path) => {
1473
+ throw new Error("Fake backend: exists() is not implemented");
1474
+ },
1475
+ getfat: (_path) => {
1476
+ throw new Error("Fake backend: getfat() is not implemented");
1477
+ },
1478
+ readVirtualOutput: (_output) => {
1479
+ throw new Error("Fake backend: readVirtualOutput() is not implemented");
1480
+ },
1481
+ dafopr: (_path) => {
1482
+ throw new Error("Fake backend: dafopr() is not implemented");
1483
+ },
1484
+ dafcls: (_handle) => {
1485
+ throw new Error("Fake backend: dafcls() is not implemented");
1486
+ },
1487
+ dafbfs: (_handle) => {
1488
+ throw new Error("Fake backend: dafbfs() is not implemented");
1489
+ },
1490
+ daffna: (_handle) => {
1491
+ throw new Error("Fake backend: daffna() is not implemented");
1492
+ },
1493
+ dasopr: (_path) => {
1494
+ throw new Error("Fake backend: dasopr() is not implemented");
1495
+ },
1496
+ dascls: (_handle) => {
1497
+ throw new Error("Fake backend: dascls() is not implemented");
1498
+ },
1499
+ dlaopn: (_path, _ftype, _ifname, _ncomch) => {
1500
+ throw new Error("Fake backend: dlaopn() is not implemented");
1501
+ },
1502
+ dlabfs: (_handle) => {
1503
+ throw new Error("Fake backend: dlabfs() is not implemented");
1504
+ },
1505
+ dlafns: (_handle, _descr) => {
1506
+ throw new Error("Fake backend: dlafns() is not implemented");
1507
+ },
1508
+ dlacls: (_handle) => {
1509
+ throw new Error("Fake backend: dlacls() is not implemented");
1510
+ },
1511
+ // --- EK (not implemented in fake backend) ---
1512
+ ekopr: (_path) => {
1513
+ throw new Error("Fake backend: ekopr() is not implemented");
1514
+ },
1515
+ ekopw: (_path) => {
1516
+ throw new Error("Fake backend: ekopw() is not implemented");
1517
+ },
1518
+ ekopn: (_path, _ifname, _ncomch) => {
1519
+ throw new Error("Fake backend: ekopn() is not implemented");
1520
+ },
1521
+ ekcls: (_handle) => {
1522
+ throw new Error("Fake backend: ekcls() is not implemented");
1523
+ },
1524
+ ekntab: () => {
1525
+ throw new Error("Fake backend: ekntab() is not implemented");
1526
+ },
1527
+ ektnam: (_n) => {
1528
+ throw new Error("Fake backend: ektnam() is not implemented");
1529
+ },
1530
+ eknseg: (_handle) => {
1531
+ throw new Error("Fake backend: eknseg() is not implemented");
1532
+ },
1533
+ // --- EK query/data ops (not implemented in fake backend) ---
1534
+ ekfind: (_query) => {
1535
+ throw new Error("Fake backend: ekfind() is not implemented");
1536
+ },
1537
+ ekgc: (_selidx, _row, _elment) => {
1538
+ throw new Error("Fake backend: ekgc() is not implemented");
1539
+ },
1540
+ ekgd: (_selidx, _row, _elment) => {
1541
+ throw new Error("Fake backend: ekgd() is not implemented");
1542
+ },
1543
+ ekgi: (_selidx, _row, _elment) => {
1544
+ throw new Error("Fake backend: ekgi() is not implemented");
1545
+ },
1546
+ ekifld: (_handle, _tabnam, _nrows, _cnames, _decls) => {
1547
+ throw new Error("Fake backend: ekifld() is not implemented");
1548
+ },
1549
+ ekacli: (_handle, _segno, _column, _ivals, _entszs, _nlflgs, _rcptrs) => {
1550
+ throw new Error("Fake backend: ekacli() is not implemented");
1551
+ },
1552
+ ekacld: (_handle, _segno, _column, _dvals, _entszs, _nlflgs, _rcptrs) => {
1553
+ throw new Error("Fake backend: ekacld() is not implemented");
1554
+ },
1555
+ ekaclc: (_handle, _segno, _column, _cvals, _entszs, _nlflgs, _rcptrs) => {
1556
+ throw new Error("Fake backend: ekaclc() is not implemented");
1557
+ },
1558
+ ekffld: (_handle, _segno, _rcptrs) => {
1559
+ throw new Error("Fake backend: ekffld() is not implemented");
1560
+ },
1561
+ // --- DSK writer (not implemented in fake backend) ---
1562
+ dskopn: (_path, _ifname, _ncomch) => {
1563
+ throw new Error("Fake backend: dskopn() is not implemented");
1564
+ },
1565
+ dskmi2: (_nv, _vrtces, _np, _plates, _finscl, _corscl, _worksz, _voxpsz, _voxlsz, _makvtl, _spxisz) => {
1566
+ throw new Error("Fake backend: dskmi2() is not implemented");
1567
+ },
1568
+ dskw02: (_handle, _center, _surfid, _dclass, _frame, _corsys, _corpar, _mncor1, _mxcor1, _mncor2, _mxcor2, _mncor3, _mxcor3, _first, _last, _nv, _vrtces, _np, _plates, _spaixd, _spaixi) => {
1569
+ throw new Error("Fake backend: dskw02() is not implemented");
1570
+ },
1571
+ // -- DSK ----------------------------------------------------------------
1572
+ dskobj: (_dsk, _bodids) => {
1573
+ throw new Error("Fake backend: dskobj() is not implemented");
1574
+ },
1575
+ dsksrf: (_dsk, _bodyid, _srfids) => {
1576
+ throw new Error("Fake backend: dsksrf() is not implemented");
1577
+ },
1578
+ dskgd: (_handle, _dladsc) => {
1579
+ throw new Error("Fake backend: dskgd() is not implemented");
1580
+ },
1581
+ dskb02: (_handle, _dladsc) => {
1582
+ throw new Error("Fake backend: dskb02() is not implemented");
1583
+ },
1584
+ // -- Cells + windows -----------------------------------------------------
1585
+ //
1586
+ // The fake backend is intentionally minimal and does not attempt to
1587
+ // simulate CSPICE cell/window semantics.
1588
+ newIntCell: (_size) => {
1589
+ throw new Error(spiceCellUnsupported);
1590
+ },
1591
+ newDoubleCell: (_size) => {
1592
+ throw new Error(spiceCellUnsupported);
1593
+ },
1594
+ newCharCell: (_size, _length) => {
1595
+ throw new Error(spiceCellUnsupported);
1596
+ },
1597
+ newWindow: (_maxIntervals) => {
1598
+ throw new Error(spiceCellUnsupported);
1599
+ },
1600
+ freeCell: (_cell) => {
1601
+ throw new Error(spiceCellUnsupported);
1602
+ },
1603
+ freeWindow: (_window) => {
1604
+ throw new Error(spiceCellUnsupported);
1605
+ },
1606
+ ssize: (_size, _cell) => {
1607
+ throw new Error(spiceCellUnsupported);
1608
+ },
1609
+ scard: (_card, _cell) => {
1610
+ throw new Error(spiceCellUnsupported);
1611
+ },
1612
+ card: (_cell) => {
1613
+ throw new Error(spiceCellUnsupported);
1614
+ },
1615
+ size: (_cell) => {
1616
+ throw new Error(spiceCellUnsupported);
1617
+ },
1618
+ valid: (_size, _n, _cell) => {
1619
+ throw new Error(spiceCellUnsupported);
1620
+ },
1621
+ insrti: (_item, _cell) => {
1622
+ throw new Error(spiceCellUnsupported);
1623
+ },
1624
+ insrtd: (_item, _cell) => {
1625
+ throw new Error(spiceCellUnsupported);
1626
+ },
1627
+ insrtc: (_item, _cell) => {
1628
+ throw new Error(spiceCellUnsupported);
1629
+ },
1630
+ cellGeti: (_cell, _index) => {
1631
+ throw new Error(spiceCellUnsupported);
1632
+ },
1633
+ cellGetd: (_cell, _index) => {
1634
+ throw new Error(spiceCellUnsupported);
1635
+ },
1636
+ cellGetc: (_cell, _index) => {
1637
+ throw new Error(spiceCellUnsupported);
1638
+ },
1639
+ wninsd: (_left, _right, _window) => {
1640
+ throw new Error(spiceCellUnsupported);
1641
+ },
1642
+ wncard: (_window) => {
1643
+ throw new Error(spiceCellUnsupported);
1644
+ },
1645
+ wnfetd: (_window, _index) => {
1646
+ throw new Error(spiceCellUnsupported);
1647
+ },
1648
+ wnvald: (_size, _n, _window) => {
1649
+ throw new Error(spiceCellUnsupported);
1650
+ },
584
1651
  reclat: (rect) => {
585
1652
  const x = rect[0];
586
1653
  const y = rect[1];
@@ -622,6 +1689,18 @@ export function createFakeBackend() {
622
1689
  vcrss: (a, b) => vcrss(a, b),
623
1690
  mxv: (m, v) => mxv(m, v),
624
1691
  mtxv: (m, v) => mtxv(m, v),
1692
+ vadd: (a, b) => vadd(a, b),
1693
+ vsub: (a, b) => vsub(a, b),
1694
+ vminus: (v) => vscale(-1, v),
1695
+ vscl: (s, v) => vscale(s, v),
1696
+ mxm: (a, b) => mmul3(a, b),
1697
+ rotate: (angle, axis) => rotateRowMajor(angle, axis),
1698
+ // CSPICE rotmat_c left-multiplies: mout = rotate(angle, axis) * m
1699
+ rotmat: (m, angle, axis) => mmul3(rotateRowMajor(angle, axis), m),
1700
+ axisar: (axis, angle) => axisAngleToRotationRowMajor(axis, angle),
1701
+ georec: (lon, lat, alt, re, f) => georec(lon, lat, alt, re, f),
1702
+ recgeo: (rect, re, f) => recgeo(rect, re, f),
625
1703
  };
1704
+ return backend;
626
1705
  }
627
1706
  //# sourceMappingURL=index.js.map