@kelnishi/satmouse-client 0.9.4 → 0.9.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,118 @@
1
+ # @kelnishi/satmouse-client
2
+
3
+ Client SDK for [SatMouse](https://kelnishi.github.io/SatMouse/) — stream 6DOF spatial input from SpaceMouse and other devices to web apps and PWAs.
4
+
5
+ Three tree-shakeable modules:
6
+
7
+ | Module | Import | Purpose |
8
+ |---|---|---|
9
+ | **core** | `@kelnishi/satmouse-client` | Connection, discovery, binary decode. Zero dependencies. |
10
+ | **utils** | `@kelnishi/satmouse-client/utils` | InputManager, transforms, per-device config, action mapping, persistence |
11
+ | **react** | `@kelnishi/satmouse-client/react` | Provider, hooks, headless components |
12
+
13
+ ## Quick Start
14
+
15
+ ```bash
16
+ npm install @kelnishi/satmouse-client
17
+ ```
18
+
19
+ ### Vanilla
20
+
21
+ ```typescript
22
+ import { SatMouseConnection } from "@kelnishi/satmouse-client";
23
+ import { InputManager } from "@kelnishi/satmouse-client/utils";
24
+
25
+ const connection = new SatMouseConnection();
26
+ const manager = new InputManager();
27
+ manager.addConnection(connection);
28
+
29
+ manager.onSpatialData((data) => {
30
+ console.log(data.translation, data.rotation);
31
+ });
32
+
33
+ await connection.connect();
34
+ ```
35
+
36
+ ### React
37
+
38
+ ```tsx
39
+ import { SatMouseProvider, useSpatialData } from "@kelnishi/satmouse-client/react";
40
+
41
+ function App() {
42
+ return (
43
+ <SatMouseProvider>
44
+ <Scene />
45
+ </SatMouseProvider>
46
+ );
47
+ }
48
+
49
+ function Scene() {
50
+ const data = useSpatialData();
51
+ // data.translation.x/y/z, data.rotation.x/y/z
52
+ }
53
+ ```
54
+
55
+ ## Per-Device Configuration
56
+
57
+ ```typescript
58
+ const manager = new InputManager({
59
+ sensitivity: { translation: 0.001, rotation: 0.001 },
60
+ devices: {
61
+ "hid-054c-*": { sensitivity: { translation: 0.002 } },
62
+ "spacemouse-c635": { flip: { rz: false } },
63
+ },
64
+ });
65
+
66
+ // Query devices and their resolved config
67
+ const devices = manager.getDevicesWithConfig();
68
+
69
+ // Update per-device
70
+ manager.updateDeviceConfig("spacemouse-c635", {
71
+ sensitivity: { rotation: 0.005 },
72
+ });
73
+ ```
74
+
75
+ ## Action Mapping
76
+
77
+ Remap input axes to named actions. Default passes through 1:1.
78
+
79
+ ```typescript
80
+ import { InputManager, swapActions, DEFAULT_ACTION_MAP } from "@kelnishi/satmouse-client/utils";
81
+
82
+ // Swap ty and tz
83
+ const manager = new InputManager({
84
+ actionMap: swapActions(DEFAULT_ACTION_MAP, "ty", "tz"),
85
+ });
86
+
87
+ // Per-device remapping
88
+ manager.updateDeviceConfig("hid-054c-*", {
89
+ actionMap: {
90
+ tx: { source: "tx" },
91
+ tz: { source: "ty", invert: true },
92
+ ry: { source: "rx", scale: 2.0 },
93
+ },
94
+ });
95
+
96
+ // Named action values
97
+ manager.onActionValues((values) => {
98
+ scene.pan(values.tx, values.ty);
99
+ scene.zoom(values.tz);
100
+ });
101
+ ```
102
+
103
+ ## Connection Options
104
+
105
+ ```typescript
106
+ // Auto-discover via Thing Description
107
+ new SatMouseConnection();
108
+
109
+ // Direct URL
110
+ new SatMouseConnection({ wsUrl: "ws://192.168.1.42:18945/spatial" });
111
+
112
+ // Via satmouse:// URI
113
+ new SatMouseConnection({ uri: "satmouse://connect?host=192.168.1.42" });
114
+ ```
115
+
116
+ ## License
117
+
118
+ MIT — [GitHub](https://github.com/kelnishi/SatMouse)
@@ -427,6 +427,57 @@ function launchSatMouse(options) {
427
427
  });
428
428
  }
429
429
 
