@kelnishi/satmouse-client 0.9.9 → 0.9.13

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.
@@ -21,6 +21,8 @@ interface SpatialData {
21
21
  translation: Vec3;
22
22
  rotation: Vec3;
23
23
  timestamp: number;
24
+ /** Source device ID (e.g., "cnx-c635", "hid-054c-5c4") */
25
+ deviceId?: string;
24
26
  }
25
27
  /** Button press/release event — matches button-event.schema.json */
26
28
  interface ButtonEvent {
@@ -39,7 +41,7 @@ interface DeviceInfo$1 {
39
41
  connectionType?: "usb" | "wireless" | "bluetooth" | "unknown";
40
42
  connected?: boolean;
41
43
  }
42
- type ConnectionState = "disconnected" | "connecting" | "connected";
44
+ type ConnectionState = "disconnected" | "connecting" | "connected" | "failed";
43
45
  type TransportProtocol = "webtransport" | "websocket" | "none";
44
46
  interface SatMouseEvents {
45
47
  spatialData: (data: SpatialData) => void;
@@ -67,6 +69,8 @@ interface ConnectOptions {
67
69
  transports?: TransportProtocol[];
68
70
  /** Auto-reconnect delay in ms. 0 to disable. Default: 2000 */
69
71
  reconnectDelay?: number;
72
+ /** Max reconnect attempts before giving up. Default: 3 */
73
+ maxRetries?: number;
70
74
  /** WebSocket subprotocol. Default: "satmouse-json" */
71
75
  wsSubprotocol?: "satmouse-json" | "satmouse-binary";
72
76
  }
@@ -83,12 +87,15 @@ declare class SatMouseConnection extends TypedEmitter<SatMouseEvents> {
83
87
  private reconnectTimer;
84
88
  private intentionalClose;
85
89
  private deviceInfoUrl;
90
+ private retryCount;
86
91
  private _state;
87
92
  private _protocol;
88
93
  get state(): ConnectionState;
89
94
  get protocol(): TransportProtocol;
90
95
  constructor(options?: ConnectOptions);
91
96
  connect(): Promise<void>;
97
+ /** Reset retry count and reconnect. Use after "failed" state. */
98
+ retry(): void;
92
99
  disconnect(): void;
93
100
  fetchDeviceInfo(): Promise<DeviceInfo$1[]>;
94
101
  private tryTransport;
@@ -212,6 +219,9 @@ declare class InputManager extends TypedEmitter<InputManagerEvents> {
212
219
  private connections;
213
220
  private storage?;
214
221
  private knownDevices;
222
+ private deviceAccumulators;
223
+ private accDirty;
224
+ private flushTimer;
215
225
  private _config;
216
226
  get config(): InputConfig;
217
227
  constructor(config?: Partial<InputConfig>, storage?: StorageAdapter);
@@ -240,7 +250,11 @@ declare class InputManager extends TypedEmitter<InputManagerEvents> {
240
250
  /** Register a callback for action values. Returns unsubscribe function. */
241
251
  onActionValues(callback: (values: ActionValues) => void): () => void;
242
252
  private wireConnection;
243
- private processSpatialData;
253
+ private flushAccumulator;
254
+ /** Per-device transforms: flip, sensitivity, dead zone, dominant, axis remap */
255
+ private processPerDevice;
256
+ /** Global transforms applied after per-device merge: locks + action map */
257
+ private applyGlobalTransforms;
244
258
  }
245
259
 
246
260
  interface SatMouseContextValue {
@@ -267,6 +267,7 @@ function parseSatMouseUri(uri) {
267
267
  var DEFAULT_OPTIONS = {
268
268
  transports: ["webtransport", "websocket"],
269
269
  reconnectDelay: 2e3,
270
+ maxRetries: 3,
270
271
  wsSubprotocol: "satmouse-json"
271
272
  };
272
273
  var SatMouseConnection = class extends TypedEmitter {
@@ -275,6 +276,7 @@ var SatMouseConnection = class extends TypedEmitter {
275
276
  reconnectTimer = null;
276
277
  intentionalClose = false;
277
278
  deviceInfoUrl = null;
279
+ retryCount = 0;
278
280
  _state = "disconnected";
279
281
  _protocol = "none";
280
282
  get state() {
@@ -335,6 +337,12 @@ var SatMouseConnection = class extends TypedEmitter {
335
337
  this.setState("disconnected", "none");
336
338
  this.scheduleReconnect();
337
339
  }
340
+ /** Reset retry count and reconnect. Use after "failed" state. */
341
+ retry() {
342
+ this.retryCount = 0;
343
+ this.intentionalClose = false;
344
+ this.connect();
345
+ }
338
346
  disconnect() {
339
347
  this.intentionalClose = true;
340
348
  this.clearReconnect();
@@ -366,6 +374,7 @@ var SatMouseConnection = class extends TypedEmitter {
366
374
  try {
367
375
  await adapter.connect();
368
376
  this.transport = adapter;
377
+ this.retryCount = 0;
369
378
  this.setState("connected", adapter.protocol);
370
379
  return true;
371
380
  } catch (err) {
@@ -381,6 +390,13 @@ var SatMouseConnection = class extends TypedEmitter {
381
390
  }
382
391
  scheduleReconnect() {
383
392
  if (this.options.reconnectDelay <= 0 || this.intentionalClose) return;
393
+ this.retryCount++;
394
+ console.log(`[SatMouse] Reconnect attempt ${this.retryCount}/${this.options.maxRetries}`);
395
+ if (this.retryCount > this.options.maxRetries) {
396
+ console.log("[SatMouse] Max retries exceeded, giving up");
397
+ this.setState("failed", "none");
398
+ return;
399
+ }
384
400
  this.clearReconnect();
385
401
  this.reconnectTimer = setTimeout(() => {
386
402
  this.reconnectTimer = null;
@@ -479,14 +495,17 @@ function actionValuesToSpatialData(values, timestamp) {
479
495
  // src/utils/config.ts
480
496
  var DEFAULT_CONFIG = {
481
497
  sensitivity: { translation: 1e-3, rotation: 1e-3 },
482
- flip: { tx: false, ty: true, tz: true, rx: false, ry: true, rz: true },
498
+ flip: { tx: false, ty: false, tz: false, rx: false, ry: false, rz: false },
483
499
  deadZone: 0,
484
500
  dominant: false,
485
501
  axisRemap: { tx: "x", ty: "y", tz: "z", rx: "x", ry: "y", rz: "z" },
486
502
  lockPosition: false,
487
503
  lockRotation: false,
488
504
  actionMap: { ...DEFAULT_ACTION_MAP },
489
- devices: {}
505
+ devices: {
506
+ // SpaceMouse Z-up → Three.js Y-up axis correction
507
+ "cnx-*": { flip: { ty: true, tz: true, ry: true, rz: true } }
508
+ }
490
509
  };
491
510
  function mergeConfig(base, partial) {
492
511
  const merged = {
@@ -650,6 +669,10 @@ var InputManager = class extends TypedEmitter {
650
669
  connections = [];
651
670
  storage;
652
671
  knownDevices = /* @__PURE__ */ new Map();
672
+ // Per-device accumulators: latest value from each device per frame tick
673
+ deviceAccumulators = /* @__PURE__ */ new Map();
674
+ accDirty = false;
675
+ flushTimer = null;
653
676
  _config;
654
677
  get config() {
655
678
  return this._config;
@@ -659,6 +682,7 @@ var InputManager = class extends TypedEmitter {
659
682
  this.storage = storage;
660
683
  const persisted = loadSettings(storage);
661
684
  this._config = mergeConfig(DEFAULT_CONFIG, { ...config, ...persisted });
685
+ this.flushTimer = setInterval(() => this.flushAccumulator(), 16);
662
686
  }
663
687
  /** Add a connection to the managed set */
664
688
  addConnection(connection) {
@@ -678,6 +702,10 @@ var InputManager = class extends TypedEmitter {
678
702
  /** Disconnect all managed connections */
679
703
  disconnect() {
680
704
  for (const c of this.connections) c.disconnect();
705
+ if (this.flushTimer) {
706
+ clearInterval(this.flushTimer);
707
+ this.flushTimer = null;
708
+ }
681
709
  }
682
710
  /** Fetch device info from all connections */
683
711
  async fetchDeviceInfo() {
@@ -740,9 +768,17 @@ var InputManager = class extends TypedEmitter {
740
768
  wireConnection(connection) {
741
769
  connection.on("spatialData", (raw) => {
742
770
  this.emit("rawSpatialData", raw);
743
- const { spatial, actions } = this.processSpatialData(raw);
744
- if (spatial) this.emit("spatialData", spatial);
745
- if (actions) this.emit("actionValues", actions);
771
+ const id = raw.deviceId ?? "_default";
772
+ const processed = this.processPerDevice(raw, id);
773
+ this.deviceAccumulators.set(id, {
774
+ tx: processed.translation.x,
775
+ ty: processed.translation.y,
776
+ tz: processed.translation.z,
777
+ rx: processed.rotation.x,
778
+ ry: processed.rotation.y,
779
+ rz: processed.rotation.z
780
+ });
781
+ this.accDirty = true;
746
782
  });
747
783
  connection.on("buttonEvent", (event) => this.emit("buttonEvent", event));
748
784
  connection.on("stateChange", (state, proto) => this.emit("stateChange", state, proto));
@@ -752,14 +788,42 @@ var InputManager = class extends TypedEmitter {
752
788
  this.emit("deviceStatus", event, device);
753
789
  });
754
790
  }
755
- processSpatialData(raw) {
756
- const cfg = this._config;
791
+ flushAccumulator() {
792
+ if (!this.accDirty) return;
793
+ const merged = { tx: 0, ty: 0, tz: 0, rx: 0, ry: 0, rz: 0 };
794
+ for (const acc of this.deviceAccumulators.values()) {
795
+ merged.tx += acc.tx;
796
+ merged.ty += acc.ty;
797
+ merged.tz += acc.tz;
798
+ merged.rx += acc.rx;
799
+ merged.ry += acc.ry;
800
+ merged.rz += acc.rz;
801
+ }
802
+ this.deviceAccumulators.clear();
803
+ this.accDirty = false;
804
+ const data = {
805
+ translation: { x: merged.tx, y: merged.ty, z: merged.tz },
806
+ rotation: { x: merged.rx, y: merged.ry, z: merged.rz },
807
+ timestamp: performance.now() * 1e3
808
+ };
809
+ const { spatial, actions } = this.applyGlobalTransforms(data);
810
+ if (spatial) this.emit("spatialData", spatial);
811
+ if (actions) this.emit("actionValues", actions);
812
+ }
813
+ /** Per-device transforms: flip, sensitivity, dead zone, dominant, axis remap */
814
+ processPerDevice(raw, deviceId) {
815
+ const cfg = resolveDeviceConfig(this._config, deviceId);
757
816
  let data = raw;
758
817
  if (cfg.deadZone > 0) data = applyDeadZone(data, cfg.deadZone);
759
818
  if (cfg.dominant) data = applyDominant(data);
760
819
  data = applyFlip(data, cfg.flip);
761
820
  data = applyAxisRemap(data, cfg.axisRemap);
762
821
  data = applySensitivity(data, cfg.sensitivity);
822
+ return data;
823
+ }
824
+ /** Global transforms applied after per-device merge: locks + action map */
825
+ applyGlobalTransforms(data) {
826
+ const cfg = this._config;
763
827
  if (cfg.lockPosition) {
764
828
  data = { ...data, translation: { x: 0, y: 0, z: 0 } };
765
829
  }