@kelnishi/satmouse-client 0.9.12 → 0.9.14

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.
@@ -0,0 +1,295 @@
1
+ import { DEFAULT_ACTION_MAP } from '../chunk-I47ZOX3M.js';
2
+
3
+ // src/elements/registry.ts
4
+ var globalManager = null;
5
+ var listeners = /* @__PURE__ */ new Set();
6
+ function registerSatMouse(manager) {
7
+ globalManager = manager;
8
+ for (const fn of listeners) fn(manager);
9
+ listeners.clear();
10
+ }
11
+ function getManager() {
12
+ return globalManager;
13
+ }
14
+ function onManagerReady(fn) {
15
+ if (globalManager) fn(globalManager);
16
+ else listeners.add(fn);
17
+ }
18
+
19
+ // src/elements/satmouse-status.ts
20
+ var TEMPLATE = `
21
+ <style>
22
+ :host { display: inline-flex; align-items: center; gap: 8px; font-family: inherit; font-size: 13px; }
23
+ .dot { width: 8px; height: 8px; border-radius: 50%; background: #e74c3c; transition: background 0.3s; }
24
+ .dot[data-state="connected"] { background: #2ecc71; }
25
+ .dot[data-state="connecting"] { background: #f39c12; }
26
+ .dot[data-state="failed"] { background: #e74c3c; }
27
+ .protocol { color: #7f8c8d; font-size: 11px; text-transform: uppercase; letter-spacing: 1px; }
28
+ .launch { padding: 4px 12px; background: #2980b9; color: #fff; border-radius: 4px; font-size: 11px;
29
+ text-decoration: none; cursor: pointer; border: none; font-family: inherit; display: none; }
30
+ .launch:hover { background: #3498db; }
31
+ </style>
32
+ <span class="dot"></span>
33
+ <span class="text">Disconnected</span>
34
+ <span class="protocol"></span>
35
+ <button class="launch">Launch SatMouse</button>
36
+ `;
37
+ var SatMouseStatus = class extends HTMLElement {
38
+ dot;
39
+ text;
40
+ proto;
41
+ launch;
42
+ disconnectTimer = null;
43
+ constructor() {
44
+ super();
45
+ const shadow = this.attachShadow({ mode: "open" });
46
+ shadow.innerHTML = TEMPLATE;
47
+ this.dot = shadow.querySelector(".dot");
48
+ this.text = shadow.querySelector(".text");
49
+ this.proto = shadow.querySelector(".protocol");
50
+ this.launch = shadow.querySelector(".launch");
51
+ this.launch.addEventListener("click", () => {
52
+ window.location.href = "satmouse://launch";
53
+ setTimeout(() => {
54
+ if (!document.hidden) {
55
+ if (confirm("SatMouse doesn't appear to be installed. Go to the download page?")) {
56
+ window.open("https://github.com/kelnishi/SatMouse/releases/latest", "_blank", "noopener");
57
+ }
58
+ }
59
+ }, 1e3);
60
+ });
61
+ }
62
+ connectedCallback() {
63
+ this.disconnectTimer = setTimeout(() => {
64
+ this.launch.style.display = "inline-block";
65
+ }, 3e3);
66
+ onManagerReady((manager) => this.bind(manager));
67
+ }
68
+ bind(manager) {
69
+ manager.on("stateChange", (state, protocol) => {
70
+ this.dot.dataset.state = state;
71
+ this.proto.textContent = protocol !== "none" ? protocol : "";
72
+ if (this.disconnectTimer) {
73
+ clearTimeout(this.disconnectTimer);
74
+ this.disconnectTimer = null;
75
+ }
76
+ if (state === "connected") {
77
+ this.text.textContent = "Connected";
78
+ this.launch.style.display = "none";
79
+ } else if (state === "connecting") {
80
+ this.text.textContent = "Connecting...";
81
+ this.launch.style.display = "none";
82
+ } else if (state === "failed") {
83
+ this.text.textContent = "Not running";
84
+ this.launch.style.display = "inline-block";
85
+ } else {
86
+ this.text.textContent = "Disconnected";
87
+ this.launch.style.display = "none";
88
+ }
89
+ });
90
+ }
91
+ };
92
+ customElements.define("satmouse-status", SatMouseStatus);
93
+
94
+ // src/elements/satmouse-devices.ts
95
+ var AXES = ["tx", "ty", "tz", "rx", "ry", "rz"];
96
+ var STYLES = `
97
+ <style>
98
+ :host { display: block; font-family: inherit; font-size: 12px; }
99
+ .panel { background: #0f3460; border: 1px solid #1a4a8a; border-radius: 6px; padding: 10px; margin-bottom: 8px; }
100
+ summary { cursor: pointer; font-weight: 600; color: #e0e0e0; font-size: 13px; }
101
+ .type { font-size: 10px; color: #7f8c8d; text-transform: uppercase; margin-left: 6px; }
102
+ .controls { margin-top: 8px; display: flex; flex-direction: column; gap: 6px; }
103
+ .slider-row { display: flex; align-items: center; gap: 6px; }
104
+ .slider-row label { color: #7f8c8d; font-weight: 600; width: 38px; flex-shrink: 0; }
105
+ .slider-row input[type="range"] { flex: 1; min-width: 0; height: 4px; accent-color: #3498db; }
106
+ .slider-row span { color: #7f8c8d; font-family: monospace; font-size: 10px; min-width: 44px; text-align: right; }
107
+ .flip-group { display: flex; flex-direction: column; gap: 4px; }
108
+ .flip-row { display: flex; gap: 8px; }
109
+ .flip-row label { display: flex; align-items: center; gap: 2px; color: #7f8c8d; min-width: 36px; }
110
+ .flip-row input { accent-color: #e74c3c; }
111
+ .remap-group { display: grid; grid-template-columns: 1fr 1fr; gap: 3px 8px; }
112
+ .remap-row { display: flex; gap: 4px; align-items: center; }
113
+ .remap-row label { color: #7f8c8d; width: 24px; flex-shrink: 0; }
114
+ .remap-row select { background: #16213e; color: #e0e0e0; border: 1px solid #1a4a8a; border-radius: 3px;
115
+ font-size: 11px; padding: 1px 4px; flex: 1; min-width: 0; }
116
+ .empty { color: #7f8c8d; font-style: italic; }
117
+ </style>
118
+ `;
119
+ function mapSlider(v) {
120
+ return 1e-4 * Math.pow(500, v / 100);
121
+ }
122
+ function unmapSlider(v) {
123
+ return 100 * Math.log(v / 1e-4) / Math.log(500);
124
+ }
125
+ var SatMouseDevices = class extends HTMLElement {
126
+ manager = null;
127
+ container;
128
+ constructor() {
129
+ super();
130
+ const shadow = this.attachShadow({ mode: "open" });
131
+ shadow.innerHTML = STYLES + `<div class="container"><span class="empty">No devices</span></div>`;
132
+ this.container = shadow.querySelector(".container");
133
+ }
134
+ connectedCallback() {
135
+ onManagerReady((manager) => {
136
+ this.manager = manager;
137
+ manager.on("deviceStatus", (event, device) => {
138
+ if (event === "connected") this.addDevice(device);
139
+ else this.removeDevice(device);
140
+ });
141
+ manager.on("stateChange", (state) => {
142
+ if (state === "connected") {
143
+ manager.fetchDeviceInfo().then((devices) => devices.forEach((d) => this.addDevice(d)));
144
+ }
145
+ });
146
+ });
147
+ }
148
+ addDevice(device) {
149
+ if (this.shadowRoot.getElementById(`dev-${device.id}`)) return;
150
+ const empty = this.container.querySelector(".empty");
151
+ if (empty) empty.remove();
152
+ const mgr = this.manager;
153
+ const cfg = mgr.getDeviceConfig(device.id);
154
+ const panel = document.createElement("details");
155
+ panel.className = "panel";
156
+ panel.id = `dev-${device.id}`;
157
+ panel.open = true;
158
+ const summary = document.createElement("summary");
159
+ summary.innerHTML = `${device.model ?? device.name}<span class="type">${device.connectionType ?? ""}</span>`;
160
+ panel.appendChild(summary);
161
+ const controls = document.createElement("div");
162
+ controls.className = "controls";
163
+ for (const type of ["translation", "rotation"]) {
164
+ const row = document.createElement("div");
165
+ row.className = "slider-row";
166
+ const val = cfg.sensitivity?.[type] ?? mgr.config.sensitivity[type];
167
+ row.innerHTML = `<label>${type === "translation" ? "Trans" : "Rot"}</label><input type="range" min="0" max="100" value="${Math.round(unmapSlider(val))}"><span>${val.toFixed(4)}</span>`;
168
+ const slider = row.querySelector("input");
169
+ const span = row.querySelector("span");
170
+ slider.addEventListener("input", () => {
171
+ const v = mapSlider(+slider.value);
172
+ span.textContent = v.toFixed(4);
173
+ mgr.updateDeviceConfig(device.id, {
174
+ sensitivity: { ...mgr.getDeviceConfig(device.id).sensitivity, [type]: v }
175
+ });
176
+ });
177
+ controls.appendChild(row);
178
+ }
179
+ const flipGroup = document.createElement("div");
180
+ flipGroup.className = "flip-group";
181
+ for (const group of [["tx", "ty", "tz"], ["rx", "ry", "rz"]]) {
182
+ const row = document.createElement("div");
183
+ row.className = "flip-row";
184
+ for (const axis of group) {
185
+ const label = document.createElement("label");
186
+ const cb = document.createElement("input");
187
+ cb.type = "checkbox";
188
+ cb.checked = cfg.flip?.[axis] ?? mgr.config.flip[axis];
189
+ cb.addEventListener("change", () => {
190
+ mgr.updateDeviceConfig(device.id, {
191
+ flip: { ...mgr.getDeviceConfig(device.id).flip, [axis]: cb.checked }
192
+ });
193
+ });
194
+ label.appendChild(cb);
195
+ label.appendChild(document.createTextNode(axis.toUpperCase()));
196
+ row.appendChild(label);
197
+ }
198
+ flipGroup.appendChild(row);
199
+ }
200
+ controls.appendChild(flipGroup);
201
+ const remapGroup = document.createElement("div");
202
+ remapGroup.className = "remap-group";
203
+ const actionMap = cfg.actionMap ?? mgr.config.actionMap;
204
+ for (const action of AXES) {
205
+ const row = document.createElement("div");
206
+ row.className = "remap-row";
207
+ row.innerHTML = `<label>${action.toUpperCase()}</label>`;
208
+ const sel = document.createElement("select");
209
+ for (const src of AXES) {
210
+ const opt = document.createElement("option");
211
+ opt.value = src;
212
+ opt.textContent = src.toUpperCase();
213
+ if (actionMap[action]?.source === src) opt.selected = true;
214
+ sel.appendChild(opt);
215
+ }
216
+ sel.addEventListener("change", () => {
217
+ const current = mgr.getDeviceConfig(device.id).actionMap ?? { ...DEFAULT_ACTION_MAP };
218
+ current[action] = { ...current[action], source: sel.value };
219
+ mgr.updateDeviceConfig(device.id, { actionMap: current });
220
+ });
221
+ row.appendChild(sel);
222
+ remapGroup.appendChild(row);
223
+ }
224
+ controls.appendChild(remapGroup);
225
+ panel.appendChild(controls);
226
+ this.container.appendChild(panel);
227
+ }
228
+ removeDevice(device) {
229
+ this.shadowRoot.getElementById(`dev-${device.id}`)?.remove();
230
+ if (this.container.children.length === 0) {
231
+ this.container.innerHTML = `<span class="empty">No devices</span>`;
232
+ }
233
+ }
234
+ };
235
+ customElements.define("satmouse-devices", SatMouseDevices);
236
+
237
+ // src/elements/satmouse-debug.ts
238
+ var TEMPLATE2 = `
239
+ <style>
240
+ :host { display: block; font-family: monospace; font-size: 12px; }
241
+ .row { display: flex; justify-content: space-between; padding: 2px 0; }
242
+ .label { color: #7f8c8d; font-weight: 600; width: 28px; }
243
+ .value { color: #3498db; text-align: right; min-width: 50px; }
244
+ .meta { color: #7f8c8d; font-size: 11px; padding: 2px 0; }
245
+ </style>
246
+ <div class="meta"><span class="state">Disconnected</span> \xB7 <span class="protocol"></span> \xB7 <span class="fps">0</span> fps</div>
247
+ <div class="row"><span class="label">TX</span><span class="value" id="tx">0</span></div>
248
+ <div class="row"><span class="label">TY</span><span class="value" id="ty">0</span></div>
249
+ <div class="row"><span class="label">TZ</span><span class="value" id="tz">0</span></div>
250
+ <div class="row"><span class="label">RX</span><span class="value" id="rx">0</span></div>
251
+ <div class="row"><span class="label">RY</span><span class="value" id="ry">0</span></div>
252
+ <div class="row"><span class="label">RZ</span><span class="value" id="rz">0</span></div>
253
+ `;
254
+ var SatMouseDebug = class extends HTMLElement {
255
+ els = {};
256
+ frameCount = 0;
257
+ constructor() {
258
+ super();
259
+ const shadow = this.attachShadow({ mode: "open" });
260
+ shadow.innerHTML = TEMPLATE2;
261
+ for (const id of ["tx", "ty", "tz", "rx", "ry", "rz"]) {
262
+ this.els[id] = shadow.getElementById(id);
263
+ }
264
+ this.els.state = shadow.querySelector(".state");
265
+ this.els.protocol = shadow.querySelector(".protocol");
266
+ this.els.fps = shadow.querySelector(".fps");
267
+ }
268
+ connectedCallback() {
269
+ onManagerReady((manager) => this.bind(manager));
270
+ }
271
+ bind(manager) {
272
+ manager.on("rawSpatialData", (data) => {
273
+ this.frameCount++;
274
+ this.els.tx.textContent = String(Math.round(data.translation.x));
275
+ this.els.ty.textContent = String(Math.round(data.translation.y));
276
+ this.els.tz.textContent = String(Math.round(data.translation.z));
277
+ this.els.rx.textContent = String(Math.round(data.rotation.x));
278
+ this.els.ry.textContent = String(Math.round(data.rotation.y));
279
+ this.els.rz.textContent = String(Math.round(data.rotation.z));
280
+ });
281
+ manager.on("stateChange", (state, protocol) => {
282
+ this.els.state.textContent = state;
283
+ this.els.protocol.textContent = protocol !== "none" ? protocol : "";
284
+ });
285
+ setInterval(() => {
286
+ this.els.fps.textContent = String(this.frameCount);
287
+ this.frameCount = 0;
288
+ }, 1e3);
289
+ }
290
+ };
291
+ customElements.define("satmouse-debug", SatMouseDebug);
292
+
293
+ export { SatMouseDebug, SatMouseDevices, SatMouseStatus, getManager, registerSatMouse };
294
+ //# sourceMappingURL=index.js.map
295
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/elements/registry.ts","../../src/elements/satmouse-status.ts","../../src/elements/satmouse-devices.ts","../../src/elements/satmouse-debug.ts"],"names":["TEMPLATE"],"mappings":"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","file":"index.js","sourcesContent":["import type { InputManager } from \"../utils/input-manager.js\";\n\n/**\n * Global registry for SatMouse Web Components.\n *\n * Usage:\n * import { registerSatMouse } from \"@kelnishi/satmouse-client/elements\";\n * registerSatMouse(manager);\n * // All <satmouse-*> elements auto-connect to this manager\n */\n\nlet globalManager: InputManager | null = null;\nconst listeners = new Set<(m: InputManager) => void>();\n\nexport function registerSatMouse(manager: InputManager): void {\n globalManager = manager;\n for (const fn of listeners) fn(manager);\n listeners.clear();\n}\n\nexport function getManager(): InputManager | null {\n return globalManager;\n}\n\nexport function onManagerReady(fn: (m: InputManager) => void): void {\n if (globalManager) fn(globalManager);\n else listeners.add(fn);\n}\n","import { onManagerReady } from \"./registry.js\";\nimport type { InputManager } from \"../utils/input-manager.js\";\nimport type { ConnectionState, TransportProtocol } from \"../core/types.js\";\n\nconst TEMPLATE = `\n<style>\n :host { display: inline-flex; align-items: center; gap: 8px; font-family: inherit; font-size: 13px; }\n .dot { width: 8px; height: 8px; border-radius: 50%; background: #e74c3c; transition: background 0.3s; }\n .dot[data-state=\"connected\"] { background: #2ecc71; }\n .dot[data-state=\"connecting\"] { background: #f39c12; }\n .dot[data-state=\"failed\"] { background: #e74c3c; }\n .protocol { color: #7f8c8d; font-size: 11px; text-transform: uppercase; letter-spacing: 1px; }\n .launch { padding: 4px 12px; background: #2980b9; color: #fff; border-radius: 4px; font-size: 11px;\n text-decoration: none; cursor: pointer; border: none; font-family: inherit; display: none; }\n .launch:hover { background: #3498db; }\n</style>\n<span class=\"dot\"></span>\n<span class=\"text\">Disconnected</span>\n<span class=\"protocol\"></span>\n<button class=\"launch\">Launch SatMouse</button>\n`;\n\nexport class SatMouseStatus extends HTMLElement {\n private dot!: HTMLElement;\n private text!: HTMLElement;\n private proto!: HTMLElement;\n private launch!: HTMLButtonElement;\n private disconnectTimer: ReturnType<typeof setTimeout> | null = null;\n\n constructor() {\n super();\n const shadow = this.attachShadow({ mode: \"open\" });\n shadow.innerHTML = TEMPLATE;\n this.dot = shadow.querySelector(\".dot\")!;\n this.text = shadow.querySelector(\".text\")!;\n this.proto = shadow.querySelector(\".protocol\")!;\n this.launch = shadow.querySelector(\".launch\")!;\n\n this.launch.addEventListener(\"click\", () => {\n // Try the URL scheme — if installed, the OS opens the app\n window.location.href = \"satmouse://launch\";\n // If we're still here after 1s, the scheme wasn't handled\n setTimeout(() => {\n if (!document.hidden) {\n if (confirm(\"SatMouse doesn't appear to be installed. Go to the download page?\")) {\n window.open(\"https://github.com/kelnishi/SatMouse/releases/latest\", \"_blank\", \"noopener\");\n }\n }\n }, 1000);\n });\n }\n\n connectedCallback() {\n // Show launch button if still disconnected after 3s (covers cold page load)\n this.disconnectTimer = setTimeout(() => { this.launch.style.display = \"inline-block\"; }, 3000);\n onManagerReady((manager) => this.bind(manager));\n }\n\n private bind(manager: InputManager): void {\n manager.on(\"stateChange\", (state: ConnectionState, protocol: TransportProtocol) => {\n this.dot.dataset.state = state;\n this.proto.textContent = protocol !== \"none\" ? protocol : \"\";\n\n if (this.disconnectTimer) { clearTimeout(this.disconnectTimer); this.disconnectTimer = null; }\n\n if (state === \"connected\") {\n this.text.textContent = \"Connected\";\n this.launch.style.display = \"none\";\n } else if (state === \"connecting\") {\n this.text.textContent = \"Connecting...\";\n this.launch.style.display = \"none\";\n } else if (state === \"failed\") {\n this.text.textContent = \"Not running\";\n this.launch.style.display = \"inline-block\";\n } else {\n this.text.textContent = \"Disconnected\";\n this.launch.style.display = \"none\";\n }\n });\n }\n}\n\ncustomElements.define(\"satmouse-status\", SatMouseStatus);\n","import { onManagerReady } from \"./registry.js\";\nimport type { InputManager } from \"../utils/input-manager.js\";\nimport type { DeviceInfo } from \"../core/types.js\";\nimport type { InputAxis } from \"../utils/action-map.js\";\nimport { DEFAULT_ACTION_MAP } from \"../utils/action-map.js\";\n\nconst AXES: InputAxis[] = [\"tx\", \"ty\", \"tz\", \"rx\", \"ry\", \"rz\"];\n\nconst STYLES = `\n<style>\n :host { display: block; font-family: inherit; font-size: 12px; }\n .panel { background: #0f3460; border: 1px solid #1a4a8a; border-radius: 6px; padding: 10px; margin-bottom: 8px; }\n summary { cursor: pointer; font-weight: 600; color: #e0e0e0; font-size: 13px; }\n .type { font-size: 10px; color: #7f8c8d; text-transform: uppercase; margin-left: 6px; }\n .controls { margin-top: 8px; display: flex; flex-direction: column; gap: 6px; }\n .slider-row { display: flex; align-items: center; gap: 6px; }\n .slider-row label { color: #7f8c8d; font-weight: 600; width: 38px; flex-shrink: 0; }\n .slider-row input[type=\"range\"] { flex: 1; min-width: 0; height: 4px; accent-color: #3498db; }\n .slider-row span { color: #7f8c8d; font-family: monospace; font-size: 10px; min-width: 44px; text-align: right; }\n .flip-group { display: flex; flex-direction: column; gap: 4px; }\n .flip-row { display: flex; gap: 8px; }\n .flip-row label { display: flex; align-items: center; gap: 2px; color: #7f8c8d; min-width: 36px; }\n .flip-row input { accent-color: #e74c3c; }\n .remap-group { display: grid; grid-template-columns: 1fr 1fr; gap: 3px 8px; }\n .remap-row { display: flex; gap: 4px; align-items: center; }\n .remap-row label { color: #7f8c8d; width: 24px; flex-shrink: 0; }\n .remap-row select { background: #16213e; color: #e0e0e0; border: 1px solid #1a4a8a; border-radius: 3px;\n font-size: 11px; padding: 1px 4px; flex: 1; min-width: 0; }\n .empty { color: #7f8c8d; font-style: italic; }\n</style>\n`;\n\nfunction mapSlider(v: number): number { return 0.0001 * Math.pow(500, v / 100); }\nfunction unmapSlider(v: number): number { return (100 * Math.log(v / 0.0001)) / Math.log(500); }\n\nexport class SatMouseDevices extends HTMLElement {\n private manager: InputManager | null = null;\n private container!: HTMLElement;\n\n constructor() {\n super();\n const shadow = this.attachShadow({ mode: \"open\" });\n shadow.innerHTML = STYLES + `<div class=\"container\"><span class=\"empty\">No devices</span></div>`;\n this.container = shadow.querySelector(\".container\")!;\n }\n\n connectedCallback() {\n onManagerReady((manager) => {\n this.manager = manager;\n manager.on(\"deviceStatus\", (event, device) => {\n if (event === \"connected\") this.addDevice(device);\n else this.removeDevice(device);\n });\n manager.on(\"stateChange\", (state) => {\n if (state === \"connected\") {\n manager.fetchDeviceInfo().then((devices) => devices.forEach((d) => this.addDevice(d)));\n }\n });\n });\n }\n\n private addDevice(device: DeviceInfo): void {\n if (this.shadowRoot!.getElementById(`dev-${device.id}`)) return;\n const empty = this.container.querySelector(\".empty\");\n if (empty) empty.remove();\n\n const mgr = this.manager!;\n const cfg = mgr.getDeviceConfig(device.id);\n\n const panel = document.createElement(\"details\");\n panel.className = \"panel\";\n panel.id = `dev-${device.id}`;\n panel.open = true;\n\n const summary = document.createElement(\"summary\");\n summary.innerHTML = `${device.model ?? device.name}<span class=\"type\">${device.connectionType ?? \"\"}</span>`;\n panel.appendChild(summary);\n\n const controls = document.createElement(\"div\");\n controls.className = \"controls\";\n\n // Sensitivity sliders\n for (const type of [\"translation\", \"rotation\"] as const) {\n const row = document.createElement(\"div\");\n row.className = \"slider-row\";\n const val = cfg.sensitivity?.[type] ?? mgr.config.sensitivity[type];\n row.innerHTML = `<label>${type === \"translation\" ? \"Trans\" : \"Rot\"}</label>` +\n `<input type=\"range\" min=\"0\" max=\"100\" value=\"${Math.round(unmapSlider(val))}\">` +\n `<span>${val.toFixed(4)}</span>`;\n const slider = row.querySelector(\"input\")! as HTMLInputElement;\n const span = row.querySelector(\"span\")!;\n slider.addEventListener(\"input\", () => {\n const v = mapSlider(+slider.value);\n span.textContent = v.toFixed(4);\n mgr.updateDeviceConfig(device.id, {\n sensitivity: { ...mgr.getDeviceConfig(device.id).sensitivity, [type]: v },\n });\n });\n controls.appendChild(row);\n }\n\n // Flip checkboxes\n const flipGroup = document.createElement(\"div\");\n flipGroup.className = \"flip-group\";\n for (const group of [[\"tx\", \"ty\", \"tz\"], [\"rx\", \"ry\", \"rz\"]] as InputAxis[][]) {\n const row = document.createElement(\"div\");\n row.className = \"flip-row\";\n for (const axis of group) {\n const label = document.createElement(\"label\");\n const cb = document.createElement(\"input\");\n cb.type = \"checkbox\";\n cb.checked = cfg.flip?.[axis] ?? mgr.config.flip[axis];\n cb.addEventListener(\"change\", () => {\n mgr.updateDeviceConfig(device.id, {\n flip: { ...mgr.getDeviceConfig(device.id).flip, [axis]: cb.checked },\n });\n });\n label.appendChild(cb);\n label.appendChild(document.createTextNode(axis.toUpperCase()));\n row.appendChild(label);\n }\n flipGroup.appendChild(row);\n }\n controls.appendChild(flipGroup);\n\n // Axis remap\n const remapGroup = document.createElement(\"div\");\n remapGroup.className = \"remap-group\";\n const actionMap = cfg.actionMap ?? mgr.config.actionMap;\n for (const action of AXES) {\n const row = document.createElement(\"div\");\n row.className = \"remap-row\";\n row.innerHTML = `<label>${action.toUpperCase()}</label>`;\n const sel = document.createElement(\"select\");\n for (const src of AXES) {\n const opt = document.createElement(\"option\");\n opt.value = src;\n opt.textContent = src.toUpperCase();\n if (actionMap[action]?.source === src) opt.selected = true;\n sel.appendChild(opt);\n }\n sel.addEventListener(\"change\", () => {\n const current = mgr.getDeviceConfig(device.id).actionMap ?? { ...DEFAULT_ACTION_MAP };\n current[action] = { ...current[action], source: sel.value as InputAxis };\n mgr.updateDeviceConfig(device.id, { actionMap: current });\n });\n row.appendChild(sel);\n remapGroup.appendChild(row);\n }\n controls.appendChild(remapGroup);\n\n panel.appendChild(controls);\n this.container.appendChild(panel);\n }\n\n private removeDevice(device: DeviceInfo): void {\n this.shadowRoot!.getElementById(`dev-${device.id}`)?.remove();\n if (this.container.children.length === 0) {\n this.container.innerHTML = `<span class=\"empty\">No devices</span>`;\n }\n }\n}\n\ncustomElements.define(\"satmouse-devices\", SatMouseDevices);\n","import { onManagerReady } from \"./registry.js\";\nimport type { InputManager } from \"../utils/input-manager.js\";\nimport type { SpatialData, ConnectionState, TransportProtocol } from \"../core/types.js\";\n\nconst TEMPLATE = `\n<style>\n :host { display: block; font-family: monospace; font-size: 12px; }\n .row { display: flex; justify-content: space-between; padding: 2px 0; }\n .label { color: #7f8c8d; font-weight: 600; width: 28px; }\n .value { color: #3498db; text-align: right; min-width: 50px; }\n .meta { color: #7f8c8d; font-size: 11px; padding: 2px 0; }\n</style>\n<div class=\"meta\"><span class=\"state\">Disconnected</span> · <span class=\"protocol\"></span> · <span class=\"fps\">0</span> fps</div>\n<div class=\"row\"><span class=\"label\">TX</span><span class=\"value\" id=\"tx\">0</span></div>\n<div class=\"row\"><span class=\"label\">TY</span><span class=\"value\" id=\"ty\">0</span></div>\n<div class=\"row\"><span class=\"label\">TZ</span><span class=\"value\" id=\"tz\">0</span></div>\n<div class=\"row\"><span class=\"label\">RX</span><span class=\"value\" id=\"rx\">0</span></div>\n<div class=\"row\"><span class=\"label\">RY</span><span class=\"value\" id=\"ry\">0</span></div>\n<div class=\"row\"><span class=\"label\">RZ</span><span class=\"value\" id=\"rz\">0</span></div>\n`;\n\nexport class SatMouseDebug extends HTMLElement {\n private els: Record<string, HTMLElement> = {};\n private frameCount = 0;\n\n constructor() {\n super();\n const shadow = this.attachShadow({ mode: \"open\" });\n shadow.innerHTML = TEMPLATE;\n for (const id of [\"tx\", \"ty\", \"tz\", \"rx\", \"ry\", \"rz\"]) {\n this.els[id] = shadow.getElementById(id)!;\n }\n this.els.state = shadow.querySelector(\".state\")!;\n this.els.protocol = shadow.querySelector(\".protocol\")!;\n this.els.fps = shadow.querySelector(\".fps\")!;\n }\n\n connectedCallback() {\n onManagerReady((manager) => this.bind(manager));\n }\n\n private bind(manager: InputManager): void {\n manager.on(\"rawSpatialData\", (data: SpatialData) => {\n this.frameCount++;\n this.els.tx.textContent = String(Math.round(data.translation.x));\n this.els.ty.textContent = String(Math.round(data.translation.y));\n this.els.tz.textContent = String(Math.round(data.translation.z));\n this.els.rx.textContent = String(Math.round(data.rotation.x));\n this.els.ry.textContent = String(Math.round(data.rotation.y));\n this.els.rz.textContent = String(Math.round(data.rotation.z));\n });\n\n manager.on(\"stateChange\", (state: ConnectionState, protocol: TransportProtocol) => {\n this.els.state.textContent = state;\n this.els.protocol.textContent = protocol !== \"none\" ? protocol : \"\";\n });\n\n setInterval(() => {\n this.els.fps.textContent = String(this.frameCount);\n this.frameCount = 0;\n }, 1000);\n }\n}\n\ncustomElements.define(\"satmouse-debug\", SatMouseDebug);\n"]}
@@ -269,6 +269,7 @@ function parseSatMouseUri(uri) {
269
269
  var DEFAULT_OPTIONS = {
270
270
  transports: ["webtransport", "websocket"],
271
271
  reconnectDelay: 2e3,
272
+ maxRetries: 3,
272
273
  wsSubprotocol: "satmouse-json"
273
274
  };
274
275
  var SatMouseConnection = class extends TypedEmitter {
@@ -277,6 +278,7 @@ var SatMouseConnection = class extends TypedEmitter {
277
278
  reconnectTimer = null;
278
279
  intentionalClose = false;
279
280
  deviceInfoUrl = null;
281
+ retryCount = 0;
280
282
  _state = "disconnected";
281
283
  _protocol = "none";
282
284
  get state() {
@@ -302,7 +304,7 @@ var SatMouseConnection = class extends TypedEmitter {
302
304
  this.options.tdUrl = this.options.tdUrl ?? parsed.tdUrl;
303
305
  }
304
306
  if (!wtUrl && !wsUrl) {
305
- const tdUrl = this.options.tdUrl ?? new URL("/td.json", globalThis.location?.origin ?? "http://localhost:18945").href;
307
+ const tdUrl = this.options.tdUrl ?? "http://localhost:18945/td.json";
306
308
  try {
307
309
  const td = await fetchThingDescription(tdUrl);
308
310
  const endpoints = resolveEndpoints(td);
@@ -312,7 +314,7 @@ var SatMouseConnection = class extends TypedEmitter {
312
314
  this.deviceInfoUrl = endpoints.deviceInfoUrl ?? null;
313
315
  } catch (err) {
314
316
  this.emit("error", err instanceof Error ? err : new Error(String(err)));
315
- wsUrl = `ws://${globalThis.location?.hostname ?? "localhost"}:${globalThis.location?.port ?? "18945"}/spatial`;
317
+ wsUrl = "ws://localhost:18945/spatial";
316
318
  }
317
319
  }
318
320
  for (const proto of this.options.transports) {
@@ -337,6 +339,12 @@ var SatMouseConnection = class extends TypedEmitter {
337
339
  this.setState("disconnected", "none");
338
340
  this.scheduleReconnect();
339
341
  }
342
+ /** Reset retry count and reconnect. Use after "failed" state. */
343
+ retry() {
344
+ this.retryCount = 0;
345
+ this.intentionalClose = false;
346
+ this.connect();
347
+ }
340
348
  disconnect() {
341
349
  this.intentionalClose = true;
342
350
  this.clearReconnect();
@@ -368,6 +376,7 @@ var SatMouseConnection = class extends TypedEmitter {
368
376
  try {
369
377
  await adapter.connect();
370
378
  this.transport = adapter;
379
+ this.retryCount = 0;
371
380
  this.setState("connected", adapter.protocol);
372
381
  return true;
373
382
  } catch (err) {
@@ -383,6 +392,13 @@ var SatMouseConnection = class extends TypedEmitter {
383
392
  }
384
393
  scheduleReconnect() {
385
394
  if (this.options.reconnectDelay <= 0 || this.intentionalClose) return;
395
+ this.retryCount++;
396
+ console.log(`[SatMouse] Reconnect attempt ${this.retryCount}/${this.options.maxRetries}`);
397
+ if (this.retryCount > this.options.maxRetries) {
398
+ console.log("[SatMouse] Max retries exceeded, giving up");
399
+ this.setState("failed", "none");
400
+ return;
401
+ }
386
402
  this.clearReconnect();
387
403
  this.reconnectTimer = setTimeout(() => {
388
404
  this.reconnectTimer = null;
@@ -481,14 +497,17 @@ function actionValuesToSpatialData(values, timestamp) {
481
497
  // src/utils/config.ts
482
498
  var DEFAULT_CONFIG = {
483
499
  sensitivity: { translation: 1e-3, rotation: 1e-3 },
484
- flip: { tx: false, ty: true, tz: true, rx: false, ry: true, rz: true },
500
+ flip: { tx: false, ty: false, tz: false, rx: false, ry: false, rz: false },
485
501
  deadZone: 0,
486
502
  dominant: false,
487
503
  axisRemap: { tx: "x", ty: "y", tz: "z", rx: "x", ry: "y", rz: "z" },
488
504
  lockPosition: false,
489
505
  lockRotation: false,
490
506
  actionMap: { ...DEFAULT_ACTION_MAP },
491
- devices: {}
507
+ devices: {
508
+ // SpaceMouse Z-up → Three.js Y-up axis correction
509
+ "cnx-*": { flip: { ty: true, tz: true, ry: true, rz: true } }
510
+ }
492
511
  };
493
512
  function mergeConfig(base, partial) {
494
513
  const merged = {
@@ -652,8 +671,8 @@ var InputManager = class extends TypedEmitter {
652
671
  connections = [];
653
672
  storage;
654
673
  knownDevices = /* @__PURE__ */ new Map();
655
- // Accumulator: sums all device inputs per frame tick
656
- accumulator = { tx: 0, ty: 0, tz: 0, rx: 0, ry: 0, rz: 0 };
674
+ // Per-device accumulators: latest value from each device per frame tick
675
+ deviceAccumulators = /* @__PURE__ */ new Map();
657
676
  accDirty = false;
658
677
  flushTimer = null;
659
678
  _config;
@@ -751,13 +770,16 @@ var InputManager = class extends TypedEmitter {
751
770
  wireConnection(connection) {
752
771
  connection.on("spatialData", (raw) => {
753
772
  this.emit("rawSpatialData", raw);
754
- const acc = this.accumulator;
755
- acc.tx = Math.abs(raw.translation.x) > Math.abs(acc.tx) ? raw.translation.x : acc.tx;
756
- acc.ty = Math.abs(raw.translation.y) > Math.abs(acc.ty) ? raw.translation.y : acc.ty;
757
- acc.tz = Math.abs(raw.translation.z) > Math.abs(acc.tz) ? raw.translation.z : acc.tz;
758
- acc.rx = Math.abs(raw.rotation.x) > Math.abs(acc.rx) ? raw.rotation.x : acc.rx;
759
- acc.ry = Math.abs(raw.rotation.y) > Math.abs(acc.ry) ? raw.rotation.y : acc.ry;
760
- acc.rz = Math.abs(raw.rotation.z) > Math.abs(acc.rz) ? raw.rotation.z : acc.rz;
773
+ const id = raw.deviceId ?? "_default";
774
+ const processed = this.processPerDevice(raw, id);
775
+ this.deviceAccumulators.set(id, {
776
+ tx: processed.translation.x,
777
+ ty: processed.translation.y,
778
+ tz: processed.translation.z,
779
+ rx: processed.rotation.x,
780
+ ry: processed.rotation.y,
781
+ rz: processed.rotation.z
782
+ });
761
783
  this.accDirty = true;
762
784
  });
763
785
  connection.on("buttonEvent", (event) => this.emit("buttonEvent", event));
@@ -770,25 +792,40 @@ var InputManager = class extends TypedEmitter {
770
792
  }
771
793
  flushAccumulator() {
772
794
  if (!this.accDirty) return;
773
- const raw = {
774
- translation: { x: this.accumulator.tx, y: this.accumulator.ty, z: this.accumulator.tz },
775
- rotation: { x: this.accumulator.rx, y: this.accumulator.ry, z: this.accumulator.rz },
795
+ const merged = { tx: 0, ty: 0, tz: 0, rx: 0, ry: 0, rz: 0 };
796
+ for (const acc of this.deviceAccumulators.values()) {
797
+ merged.tx += acc.tx;
798
+ merged.ty += acc.ty;
799
+ merged.tz += acc.tz;
800
+ merged.rx += acc.rx;
801
+ merged.ry += acc.ry;
802
+ merged.rz += acc.rz;
803
+ }
804
+ this.deviceAccumulators.clear();
805
+ this.accDirty = false;
806
+ const data = {
807
+ translation: { x: merged.tx, y: merged.ty, z: merged.tz },
808
+ rotation: { x: merged.rx, y: merged.ry, z: merged.rz },
776
809
  timestamp: performance.now() * 1e3
777
810
  };
778
- this.accumulator = { tx: 0, ty: 0, tz: 0, rx: 0, ry: 0, rz: 0 };
779
- this.accDirty = false;
780
- const { spatial, actions } = this.processSpatialData(raw);
811
+ const { spatial, actions } = this.applyGlobalTransforms(data);
781
812
  if (spatial) this.emit("spatialData", spatial);
782
813
  if (actions) this.emit("actionValues", actions);
783
814
  }
784
- processSpatialData(raw) {
785
- const cfg = this._config;
815
+ /** Per-device transforms: flip, sensitivity, dead zone, dominant, axis remap */
816
+ processPerDevice(raw, deviceId) {
817
+ const cfg = resolveDeviceConfig(this._config, deviceId);
786
818
  let data = raw;
787
819
  if (cfg.deadZone > 0) data = applyDeadZone(data, cfg.deadZone);
788
820
  if (cfg.dominant) data = applyDominant(data);
789
821
  data = applyFlip(data, cfg.flip);
790
822
  data = applyAxisRemap(data, cfg.axisRemap);
791
823
  data = applySensitivity(data, cfg.sensitivity);
824
+ return data;
825
+ }
826
+ /** Global transforms applied after per-device merge: locks + action map */
827
+ applyGlobalTransforms(data) {
828
+ const cfg = this._config;
792
829
  if (cfg.lockPosition) {
793
830
  data = { ...data, translation: { x: 0, y: 0, z: 0 } };
794
831
  }