@kelnishi/satmouse-client 0.10.8 → 0.12.0

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.
@@ -16,10 +16,12 @@ interface Vec3 {
16
16
  y: number;
17
17
  z: number;
18
18
  }
19
- /** 6DOF spatial input frame — matches spatial-data.schema.json */
19
+ /** 6DOF+W spatial input frame */
20
20
  interface SpatialData {
21
21
  translation: Vec3;
22
22
  rotation: Vec3;
23
+ /** Virtual W axis — application-defined (e.g., zoom, scroll, tool size) */
24
+ w?: number;
23
25
  timestamp: number;
24
26
  /** Source device ID (e.g., "cnx-c635", "hid-054c-5c4") */
25
27
  deviceId?: string;
@@ -46,6 +48,8 @@ interface DeviceInfo$1 {
46
48
  axisLabels?: string[];
47
49
  /** Number of buttons this device provides */
48
50
  buttonCount?: number;
51
+ /** Human-readable labels for buttons (indexed by targetButton) */
52
+ buttonLabels?: string[];
49
53
  }
50
54
  type ConnectionState = "disconnected" | "connecting" | "connected" | "failed";
51
55
  type TransportProtocol = "webtransport" | "websocket" | "none";
@@ -111,7 +115,7 @@ declare class SatMouseConnection extends TypedEmitter<SatMouseEvents> {
111
115
  }
112
116
 
113
117
  /** Axis identifier — full or half */
114
- type InputAxis = "tx" | "ty" | "tz" | "rx" | "ry" | "rz" | "tx+" | "ty+" | "tz+" | "rx+" | "ry+" | "rz+" | "tx-" | "ty-" | "tz-" | "rx-" | "ry-" | "rz-";
118
+ type InputAxis = "tx" | "ty" | "tz" | "rx" | "ry" | "rz" | "w" | "tx+" | "ty+" | "tz+" | "rx+" | "ry+" | "rz+" | "w+" | "tx-" | "ty-" | "tz-" | "rx-" | "ry-" | "rz-" | "w-";
115
119
  /** A single axis route — reads from source, writes to target */
116
120
  interface AxisRoute {
117
121
  source: InputAxis;
@@ -120,12 +124,27 @@ interface AxisRoute {
120
124
  flip?: boolean;
121
125
  }
122
126
 
127
+ /** Maps a device button to a keyboard key */
128
+ interface ButtonRoute {
129
+ /** Device button index */
130
+ button: number;
131
+ /** Keyboard key value (KeyboardEvent.key, e.g., "a", "Shift", "ArrowUp") */
132
+ key: string;
133
+ /** Keyboard code (KeyboardEvent.code, e.g., "KeyA", "ShiftLeft"). Optional. */
134
+ code?: string;
135
+ }
123
136
  /** Per-device configuration */
124
137
  interface DeviceConfig {
125
138
  /** Axis routing — each entry maps a device input to an output with optional flip */
126
139
  routes?: AxisRoute[];
127
- /** Scale multiplier applied to all axes (default: 1) */
128
- scale?: number;
140
+ /** Button-to-key mappings */
141
+ buttonRoutes?: ButtonRoute[];
142
+ /** Scale multiplier for translation axes (tx, ty, tz) */
143
+ translateScale?: number;
144
+ /** Scale multiplier for rotation axes (rx, ry, rz) */
145
+ rotateScale?: number;
146
+ /** Scale multiplier for W axis */
147
+ wScale?: number;
129
148
  /** Dead zone threshold (0-1). Values below this are zeroed. */
130
149
  deadZone?: number;
131
150
  /** Only pass the strongest axis, zero all others */
@@ -135,8 +154,14 @@ interface DeviceConfig {
135
154
  interface InputConfig {
136
155
  /** Default axis routes (used when device has no override) */
137
156
  routes: AxisRoute[];
138
- /** Default scale */
139
- scale: number;
157
+ /** Default button-to-key mappings */
158
+ buttonRoutes: ButtonRoute[];
159
+ /** Scale multiplier for translation axes */
160
+ translateScale: number;
161
+ /** Scale multiplier for rotation axes */
162
+ rotateScale: number;
163
+ /** Scale multiplier for W axis */
164
+ wScale: number;
140
165
  /** Dead zone threshold */
141
166
  deadZone: number;
142
167
  /** Dominant axis mode */
@@ -201,6 +226,10 @@ declare class InputManager extends TypedEmitter<InputManagerEvents> {
201
226
  private processPerDevice;
202
227
  /** Get the effective routes for a device: device config override > device axes metadata > global default */
203
228
  private resolveRoutes;
229
+ /** Dispatch KeyboardEvents for button routes matching this button event */
230
+ private dispatchButtonKeys;
231
+ /** Gather all button routes from global config + all device configs */
232
+ private collectButtonRoutes;
204
233
  }
205
234
 
206
235
  interface SatMouseContextValue {
@@ -472,35 +472,45 @@ function readAxis(data, axis) {
472
472
  return data.rotation.y;
473
473
  case "rz":
474
474
  return data.rotation.z;
475
+ case "w":
476
+ return data.w ?? 0;
475
477
  default:
476
478
  return 0;
477
479
  }
478
480
  }
479
- function writeAxis(t, r, axis, value) {
481
+ function writeAxis(acc, axis, value) {
480
482
  const isNeg = axis.endsWith("-");
481
483
  const base = axis.replace(/[+-]$/, "");
482
484
  const sign = isNeg ? -1 : 1;
485
+ if (base === "w") {
486
+ acc.w += value * sign;
487
+ return;
488
+ }
483
489
  const group = base[0];
484
490
  const key = base[1];
485
- if (group === "t") t[key] += value * sign;
486
- else r[key] += value * sign;
491
+ if (group === "t") acc.t[key] += value * sign;
492
+ else acc.r[key] += value * sign;
487
493
  }
488
- function applyRoutes(data, routes, scale = 1) {
489
- const t = { x: 0, y: 0, z: 0 };
490
- const r = { x: 0, y: 0, z: 0 };
494
+ function applyRoutes(data, routes, translateScale = 1, rotateScale = 1, wScale = 1) {
495
+ const acc = { t: { x: 0, y: 0, z: 0 }, r: { x: 0, y: 0, z: 0 }, w: 0 };
491
496
  for (const route of routes) {
492
497
  let value = readAxis(data, route.source);
493
498
  if (route.flip) value = -value;
499
+ const targetBase = route.target.replace(/[+-]$/, "");
500
+ const scale = targetBase === "w" ? wScale : targetBase[0] === "t" ? translateScale : rotateScale;
494
501
  value *= scale;
495
- writeAxis(t, r, route.target, value);
502
+ writeAxis(acc, route.target, value);
496
503
  }
497
- return { translation: t, rotation: r, timestamp: data.timestamp, deviceId: data.deviceId };
504
+ return { translation: acc.t, rotation: acc.r, w: acc.w || void 0, timestamp: data.timestamp, deviceId: data.deviceId };
498
505
  }
499
506
 
500
507
  // src/utils/config.ts
501
508
  var DEFAULT_CONFIG = {
502
509
  routes: DEFAULT_ROUTES,
503
- scale: 1e-3,
510
+ buttonRoutes: [],
511
+ translateScale: 1e-3,
512
+ rotateScale: 1e-3,
513
+ wScale: 1e-3,
504
514
  deadZone: 0,
505
515
  dominant: false,
506
516
  lockPosition: false,
@@ -534,6 +544,7 @@ function mergeConfig(base, partial) {
534
544
  ...base,
535
545
  ...partial,
536
546
  routes: partial.routes ?? [...base.routes],
547
+ buttonRoutes: partial.buttonRoutes ?? [...base.buttonRoutes],
537
548
  devices: { ...base.devices }
538
549
  };
539
550
  if (partial.devices) {
@@ -559,7 +570,10 @@ function resolveDeviceConfig(config, deviceId) {
559
570
  return {
560
571
  ...config,
561
572
  routes: deviceOverride.routes ?? config.routes,
562
- scale: deviceOverride.scale ?? config.scale,
573
+ buttonRoutes: deviceOverride.buttonRoutes ?? config.buttonRoutes,
574
+ translateScale: deviceOverride.translateScale ?? config.translateScale,
575
+ rotateScale: deviceOverride.rotateScale ?? config.rotateScale,
576
+ wScale: deviceOverride.wScale ?? config.wScale,
563
577
  deadZone: deviceOverride.deadZone ?? config.deadZone,
564
578
  dominant: deviceOverride.dominant ?? config.dominant
565
579
  };
@@ -663,7 +677,10 @@ var InputManager = class extends TypedEmitter {
663
677
  const resolved = resolveDeviceConfig(this._config, deviceId);
664
678
  return {
665
679
  routes: resolved.routes,
666
- scale: resolved.scale,
680
+ buttonRoutes: resolved.buttonRoutes,
681
+ translateScale: resolved.translateScale,
682
+ rotateScale: resolved.rotateScale,
683
+ wScale: resolved.wScale,
667
684
  deadZone: resolved.deadZone,
668
685
  dominant: resolved.dominant
669
686
  };
@@ -711,11 +728,15 @@ var InputManager = class extends TypedEmitter {
711
728
  tz: processed.translation.z,
712
729
  rx: processed.rotation.x,
713
730
  ry: processed.rotation.y,
714
- rz: processed.rotation.z
731
+ rz: processed.rotation.z,
732
+ w: processed.w ?? 0
715
733
  });
716
734
  this.accDirty = true;
717
735
  });
718
- connection.on("buttonEvent", (event) => this.emit("buttonEvent", event));
736
+ connection.on("buttonEvent", (event) => {
737
+ this.dispatchButtonKeys(event);
738
+ this.emit("buttonEvent", event);
739
+ });
719
740
  connection.on("stateChange", (state, proto) => {
720
741
  this._state = state;
721
742
  this._protocol = proto;
@@ -729,7 +750,7 @@ var InputManager = class extends TypedEmitter {
729
750
  }
730
751
  flushAccumulator() {
731
752
  if (!this.accDirty) return;
732
- const merged = { tx: 0, ty: 0, tz: 0, rx: 0, ry: 0, rz: 0 };
753
+ const merged = { tx: 0, ty: 0, tz: 0, rx: 0, ry: 0, rz: 0, w: 0 };
733
754
  for (const acc of this.deviceAccumulators.values()) {
734
755
  merged.tx += acc.tx;
735
756
  merged.ty += acc.ty;
@@ -737,12 +758,14 @@ var InputManager = class extends TypedEmitter {
737
758
  merged.rx += acc.rx;
738
759
  merged.ry += acc.ry;
739
760
  merged.rz += acc.rz;
761
+ merged.w += acc.w;
740
762
  }
741
763
  this.deviceAccumulators.clear();
742
764
  this.accDirty = false;
743
765
  let data = {
744
766
  translation: { x: merged.tx, y: merged.ty, z: merged.tz },
745
767
  rotation: { x: merged.rx, y: merged.ry, z: merged.rz },
768
+ w: merged.w || void 0,
746
769
  timestamp: performance.now() * 1e3
747
770
  };
748
771
  if (this._config.lockPosition) {
@@ -783,7 +806,7 @@ var InputManager = class extends TypedEmitter {
783
806
  }
784
807
  const device = this.knownDevices.get(deviceId);
785
808
  const deviceRoutes = this.resolveRoutes(deviceId, device);
786
- data = applyRoutes(data, deviceRoutes, cfg.scale);
809
+ data = applyRoutes(data, deviceRoutes, cfg.translateScale, cfg.rotateScale, cfg.wScale);
787
810
  return data;
788
811
  }
789
812
  /** Get the effective routes for a device: device config override > device axes metadata > global default */
@@ -798,6 +821,27 @@ var InputManager = class extends TypedEmitter {
798
821
  if (device?.axes) return buildRoutes(device.axes);
799
822
  return DEFAULT_ROUTES;
800
823
  }
824
+ /** Dispatch KeyboardEvents for button routes matching this button event */
825
+ dispatchButtonKeys(event) {
826
+ if (typeof document === "undefined") return;
827
+ const allRoutes = this.collectButtonRoutes();
828
+ for (const route of allRoutes) {
829
+ if (route.button === event.button) {
830
+ document.dispatchEvent(new KeyboardEvent(
831
+ event.pressed ? "keydown" : "keyup",
832
+ { key: route.key, code: route.code ?? "", bubbles: true }
833
+ ));
834
+ }
835
+ }
836
+ }
837
+ /** Gather all button routes from global config + all device configs */
838
+ collectButtonRoutes() {
839
+ const routes = [...this._config.buttonRoutes];
840
+ for (const devCfg of Object.values(this._config.devices)) {
841
+ if (devCfg.buttonRoutes) routes.push(...devCfg.buttonRoutes);
842
+ }
843
+ return routes;
844
+ }
801
845
  };
802
846
  var SatMouseContext = createContext(null);
803
847
  function SatMouseProvider({