@kelnishi/satmouse-client 0.9.14 → 0.10.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.
@@ -444,16 +444,24 @@ function launchSatMouse(options) {
444
444
  }
445
445
 
446
446
  // src/utils/action-map.ts
447
- var DEFAULT_ACTION_MAP = {
448
- tx: { source: "tx" },
449
- ty: { source: "ty" },
450
- tz: { source: "tz" },
451
- rx: { source: "rx" },
452
- ry: { source: "ry" },
453
- rz: { source: "rz" }
454
- };
447
+ var DEFAULT_ROUTES = [
448
+ { source: "tx", target: "tx" },
449
+ { source: "ty", target: "ty" },
450
+ { source: "tz", target: "tz" },
451
+ { source: "rx", target: "rx" },
452
+ { source: "ry", target: "ry" },
453
+ { source: "rz", target: "rz" }
454
+ ];
455
+ function buildRoutes(axes) {
456
+ return axes.map((axis) => {
457
+ const base = axis.replace(/[+-]$/, "");
458
+ const flip = axis.endsWith("-");
459
+ return { source: axis, target: base, ...flip && { flip: true } };
460
+ });
461
+ }
455
462
  function readAxis(data, axis) {
456
- switch (axis) {
463
+ const base = axis.replace(/[+-]$/, "");
464
+ switch (base) {
457
465
  case "tx":
458
466
  return data.translation.x;
459
467
  case "ty":
@@ -466,76 +474,77 @@ function readAxis(data, axis) {
466
474
  return data.rotation.y;
467
475
  case "rz":
468
476
  return data.rotation.z;
477
+ default:
478
+ return 0;
469
479
  }
470
480
  }
471
- function applyActionMap(data, map) {
472
- const result = {};
473
- for (const [action, binding] of Object.entries(map)) {
474
- let value = readAxis(data, binding.source);
475
- if (binding.invert) value = -value;
476
- value *= binding.scale ?? 1;
477
- result[action] = value;
478
- }
479
- return result;
481
+ function writeAxis(t, r, axis, value) {
482
+ const isNeg = axis.endsWith("-");
483
+ const base = axis.replace(/[+-]$/, "");
484
+ const sign = isNeg ? -1 : 1;
485
+ const group = base[0];
486
+ const key = base[1];
487
+ if (group === "t") t[key] += value * sign;
488
+ else r[key] += value * sign;
480
489
  }
481
- function actionValuesToSpatialData(values, timestamp) {
482
- return {
483
- translation: {
484
- x: values.tx ?? 0,
485
- y: values.ty ?? 0,
486
- z: values.tz ?? 0
487
- },
488
- rotation: {
489
- x: values.rx ?? 0,
490
- y: values.ry ?? 0,
491
- z: values.rz ?? 0
492
- },
493
- timestamp
494
- };
490
+ function applyRoutes(data, routes, scale = 1) {
491
+ const t = { x: 0, y: 0, z: 0 };
492
+ const r = { x: 0, y: 0, z: 0 };
493
+ for (const route of routes) {
494
+ let value = readAxis(data, route.source);
495
+ if (route.flip) value = -value;
496
+ value *= scale;
497
+ writeAxis(t, r, route.target, value);
498
+ }
499
+ return { translation: t, rotation: r, timestamp: data.timestamp, deviceId: data.deviceId };
495
500
  }
496
501
 
497
502
  // src/utils/config.ts
498
503
  var DEFAULT_CONFIG = {
499
- sensitivity: { translation: 1e-3, rotation: 1e-3 },
500
- flip: { tx: false, ty: false, tz: false, rx: false, ry: false, rz: false },
504
+ routes: DEFAULT_ROUTES,
505
+ scale: 1e-3,
501
506
  deadZone: 0,
502
507
  dominant: false,
503
- axisRemap: { tx: "x", ty: "y", tz: "z", rx: "x", ry: "y", rz: "z" },
504
508
  lockPosition: false,
505
509
  lockRotation: false,
506
- actionMap: { ...DEFAULT_ACTION_MAP },
507
510
  devices: {
508
- // SpaceMouse Z-up → Three.js Y-up axis correction
509
- "cnx-*": { flip: { ty: true, tz: true, ry: true, rz: true } }
511
+ "cnx-*": {
512
+ routes: [
513
+ { source: "tx", target: "tx" },
514
+ { source: "ty", target: "ty", flip: true },
515
+ { source: "tz", target: "tz", flip: true },
516
+ { source: "rx", target: "rx" },
517
+ { source: "ry", target: "ry", flip: true },
518
+ { source: "rz", target: "rz", flip: true }
519
+ ]
520
+ },
521
+ // PlayStation: L2 (ty) → TY, R2 (ry) → TY flipped (push-pull)
522
+ "hid-54c-*": {
523
+ routes: [
524
+ { source: "tx", target: "tx" },
525
+ { source: "tz", target: "tz" },
526
+ { source: "rz", target: "rz" },
527
+ { source: "rx", target: "rx" },
528
+ { source: "ty", target: "ty" },
529
+ { source: "ry", target: "ty", flip: true }
530
+ ]
531
+ }
510
532
  }
511
533
  };
512
534
  function mergeConfig(base, partial) {
513
535
  const merged = {
514
536
  ...base,
515
537
  ...partial,
516
- sensitivity: { ...base.sensitivity, ...partial.sensitivity },
517
- flip: { ...base.flip, ...partial.flip },
518
- axisRemap: { ...base.axisRemap, ...partial.axisRemap },
519
- actionMap: partial.actionMap ? { ...base.actionMap, ...partial.actionMap } : { ...base.actionMap },
538
+ routes: partial.routes ?? [...base.routes],
520
539
  devices: { ...base.devices }
521
540
  };
522
541
  if (partial.devices) {
523
542
  for (const [key, devCfg] of Object.entries(partial.devices)) {
524
- merged.devices[key] = mergeDeviceConfig(merged.devices[key], devCfg);
543
+ merged.devices[key] = { ...merged.devices[key], ...devCfg };
525
544
  }
526
545
  }
527
546
  return merged;
528
547
  }
529
- function mergeDeviceConfig(base, partial) {
530
- if (!base) return partial;
531
- return {
532
- ...base,
533
- ...partial,
534
- sensitivity: partial.sensitivity ? { ...base.sensitivity, ...partial.sensitivity } : base.sensitivity,
535
- flip: partial.flip ? { ...base.flip, ...partial.flip } : base.flip,
536
- axisRemap: partial.axisRemap ? { ...base.axisRemap, ...partial.axisRemap } : base.axisRemap
537
- };
538
- }
539
548
  function resolveDeviceConfig(config, deviceId) {
540
549
  let deviceOverride;
541
550
  if (config.devices[deviceId]) {
@@ -551,14 +560,10 @@ function resolveDeviceConfig(config, deviceId) {
551
560
  if (!deviceOverride) return config;
552
561
  return {
553
562
  ...config,
554
- sensitivity: { ...config.sensitivity, ...deviceOverride.sensitivity },
555
- flip: { ...config.flip, ...deviceOverride.flip },
563
+ routes: deviceOverride.routes ?? config.routes,
564
+ scale: deviceOverride.scale ?? config.scale,
556
565
  deadZone: deviceOverride.deadZone ?? config.deadZone,
557
- dominant: deviceOverride.dominant ?? config.dominant,
558
- axisRemap: { ...config.axisRemap, ...deviceOverride.axisRemap },
559
- actionMap: deviceOverride.actionMap ? { ...config.actionMap, ...deviceOverride.actionMap } : config.actionMap,
560
- lockPosition: deviceOverride.lockPosition ?? config.lockPosition,
561
- lockRotation: deviceOverride.lockRotation ?? config.lockRotation
566
+ dominant: deviceOverride.dominant ?? config.dominant
562
567
  };
563
568
  }
564
569
 
@@ -577,6 +582,11 @@ function saveSettings(config, storage) {
577
582
  if (!s) return;
578
583
  s.setItem(STORAGE_KEY, JSON.stringify(config));
579
584
  }
585
+ function clearSettings(storage) {
586
+ const s = getStorage(storage);
587
+ if (!s) return;
588
+ s.setItem(STORAGE_KEY, "{}");
589
+ }
580
590
  function loadSettings(storage) {
581
591
  const s = getStorage(storage);
582
592
  if (!s) return null;
@@ -589,89 +599,11 @@ function loadSettings(storage) {
589
599
  }
590
600
  }
591
601
 
592
- // src/utils/transforms.ts
593
- function applyFlip(data, flip) {
594
- return {
595
- ...data,
596
- translation: {
597
- x: flip.tx ? -data.translation.x : data.translation.x,
598
- y: flip.ty ? -data.translation.y : data.translation.y,
599
- z: flip.tz ? -data.translation.z : data.translation.z
600
- },
601
- rotation: {
602
- x: flip.rx ? -data.rotation.x : data.rotation.x,
603
- y: flip.ry ? -data.rotation.y : data.rotation.y,
604
- z: flip.rz ? -data.rotation.z : data.rotation.z
605
- }
606
- };
607
- }
608
- function applySensitivity(data, sens) {
609
- return {
610
- ...data,
611
- translation: {
612
- x: data.translation.x * sens.translation,
613
- y: data.translation.y * sens.translation,
614
- z: data.translation.z * sens.translation
615
- },
616
- rotation: {
617
- x: data.rotation.x * sens.rotation,
618
- y: data.rotation.y * sens.rotation,
619
- z: data.rotation.z * sens.rotation
620
- }
621
- };
622
- }
623
- function applyDominant(data) {
624
- const axes = [
625
- { group: "t", key: "x", v: Math.abs(data.translation.x) },
626
- { group: "t", key: "y", v: Math.abs(data.translation.y) },
627
- { group: "t", key: "z", v: Math.abs(data.translation.z) },
628
- { group: "r", key: "x", v: Math.abs(data.rotation.x) },
629
- { group: "r", key: "y", v: Math.abs(data.rotation.y) },
630
- { group: "r", key: "z", v: Math.abs(data.rotation.z) }
631
- ];
632
- const max = axes.reduce((a, b) => b.v > a.v ? b : a);
633
- const t = { x: 0, y: 0, z: 0 };
634
- const r = { x: 0, y: 0, z: 0 };
635
- if (max.group === "t") t[max.key] = data.translation[max.key];
636
- else r[max.key] = data.rotation[max.key];
637
- return { ...data, translation: t, rotation: r };
638
- }
639
- function applyDeadZone(data, threshold) {
640
- const dz = (v) => Math.abs(v) < threshold ? 0 : v;
641
- return {
642
- ...data,
643
- translation: { x: dz(data.translation.x), y: dz(data.translation.y), z: dz(data.translation.z) },
644
- rotation: { x: dz(data.rotation.x), y: dz(data.rotation.y), z: dz(data.rotation.z) }
645
- };
646
- }
647
- function applyAxisRemap(data, map) {
648
- return {
649
- ...data,
650
- translation: {
651
- x: 0,
652
- y: 0,
653
- z: 0,
654
- [map.tx]: data.translation.x,
655
- [map.ty]: data.translation.y,
656
- [map.tz]: data.translation.z
657
- },
658
- rotation: {
659
- x: 0,
660
- y: 0,
661
- z: 0,
662
- [map.rx]: data.rotation.x,
663
- [map.ry]: data.rotation.y,
664
- [map.rz]: data.rotation.z
665
- }
666
- };
667
- }
668
-
669
602
  // src/utils/input-manager.ts
670
603
  var InputManager = class extends TypedEmitter {
671
604
  connections = [];
672
605
  storage;
673
606
  knownDevices = /* @__PURE__ */ new Map();
674
- // Per-device accumulators: latest value from each device per frame tick
675
607
  deviceAccumulators = /* @__PURE__ */ new Map();
676
608
  accDirty = false;
677
609
  flushTimer = null;
@@ -686,22 +618,18 @@ var InputManager = class extends TypedEmitter {
686
618
  this._config = mergeConfig(DEFAULT_CONFIG, { ...config, ...persisted });
687
619
  this.flushTimer = setInterval(() => this.flushAccumulator(), 16);
688
620
  }
689
- /** Add a connection to the managed set */
690
621
  addConnection(connection) {
691
622
  this.connections.push(connection);
692
623
  this.wireConnection(connection);
693
624
  }
694
- /** Remove a connection */
695
625
  removeConnection(connection) {
696
626
  const idx = this.connections.indexOf(connection);
697
627
  if (idx !== -1) this.connections.splice(idx, 1);
698
628
  connection.removeAllListeners();
699
629
  }
700
- /** Connect all managed connections */
701
630
  async connect() {
702
631
  await Promise.all(this.connections.map((c) => c.connect()));
703
632
  }
704
- /** Disconnect all managed connections */
705
633
  disconnect() {
706
634
  for (const c of this.connections) c.disconnect();
707
635
  if (this.flushTimer) {
@@ -709,41 +637,32 @@ var InputManager = class extends TypedEmitter {
709
637
  this.flushTimer = null;
710
638
  }
711
639
  }
712
- /** Fetch device info from all connections */
713
640
  async fetchDeviceInfo() {
714
641
  const results = await Promise.all(this.connections.map((c) => c.fetchDeviceInfo()));
715
642
  const devices = results.flat();
716
643
  for (const d of devices) this.knownDevices.set(d.id, d);
717
644
  return devices;
718
645
  }
719
- /** Get all known connected devices paired with their resolved config */
720
646
  getDevicesWithConfig() {
721
647
  return Array.from(this.knownDevices.values()).map((device) => ({
722
648
  device,
723
649
  config: this.getDeviceConfig(device.id)
724
650
  }));
725
651
  }
726
- /** Get the resolved per-device config (global defaults + device overrides) */
727
652
  getDeviceConfig(deviceId) {
728
653
  const resolved = resolveDeviceConfig(this._config, deviceId);
729
654
  return {
730
- sensitivity: resolved.sensitivity,
731
- flip: resolved.flip,
655
+ routes: resolved.routes,
656
+ scale: resolved.scale,
732
657
  deadZone: resolved.deadZone,
733
- dominant: resolved.dominant,
734
- axisRemap: resolved.axisRemap,
735
- actionMap: resolved.actionMap,
736
- lockPosition: resolved.lockPosition,
737
- lockRotation: resolved.lockRotation
658
+ dominant: resolved.dominant
738
659
  };
739
660
  }
740
- /** Update global configuration. Persists by default. */
741
661
  updateConfig(partial, persist = true) {
742
662
  this._config = mergeConfig(this._config, partial);
743
663
  if (persist) saveSettings(this._config, this.storage);
744
664
  this.emit("configChange", this._config);
745
665
  }
746
- /** Update configuration for a specific device. Persists by default. */
747
666
  updateDeviceConfig(deviceId, partial, persist = true) {
748
667
  const existing = this._config.devices[deviceId] ?? {};
749
668
  this._config = mergeConfig(this._config, {
@@ -752,21 +671,25 @@ var InputManager = class extends TypedEmitter {
752
671
  if (persist) saveSettings(this._config, this.storage);
753
672
  this.emit("configChange", this._config);
754
673
  }
755
- /** Register a callback for processed spatial data. Returns unsubscribe function. */
674
+ resetDeviceConfig(deviceId, persist = true) {
675
+ const { [deviceId]: _, ...rest } = this._config.devices;
676
+ this._config = { ...this._config, devices: rest };
677
+ if (persist) saveSettings(this._config, this.storage);
678
+ this.emit("configChange", this._config);
679
+ }
680
+ resetAllConfig() {
681
+ clearSettings(this.storage);
682
+ this._config = { ...DEFAULT_CONFIG };
683
+ this.emit("configChange", this._config);
684
+ }
756
685
  onSpatialData(callback) {
757
686
  this.on("spatialData", callback);
758
687
  return () => this.off("spatialData", callback);
759
688
  }
760
- /** Register a callback for button events. Returns unsubscribe function. */
761
689
  onButtonEvent(callback) {
762
690
  this.on("buttonEvent", callback);
763
691
  return () => this.off("buttonEvent", callback);
764
692
  }
765
- /** Register a callback for action values. Returns unsubscribe function. */
766
- onActionValues(callback) {
767
- this.on("actionValues", callback);
768
- return () => this.off("actionValues", callback);
769
- }
770
693
  wireConnection(connection) {
771
694
  connection.on("spatialData", (raw) => {
772
695
  this.emit("rawSpatialData", raw);
@@ -803,38 +726,63 @@ var InputManager = class extends TypedEmitter {
803
726
  }
804
727
  this.deviceAccumulators.clear();
805
728
  this.accDirty = false;
806
- const data = {
729
+ let data = {
807
730
  translation: { x: merged.tx, y: merged.ty, z: merged.tz },
808
731
  rotation: { x: merged.rx, y: merged.ry, z: merged.rz },
809
732
  timestamp: performance.now() * 1e3
810
733
  };
811
- const { spatial, actions } = this.applyGlobalTransforms(data);
812
- if (spatial) this.emit("spatialData", spatial);
813
- if (actions) this.emit("actionValues", actions);
734
+ if (this._config.lockPosition) {
735
+ data = { ...data, translation: { x: 0, y: 0, z: 0 } };
736
+ }
737
+ if (this._config.lockRotation) {
738
+ data = { ...data, rotation: { x: 0, y: 0, z: 0 } };
739
+ }
740
+ this.emit("spatialData", data);
814
741
  }
815
- /** Per-device transforms: flip, sensitivity, dead zone, dominant, axis remap */
742
+ /** Per-device: deadZone dominant routes (flip + scale + remap in one pass) */
816
743
  processPerDevice(raw, deviceId) {
817
744
  const cfg = resolveDeviceConfig(this._config, deviceId);
818
745
  let data = raw;
819
- if (cfg.deadZone > 0) data = applyDeadZone(data, cfg.deadZone);
820
- if (cfg.dominant) data = applyDominant(data);
821
- data = applyFlip(data, cfg.flip);
822
- data = applyAxisRemap(data, cfg.axisRemap);
823
- data = applySensitivity(data, cfg.sensitivity);
746
+ if (cfg.deadZone > 0) {
747
+ const dz = (v) => Math.abs(v) < cfg.deadZone ? 0 : v;
748
+ data = {
749
+ ...data,
750
+ translation: { x: dz(data.translation.x), y: dz(data.translation.y), z: dz(data.translation.z) },
751
+ rotation: { x: dz(data.rotation.x), y: dz(data.rotation.y), z: dz(data.rotation.z) }
752
+ };
753
+ }
754
+ if (cfg.dominant) {
755
+ const axes = [
756
+ { g: "t", k: "x", v: Math.abs(data.translation.x) },
757
+ { g: "t", k: "y", v: Math.abs(data.translation.y) },
758
+ { g: "t", k: "z", v: Math.abs(data.translation.z) },
759
+ { g: "r", k: "x", v: Math.abs(data.rotation.x) },
760
+ { g: "r", k: "y", v: Math.abs(data.rotation.y) },
761
+ { g: "r", k: "z", v: Math.abs(data.rotation.z) }
762
+ ];
763
+ const max = axes.reduce((a, b) => b.v > a.v ? b : a);
764
+ const t = { x: 0, y: 0, z: 0 };
765
+ const r = { x: 0, y: 0, z: 0 };
766
+ if (max.g === "t") t[max.k] = data.translation[max.k];
767
+ else r[max.k] = data.rotation[max.k];
768
+ data = { ...data, translation: t, rotation: r };
769
+ }
770
+ const device = this.knownDevices.get(deviceId);
771
+ const deviceRoutes = this.resolveRoutes(deviceId, device);
772
+ data = applyRoutes(data, deviceRoutes, cfg.scale);
824
773
  return data;
825
774
  }
826
- /** Global transforms applied after per-device merge: locks + action map */
827
- applyGlobalTransforms(data) {
828
- const cfg = this._config;
829
- if (cfg.lockPosition) {
830
- data = { ...data, translation: { x: 0, y: 0, z: 0 } };
831
- }
832
- if (cfg.lockRotation) {
833
- data = { ...data, rotation: { x: 0, y: 0, z: 0 } };
775
+ /** Get the effective routes for a device: device config override > device axes metadata > global default */
776
+ resolveRoutes(deviceId, device) {
777
+ const devCfg = this._config.devices[deviceId];
778
+ if (devCfg?.routes && Array.isArray(devCfg.routes)) return devCfg.routes;
779
+ for (const [pattern, cfg] of Object.entries(this._config.devices)) {
780
+ if (pattern.endsWith("*") && deviceId.startsWith(pattern.slice(0, -1))) {
781
+ if (cfg.routes && Array.isArray(cfg.routes)) return cfg.routes;
782
+ }
834
783
  }
835
- const actions = applyActionMap(data, cfg.actionMap);
836
- const spatial = actionValuesToSpatialData(actions, data.timestamp);
837
- return { spatial, actions };
784
+ if (device?.axes) return buildRoutes(device.axes);
785
+ return DEFAULT_ROUTES;
838
786
  }
839
787
  };
840
788
  var SatMouseContext = react.createContext(null);
@@ -1036,63 +984,9 @@ function formatConnectionType(type) {
1036
984
  return "";
1037
985
  }
1038
986
  }
1039
- var FLIP_AXES = ["tx", "ty", "tz", "rx", "ry", "rz"];
1040
- function mapSlider(v) {
1041
- return 1e-4 * Math.pow(500, v / 100);
1042
- }
1043
- function unmapSlider(v) {
1044
- return 100 * Math.log(v / 1e-4) / Math.log(500);
1045
- }
1046
987
  function SettingsPanel({ className }) {
1047
988
  const { config, updateConfig } = useSatMouse();
1048
- const onFlip = react.useCallback(
1049
- (axis) => {
1050
- updateConfig({ flip: { ...config.flip, [axis]: !config.flip[axis] } });
1051
- },
1052
- [config.flip, updateConfig]
1053
- );
1054
989
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className, children: [
1055
- /* @__PURE__ */ jsxRuntime.jsxs("section", { "data-section": "sensitivity", children: [
1056
- /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
1057
- "Translation",
1058
- /* @__PURE__ */ jsxRuntime.jsx(
1059
- "input",
1060
- {
1061
- type: "range",
1062
- min: 0,
1063
- max: 100,
1064
- value: Math.round(unmapSlider(config.sensitivity.translation)),
1065
- onChange: (e) => updateConfig({ sensitivity: { ...config.sensitivity, translation: mapSlider(+e.target.value) } })
1066
- }
1067
- ),
1068
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: config.sensitivity.translation.toFixed(4) })
1069
- ] }),
1070
- /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
1071
- "Rotation",
1072
- /* @__PURE__ */ jsxRuntime.jsx(
1073
- "input",
1074
- {
1075
- type: "range",
1076
- min: 0,
1077
- max: 100,
1078
- value: Math.round(unmapSlider(config.sensitivity.rotation)),
1079
- onChange: (e) => updateConfig({ sensitivity: { ...config.sensitivity, rotation: mapSlider(+e.target.value) } })
1080
- }
1081
- ),
1082
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: config.sensitivity.rotation.toFixed(4) })
1083
- ] })
1084
- ] }),
1085
- /* @__PURE__ */ jsxRuntime.jsx("section", { "data-section": "flip", children: FLIP_AXES.map((axis) => /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
1086
- /* @__PURE__ */ jsxRuntime.jsx(
1087
- "input",
1088
- {
1089
- type: "checkbox",
1090
- checked: config.flip[axis],
1091
- onChange: () => onFlip(axis)
1092
- }
1093
- ),
1094
- axis.toUpperCase()
1095
- ] }, axis)) }),
1096
990
  /* @__PURE__ */ jsxRuntime.jsxs("section", { "data-section": "toggles", children: [
1097
991
  /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
1098
992
  /* @__PURE__ */ jsxRuntime.jsx(