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