@kelnishi/satmouse-client 0.9.3 → 0.9.5

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.
@@ -119,7 +119,42 @@ type AxisMap = {
119
119
  rz: keyof Vec3;
120
120
  };
121
121
 
122
+ /** Input axis identifier */
123
+ type InputAxis = "tx" | "ty" | "tz" | "rx" | "ry" | "rz";
124
+ /** A single action binding — maps one input axis to a named output */
125
+ interface ActionBinding {
126
+ /** Which input axis drives this action */
127
+ source: InputAxis;
128
+ /** Scale multiplier (default: 1) */
129
+ scale?: number;
130
+ /** Invert the value (default: false) */
131
+ invert?: boolean;
132
+ }
133
+ /**
134
+ * ActionMap defines how raw 6DOF axes map to named output actions.
135
+ *
136
+ * Client apps declare the actions they support and how device axes
137
+ * feed into them. Users can reassign axes via the settings UI.
138
+ *
139
+ * Default: 6 actions matching the 6 input axes (passthrough).
140
+ */
141
+ type ActionMap = Record<string, ActionBinding>;
142
+ /** Result of applying an ActionMap to spatial data */
143
+ type ActionValues = Record<string, number>;
144
+
145
+ /** Per-device transform overrides. Any field left undefined inherits from global defaults. */
146
+ interface DeviceConfig {
147
+ sensitivity?: Partial<SensitivityConfig>;
148
+ flip?: Partial<FlipConfig>;
149
+ deadZone?: number;
150
+ dominant?: boolean;
151
+ axisRemap?: Partial<AxisMap>;
152
+ actionMap?: ActionMap;
153
+ lockPosition?: boolean;
154
+ lockRotation?: boolean;
155
+ }
122
156
  interface InputConfig {
157
+ /** Global defaults applied to all devices */
123
158
  sensitivity: SensitivityConfig;
124
159
  flip: FlipConfig;
125
160
  deadZone: number;
@@ -127,6 +162,14 @@ interface InputConfig {
127
162
  axisRemap: AxisMap;
128
163
  lockPosition: boolean;
129
164
  lockRotation: boolean;
165
+ /** Action map — maps input axes to named output actions. Default: passthrough. */
166
+ actionMap: ActionMap;
167
+ /**
168
+ * Per-device overrides, keyed by device ID (e.g., "spacemouse-c635")
169
+ * or device family pattern (e.g., "spacemouse-*", "hid-054c-*").
170
+ * Values override global defaults for matching devices.
171
+ */
172
+ devices: Record<string, DeviceConfig>;
130
173
  }
131
174
 
132
175
  interface StorageAdapter {
@@ -135,8 +178,10 @@ interface StorageAdapter {
135
178
  }
136
179
 
137
180
  interface InputManagerEvents {
138
- /** Processed spatial data (after all transforms) */
181
+ /** Processed spatial data (after all transforms + action map) */
139
182
  spatialData: (data: SpatialData) => void;
183
+ /** Named action values from the action map */
184
+ actionValues: (values: ActionValues) => void;
140
185
  /** Raw spatial data (before transforms) */
141
186
  rawSpatialData: (data: SpatialData) => void;
142
187
  /** Button event (pass-through from connection) */
@@ -148,18 +193,25 @@ interface InputManagerEvents {
148
193
  /** Configuration changed */
149
194
  configChange: (config: InputConfig) => void;
150
195
  }
196
+ /** A connected device paired with its resolved configuration */
197
+ interface DeviceWithConfig {
198
+ device: DeviceInfo$1;
199
+ config: DeviceConfig;
200
+ }
151
201
  /**
152
202
  * Unified device service that wraps one or more SatMouseConnections
153
203
  * and provides a single processed event stream.
154
204
  *
155
- * Applies a configurable transform pipeline to spatial data:
205
+ * Applies a configurable transform pipeline per-device:
156
206
  * deadZone → dominant → flip → axisRemap → sensitivity → lock
157
207
  *
158
- * Persists settings to storage (localStorage by default).
208
+ * Per-device overrides are resolved from InputConfig.devices using
209
+ * device ID matching (exact or pattern with wildcard "*").
159
210
  */
160
211
  declare class InputManager extends TypedEmitter<InputManagerEvents> {
161
212
  private connections;
162
213
  private storage?;
214
+ private knownDevices;
163
215
  private _config;
164
216
  get config(): InputConfig;
165
217
  constructor(config?: Partial<InputConfig>, storage?: StorageAdapter);
@@ -173,12 +225,20 @@ declare class InputManager extends TypedEmitter<InputManagerEvents> {
173
225
  disconnect(): void;
174
226
  /** Fetch device info from all connections */
175
227
  fetchDeviceInfo(): Promise<DeviceInfo$1[]>;
176
- /** Update configuration. Persists by default. */
228
+ /** Get all known connected devices paired with their resolved config */
229
+ getDevicesWithConfig(): DeviceWithConfig[];
230
+ /** Get the resolved per-device config (global defaults + device overrides) */
231
+ getDeviceConfig(deviceId: string): DeviceConfig;
232
+ /** Update global configuration. Persists by default. */
177
233
  updateConfig(partial: Partial<InputConfig>, persist?: boolean): void;
234
+ /** Update configuration for a specific device. Persists by default. */
235
+ updateDeviceConfig(deviceId: string, partial: DeviceConfig, persist?: boolean): void;
178
236
  /** Register a callback for processed spatial data. Returns unsubscribe function. */
179
237
  onSpatialData(callback: (data: SpatialData) => void): () => void;
180
238
  /** Register a callback for button events. Returns unsubscribe function. */
181
239
  onButtonEvent(callback: (data: ButtonEvent) => void): () => void;
240
+ /** Register a callback for action values. Returns unsubscribe function. */
241
+ onActionValues(callback: (values: ActionValues) => void): () => void;
182
242
  private wireConnection;
183
243
  private processSpatialData;
184
244
  }
@@ -425,6 +425,57 @@ function launchSatMouse(options) {
425
425
  });
426
426
  }
427
427
 
428
+ // src/utils/action-map.ts
429
+ var DEFAULT_ACTION_MAP = {
430
+ tx: { source: "tx" },
431
+ ty: { source: "ty" },
432
+ tz: { source: "tz" },
433
+ rx: { source: "rx" },
434
+ ry: { source: "ry" },
435
+ rz: { source: "rz" }
436
+ };
437
+ function readAxis(data, axis) {
438
+ switch (axis) {
439
+ case "tx":
440
+ return data.translation.x;
441
+ case "ty":
442
+ return data.translation.y;
443
+ case "tz":
444
+ return data.translation.z;
445
+ case "rx":
446
+ return data.rotation.x;
447
+ case "ry":
448
+ return data.rotation.y;
449
+ case "rz":
450
+ return data.rotation.z;
451
+ }
452
+ }
453
+ function applyActionMap(data, map) {
454
+ const result = {};
455
+ for (const [action, binding] of Object.entries(map)) {
456
+ let value = readAxis(data, binding.source);
457
+ if (binding.invert) value = -value;
458
+ value *= binding.scale ?? 1;
459
+ result[action] = value;
460
+ }
461
+ return result;
462
+ }
463
+ function actionValuesToSpatialData(values, timestamp) {
464
+ return {
465
+ translation: {
466
+ x: values.tx ?? 0,
467
+ y: values.ty ?? 0,
468
+ z: values.tz ?? 0
469
+ },
470
+ rotation: {
471
+ x: values.rx ?? 0,
472
+ y: values.ry ?? 0,
473
+ z: values.rz ?? 0
474
+ },
475
+ timestamp
476
+ };
477
+ }
478
+
428
479
  // src/utils/config.ts
429
480
  var DEFAULT_CONFIG = {
430
481
  sensitivity: { translation: 1e-3, rotation: 1e-3 },
@@ -433,15 +484,60 @@ var DEFAULT_CONFIG = {
433
484
  dominant: false,
434
485
  axisRemap: { tx: "x", ty: "y", tz: "z", rx: "x", ry: "y", rz: "z" },
435
486
  lockPosition: false,
436
- lockRotation: false
487
+ lockRotation: false,
488
+ actionMap: { ...DEFAULT_ACTION_MAP },
489
+ devices: {}
437
490
  };
438
491
  function mergeConfig(base, partial) {
439
- return {
492
+ const merged = {
440
493
  ...base,
441
494
  ...partial,
442
495
  sensitivity: { ...base.sensitivity, ...partial.sensitivity },
443
496
  flip: { ...base.flip, ...partial.flip },
444
- axisRemap: { ...base.axisRemap, ...partial.axisRemap }
497
+ axisRemap: { ...base.axisRemap, ...partial.axisRemap },
498
+ actionMap: partial.actionMap ? { ...base.actionMap, ...partial.actionMap } : { ...base.actionMap },
499
+ devices: { ...base.devices }
500
+ };
501
+ if (partial.devices) {
502
+ for (const [key, devCfg] of Object.entries(partial.devices)) {
503
+ merged.devices[key] = mergeDeviceConfig(merged.devices[key], devCfg);
504
+ }
505
+ }
506
+ return merged;
507
+ }
508
+ function mergeDeviceConfig(base, partial) {
509
+ if (!base) return partial;
510
+ return {
511
+ ...base,
512
+ ...partial,
513
+ sensitivity: partial.sensitivity ? { ...base.sensitivity, ...partial.sensitivity } : base.sensitivity,
514
+ flip: partial.flip ? { ...base.flip, ...partial.flip } : base.flip,
515
+ axisRemap: partial.axisRemap ? { ...base.axisRemap, ...partial.axisRemap } : base.axisRemap
516
+ };
517
+ }
518
+ function resolveDeviceConfig(config, deviceId) {
519
+ let deviceOverride;
520
+ if (config.devices[deviceId]) {
521
+ deviceOverride = config.devices[deviceId];
522
+ } else {
523
+ for (const [pattern, cfg] of Object.entries(config.devices)) {
524
+ if (pattern.endsWith("*") && deviceId.startsWith(pattern.slice(0, -1))) {
525
+ deviceOverride = cfg;
526
+ break;
527
+ }
528
+ }
529
+ }
530
+ if (!deviceOverride) return config;
531
+ return {
532
+ ...config,
533
+ sensitivity: { ...config.sensitivity, ...deviceOverride.sensitivity },
534
+ flip: { ...config.flip, ...deviceOverride.flip },
535
+ deadZone: deviceOverride.deadZone ?? config.deadZone,
536
+ dominant: deviceOverride.dominant ?? config.dominant,
537
+ axisRemap: { ...config.axisRemap, ...deviceOverride.axisRemap },
538
+ actionMap: deviceOverride.actionMap ? { ...config.actionMap, ...deviceOverride.actionMap } : config.actionMap,
539
+ lockPosition: deviceOverride.lockPosition ?? config.lockPosition,
540
+ lockRotation: deviceOverride.lockRotation ?? config.lockRotation
445
541
  };
446
542
  }
447
543
 
@@ -553,6 +649,7 @@ function applyAxisRemap(data, map) {
553
649
  var InputManager = class extends TypedEmitter {
554
650
  connections = [];
555
651
  storage;
652
+ knownDevices = /* @__PURE__ */ new Map();
556
653
  _config;
557
654
  get config() {
558
655
  return this._config;
@@ -585,14 +682,46 @@ var InputManager = class extends TypedEmitter {
585
682
  /** Fetch device info from all connections */
586
683
  async fetchDeviceInfo() {
587
684
  const results = await Promise.all(this.connections.map((c) => c.fetchDeviceInfo()));
588
- return results.flat();
685
+ const devices = results.flat();
686
+ for (const d of devices) this.knownDevices.set(d.id, d);
687
+ return devices;
589
688
  }
590
- /** Update configuration. Persists by default. */
689
+ /** Get all known connected devices paired with their resolved config */
690
+ getDevicesWithConfig() {
691
+ return Array.from(this.knownDevices.values()).map((device) => ({
692
+ device,
693
+ config: this.getDeviceConfig(device.id)
694
+ }));
695
+ }
696
+ /** Get the resolved per-device config (global defaults + device overrides) */
697
+ getDeviceConfig(deviceId) {
698
+ const resolved = resolveDeviceConfig(this._config, deviceId);
699
+ return {
700
+ sensitivity: resolved.sensitivity,
701
+ flip: resolved.flip,
702
+ deadZone: resolved.deadZone,
703
+ dominant: resolved.dominant,
704
+ axisRemap: resolved.axisRemap,
705
+ actionMap: resolved.actionMap,
706
+ lockPosition: resolved.lockPosition,
707
+ lockRotation: resolved.lockRotation
708
+ };
709
+ }
710
+ /** Update global configuration. Persists by default. */
591
711
  updateConfig(partial, persist = true) {
592
712
  this._config = mergeConfig(this._config, partial);
593
713
  if (persist) saveSettings(this._config, this.storage);
594
714
  this.emit("configChange", this._config);
595
715
  }
716
+ /** Update configuration for a specific device. Persists by default. */
717
+ updateDeviceConfig(deviceId, partial, persist = true) {
718
+ const existing = this._config.devices[deviceId] ?? {};
719
+ this._config = mergeConfig(this._config, {
720
+ devices: { [deviceId]: { ...existing, ...partial } }
721
+ });
722
+ if (persist) saveSettings(this._config, this.storage);
723
+ this.emit("configChange", this._config);
724
+ }
596
725
  /** Register a callback for processed spatial data. Returns unsubscribe function. */
597
726
  onSpatialData(callback) {
598
727
  this.on("spatialData", callback);
@@ -603,15 +732,25 @@ var InputManager = class extends TypedEmitter {
603
732
  this.on("buttonEvent", callback);
604
733
  return () => this.off("buttonEvent", callback);
605
734
  }
735
+ /** Register a callback for action values. Returns unsubscribe function. */
736
+ onActionValues(callback) {
737
+ this.on("actionValues", callback);
738
+ return () => this.off("actionValues", callback);
739
+ }
606
740
  wireConnection(connection) {
607
741
  connection.on("spatialData", (raw) => {
608
742
  this.emit("rawSpatialData", raw);
609
- const processed = this.processSpatialData(raw);
610
- if (processed) this.emit("spatialData", processed);
743
+ const { spatial, actions } = this.processSpatialData(raw);
744
+ if (spatial) this.emit("spatialData", spatial);
745
+ if (actions) this.emit("actionValues", actions);
611
746
  });
612
747
  connection.on("buttonEvent", (event) => this.emit("buttonEvent", event));
613
748
  connection.on("stateChange", (state, proto) => this.emit("stateChange", state, proto));
614
- connection.on("deviceStatus", (event, device) => this.emit("deviceStatus", event, device));
749
+ connection.on("deviceStatus", (event, device) => {
750
+ if (event === "connected") this.knownDevices.set(device.id, device);
751
+ else this.knownDevices.delete(device.id);
752
+ this.emit("deviceStatus", event, device);
753
+ });
615
754
  }
616
755
  processSpatialData(raw) {
617
756
  const cfg = this._config;
@@ -627,7 +766,9 @@ var InputManager = class extends TypedEmitter {
627
766
  if (cfg.lockRotation) {
628
767
  data = { ...data, rotation: { x: 0, y: 0, z: 0 } };
629
768
  }
630
- return data;
769
+ const actions = applyActionMap(data, cfg.actionMap);
770
+ const spatial = actionValuesToSpatialData(actions, data.timestamp);
771
+ return { spatial, actions };
631
772
  }
632
773
  };
633
774
  var SatMouseContext = createContext(null);