@kelnishi/satmouse-client 0.9.12 → 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,7 +219,7 @@ declare class InputManager extends TypedEmitter<InputManagerEvents> {
212
219
  private connections;
213
220
  private storage?;
214
221
  private knownDevices;
215
- private accumulator;
222
+ private deviceAccumulators;
216
223
  private accDirty;
217
224
  private flushTimer;
218
225
  private _config;
@@ -244,7 +251,10 @@ declare class InputManager extends TypedEmitter<InputManagerEvents> {
244
251
  onActionValues(callback: (values: ActionValues) => void): () => void;
245
252
  private wireConnection;
246
253
  private flushAccumulator;
247
- private processSpatialData;
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;
248
258
  }
249
259
 
250
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,8 +669,8 @@ var InputManager = class extends TypedEmitter {
650
669
  connections = [];
651
670
  storage;
652
671
  knownDevices = /* @__PURE__ */ new Map();
653
- // Accumulator: sums all device inputs per frame tick
654
- accumulator = { tx: 0, ty: 0, tz: 0, rx: 0, ry: 0, rz: 0 };
672
+ // Per-device accumulators: latest value from each device per frame tick
673
+ deviceAccumulators = /* @__PURE__ */ new Map();
655
674
  accDirty = false;
656
675
  flushTimer = null;
657
676
  _config;
@@ -749,13 +768,16 @@ var InputManager = class extends TypedEmitter {
749
768
  wireConnection(connection) {
750
769
  connection.on("spatialData", (raw) => {
751
770
  this.emit("rawSpatialData", raw);
752
- const acc = this.accumulator;
753
- acc.tx = Math.abs(raw.translation.x) > Math.abs(acc.tx) ? raw.translation.x : acc.tx;
754
- acc.ty = Math.abs(raw.translation.y) > Math.abs(acc.ty) ? raw.translation.y : acc.ty;
755
- acc.tz = Math.abs(raw.translation.z) > Math.abs(acc.tz) ? raw.translation.z : acc.tz;
756
- acc.rx = Math.abs(raw.rotation.x) > Math.abs(acc.rx) ? raw.rotation.x : acc.rx;
757
- acc.ry = Math.abs(raw.rotation.y) > Math.abs(acc.ry) ? raw.rotation.y : acc.ry;
758
- acc.rz = Math.abs(raw.rotation.z) > Math.abs(acc.rz) ? raw.rotation.z : acc.rz;
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
+ });
759
781
  this.accDirty = true;
760
782
  });
761
783
  connection.on("buttonEvent", (event) => this.emit("buttonEvent", event));
@@ -768,25 +790,40 @@ var InputManager = class extends TypedEmitter {
768
790
  }
769
791
  flushAccumulator() {
770
792
  if (!this.accDirty) return;
771
- const raw = {
772
- translation: { x: this.accumulator.tx, y: this.accumulator.ty, z: this.accumulator.tz },
773
- rotation: { x: this.accumulator.rx, y: this.accumulator.ry, z: this.accumulator.rz },
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 },
774
807
  timestamp: performance.now() * 1e3
775
808
  };
776
- this.accumulator = { tx: 0, ty: 0, tz: 0, rx: 0, ry: 0, rz: 0 };
777
- this.accDirty = false;
778
- const { spatial, actions } = this.processSpatialData(raw);
809
+ const { spatial, actions } = this.applyGlobalTransforms(data);
779
810
  if (spatial) this.emit("spatialData", spatial);
780
811
  if (actions) this.emit("actionValues", actions);
781
812
  }
782
- processSpatialData(raw) {
783
- const cfg = this._config;
813
+ /** Per-device transforms: flip, sensitivity, dead zone, dominant, axis remap */
814
+ processPerDevice(raw, deviceId) {
815
+ const cfg = resolveDeviceConfig(this._config, deviceId);
784
816
  let data = raw;
785
817
  if (cfg.deadZone > 0) data = applyDeadZone(data, cfg.deadZone);
786
818
  if (cfg.dominant) data = applyDominant(data);
787
819
  data = applyFlip(data, cfg.flip);
788
820
  data = applyAxisRemap(data, cfg.axisRemap);
789
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;
790
827
  if (cfg.lockPosition) {
791
828
  data = { ...data, translation: { x: 0, y: 0, z: 0 } };
792
829
  }