430
+ // src/utils/action-map.ts
431
+ var DEFAULT_ACTION_MAP = {
432
+ tx: { source: "tx" },
433
+ ty: { source: "ty" },
434
+ tz: { source: "tz" },
435
+ rx: { source: "rx" },
436
+ ry: { source: "ry" },
437
+ rz: { source: "rz" }
438
+ };
439
+ function readAxis(data, axis) {
440
+ switch (axis) {
441
+ case "tx":
442
+ return data.translation.x;
443
+ case "ty":
444
+ return data.translation.y;
445
+ case "tz":
446
+ return data.translation.z;
447
+ case "rx":
448
+ return data.rotation.x;
449
+ case "ry":
450
+ return data.rotation.y;
451
+ case "rz":
452
+ return data.rotation.z;
453
+ }
454
+ }
455
+ function applyActionMap(data, map) {
456
+ const result = {};
457
+ for (const [action, binding] of Object.entries(map)) {
458
+ let value = readAxis(data, binding.source);
459
+ if (binding.invert) value = -value;
460
+ value *= binding.scale ?? 1;
461
+ result[action] = value;
462
+ }
463
+ return result;
464
+ }
465
+ function actionValuesToSpatialData(values, timestamp) {
466
+ return {
467
+ translation: {
468
+ x: values.tx ?? 0,
469
+ y: values.ty ?? 0,
470
+ z: values.tz ?? 0
471
+ },
472
+ rotation: {
473
+ x: values.rx ?? 0,
474
+ y: values.ry ?? 0,
475
+ z: values.rz ?? 0
476
+ },
477
+ timestamp
478
+ };
479
+ }
480
+
430
481
  // src/utils/config.ts
431
482
  var DEFAULT_CONFIG = {
432
483
  sensitivity: { translation: 1e-3, rotation: 1e-3 },
@@ -436,6 +487,7 @@ var DEFAULT_CONFIG = {
436
487
  axisRemap: { tx: "x", ty: "y", tz: "z", rx: "x", ry: "y", rz: "z" },
437
488
  lockPosition: false,
438
489
  lockRotation: false,
490
+ actionMap: { ...DEFAULT_ACTION_MAP },
439
491
  devices: {}
440
492
  };
441
493
  function mergeConfig(base, partial) {
@@ -445,6 +497,7 @@ function mergeConfig(base, partial) {
445
497
  sensitivity: { ...base.sensitivity, ...partial.sensitivity },
446
498
  flip: { ...base.flip, ...partial.flip },
447
499
  axisRemap: { ...base.axisRemap, ...partial.axisRemap },
500
+ actionMap: partial.actionMap ? { ...base.actionMap, ...partial.actionMap } : { ...base.actionMap },
448
501
  devices: { ...base.devices }
449
502
  };
450
503
  if (partial.devices) {
@@ -484,6 +537,7 @@ function resolveDeviceConfig(config, deviceId) {
484
537
  deadZone: deviceOverride.deadZone ?? config.deadZone,
485
538
  dominant: deviceOverride.dominant ?? config.dominant,
486
539
  axisRemap: { ...config.axisRemap, ...deviceOverride.axisRemap },
540
+ actionMap: deviceOverride.actionMap ? { ...config.actionMap, ...deviceOverride.actionMap } : config.actionMap,
487
541
  lockPosition: deviceOverride.lockPosition ?? config.lockPosition,
488
542
  lockRotation: deviceOverride.lockRotation ?? config.lockRotation
489
543
  };
@@ -650,6 +704,7 @@ var InputManager = class extends TypedEmitter {
650
704
  deadZone: resolved.deadZone,
651
705
  dominant: resolved.dominant,
652
706
  axisRemap: resolved.axisRemap,
707
+ actionMap: resolved.actionMap,
653
708
  lockPosition: resolved.lockPosition,
654
709
  lockRotation: resolved.lockRotation
655
710
  };
@@ -679,11 +734,17 @@ var InputManager = class extends TypedEmitter {
679
734
  this.on("buttonEvent", callback);
680
735
  return () => this.off("buttonEvent", callback);
681
736
  }
737
+ /** Register a callback for action values. Returns unsubscribe function. */
738
+ onActionValues(callback) {
739
+ this.on("actionValues", callback);
740
+ return () => this.off("actionValues", callback);
741
+ }
682
742
  wireConnection(connection) {
683
743
  connection.on("spatialData", (raw) => {
684
744
  this.emit("rawSpatialData", raw);
685
- const processed = this.processSpatialData(raw);
686
- if (processed) this.emit("spatialData", processed);
745
+ const { spatial, actions } = this.processSpatialData(raw);
746
+ if (spatial) this.emit("spatialData", spatial);
747
+ if (actions) this.emit("actionValues", actions);
687
748
  });
688
749
  connection.on("buttonEvent", (event) => this.emit("buttonEvent", event));
689
750
  connection.on("stateChange", (state, proto) => this.emit("stateChange", state, proto));
@@ -707,7 +768,9 @@ var InputManager = class extends TypedEmitter {
707
768
  if (cfg.lockRotation) {
708
769
  data = { ...data, rotation: { x: 0, y: 0, z: 0 } };
709
770
  }
710
- return data;
771
+ const actions = applyActionMap(data, cfg.actionMap);
772
+ const spatial = actionValuesToSpatialData(actions, data.timestamp);
773
+ return { spatial, actions };
711
774
  }
712
775
  };
713
776
  var SatMouseContext = react.createContext(null);