@kelnishi/satmouse-client 0.1.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.
@@ -0,0 +1,992 @@
1
+ import { createContext, useRef, useState, useEffect, useMemo, useContext, useCallback } from 'react';
2
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
3
+
4
+ // src/react/context.tsx
5
+
6
+ // src/core/emitter.ts
7
+ var TypedEmitter = class {
8
+ listeners = /* @__PURE__ */ new Map();
9
+ on(event, listener) {
10
+ let set = this.listeners.get(event);
11
+ if (!set) {
12
+ set = /* @__PURE__ */ new Set();
13
+ this.listeners.set(event, set);
14
+ }
15
+ set.add(listener);
16
+ return this;
17
+ }
18
+ off(event, listener) {
19
+ this.listeners.get(event)?.delete(listener);
20
+ return this;
21
+ }
22
+ emit(event, ...args) {
23
+ const set = this.listeners.get(event);
24
+ if (set) {
25
+ for (const fn of set) {
26
+ fn(...args);
27
+ }
28
+ }
29
+ }
30
+ removeAllListeners() {
31
+ this.listeners.clear();
32
+ }
33
+ };
34
+
35
+ // src/core/discovery.ts
36
+ async function fetchThingDescription(tdUrl) {
37
+ const res = await globalThis.fetch(tdUrl);
38
+ if (!res.ok) throw new Error(`Failed to fetch TD: HTTP ${res.status}`);
39
+ return res.json();
40
+ }
41
+ function resolveEndpoints(td) {
42
+ const result = {};
43
+ const spatialForms = td.events?.spatialData?.forms ?? [];
44
+ const wtForm = spatialForms.find((f) => f.subprotocol === "webtransport");
45
+ if (wtForm) {
46
+ result.webtransport = {
47
+ url: wtForm.href,
48
+ certHash: td["satmouse:certHash"]
49
+ };
50
+ }
51
+ const wsForm = spatialForms.find((f) => f.subprotocol === "websocket");
52
+ if (wsForm) {
53
+ result.websocket = { url: wsForm.href };
54
+ }
55
+ const deviceForm = td.properties?.deviceInfo?.forms?.[0];
56
+ if (deviceForm) {
57
+ result.deviceInfoUrl = deviceForm.href;
58
+ }
59
+ return result;
60
+ }
61
+
62
+ // src/core/decode.ts
63
+ function decodeBinaryFrame(buffer) {
64
+ const ab = buffer instanceof ArrayBuffer ? buffer : buffer.buffer;
65
+ const offset = buffer instanceof Uint8Array ? buffer.byteOffset : 0;
66
+ const view = new DataView(ab, offset);
67
+ return {
68
+ translation: {
69
+ x: view.getInt16(8, true),
70
+ y: view.getInt16(10, true),
71
+ z: view.getInt16(12, true)
72
+ },
73
+ rotation: {
74
+ x: view.getInt16(14, true),
75
+ y: view.getInt16(16, true),
76
+ z: view.getInt16(18, true)
77
+ },
78
+ timestamp: view.getFloat64(0, true)
79
+ };
80
+ }
81
+ function decodeWsBinaryFrame(buffer) {
82
+ const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
83
+ if (bytes.length < 1) return null;
84
+ const typePrefix = bytes[0];
85
+ if (typePrefix === 1 && bytes.length >= 25) {
86
+ return { type: "spatialData", data: decodeBinaryFrame(bytes.subarray(1, 25)) };
87
+ }
88
+ if (typePrefix === 2) {
89
+ const json = new TextDecoder().decode(bytes.subarray(1));
90
+ return { type: "buttonEvent", data: JSON.parse(json) };
91
+ }
92
+ return null;
93
+ }
94
+ function decodeButtonStream(buffer) {
95
+ const events = [];
96
+ let pos = 0;
97
+ while (pos + 4 <= buffer.length) {
98
+ const view = new DataView(buffer.buffer, buffer.byteOffset + pos);
99
+ const len = view.getUint32(0, true);
100
+ if (pos + 4 + len > buffer.length) break;
101
+ const json = new TextDecoder().decode(buffer.subarray(pos + 4, pos + 4 + len));
102
+ events.push(JSON.parse(json));
103
+ pos += 4 + len;
104
+ }
105
+ return { events, remainder: buffer.subarray(pos) };
106
+ }
107
+
108
+ // src/core/transports/webtransport.ts
109
+ var WebTransportAdapter = class {
110
+ protocol = "webtransport";
111
+ onSpatialData = null;
112
+ onButtonEvent = null;
113
+ onClose = null;
114
+ onError = null;
115
+ transport = null;
116
+ url;
117
+ certHash;
118
+ constructor(url, certHash) {
119
+ this.url = url;
120
+ this.certHash = certHash;
121
+ }
122
+ async connect() {
123
+ if (typeof globalThis.WebTransport === "undefined") {
124
+ throw new Error("WebTransport is not available in this environment");
125
+ }
126
+ const options = {};
127
+ if (this.certHash) {
128
+ options.serverCertificateHashes = [
129
+ {
130
+ algorithm: "sha-256",
131
+ value: Uint8Array.from(atob(this.certHash), (c) => c.charCodeAt(0))
132
+ }
133
+ ];
134
+ }
135
+ this.transport = new globalThis.WebTransport(this.url, options);
136
+ await this.transport.ready;
137
+ this.readDatagrams();
138
+ this.readStreams();
139
+ this.transport.closed.then(() => this.onClose?.()).catch(() => this.onClose?.());
140
+ }
141
+ close() {
142
+ try {
143
+ this.transport?.close();
144
+ } catch {
145
+ }
146
+ this.transport = null;
147
+ }
148
+ async readDatagrams() {
149
+ const reader = this.transport.datagrams.readable.getReader();
150
+ try {
151
+ while (true) {
152
+ const { value, done } = await reader.read();
153
+ if (done) break;
154
+ this.onSpatialData?.(decodeBinaryFrame(value));
155
+ }
156
+ } catch {
157
+ }
158
+ }
159
+ async readStreams() {
160
+ const reader = this.transport.incomingUnidirectionalStreams.getReader();
161
+ try {
162
+ while (true) {
163
+ const { value: stream, done } = await reader.read();
164
+ if (done) break;
165
+ this.readButtonStream(stream);
166
+ }
167
+ } catch {
168
+ }
169
+ }
170
+ async readButtonStream(stream) {
171
+ const reader = stream.getReader();
172
+ let buffer = new Uint8Array(0);
173
+ try {
174
+ while (true) {
175
+ const { value, done } = await reader.read();
176
+ if (done) break;
177
+ const newBuf = new Uint8Array(buffer.length + value.length);
178
+ newBuf.set(buffer);
179
+ newBuf.set(value, buffer.length);
180
+ const { events, remainder } = decodeButtonStream(newBuf);
181
+ for (const event of events) {
182
+ this.onButtonEvent?.(event);
183
+ }
184
+ buffer = remainder;
185
+ }
186
+ } catch {
187
+ }
188
+ }
189
+ };
190
+
191
+ // src/core/transports/websocket.ts
192
+ var WebSocketAdapter = class {
193
+ protocol = "websocket";
194
+ onSpatialData = null;
195
+ onButtonEvent = null;
196
+ onDeviceStatus = null;
197
+ onClose = null;
198
+ onError = null;
199
+ ws = null;
200
+ url;
201
+ subprotocol;
202
+ constructor(url, subprotocol = "satmouse-json") {
203
+ this.url = url;
204
+ this.subprotocol = subprotocol;
205
+ }
206
+ async connect() {
207
+ return new Promise((resolve, reject) => {
208
+ this.ws = new globalThis.WebSocket(this.url, this.subprotocol);
209
+ if (this.subprotocol === "satmouse-binary") {
210
+ this.ws.binaryType = "arraybuffer";
211
+ }
212
+ this.ws.onopen = () => resolve();
213
+ this.ws.onerror = () => {
214
+ reject(new Error(`WebSocket connection failed: ${this.url}`));
215
+ };
216
+ this.ws.onmessage = (event) => {
217
+ if (this.subprotocol === "satmouse-binary" && event.data instanceof ArrayBuffer) {
218
+ const decoded = decodeWsBinaryFrame(event.data);
219
+ if (decoded?.type === "spatialData") this.onSpatialData?.(decoded.data);
220
+ else if (decoded?.type === "buttonEvent") this.onButtonEvent?.(decoded.data);
221
+ } else if (typeof event.data === "string") {
222
+ try {
223
+ const msg = JSON.parse(event.data);
224
+ if (msg.type === "spatialData") this.onSpatialData?.(msg.data);
225
+ else if (msg.type === "buttonEvent") this.onButtonEvent?.(msg.data);
226
+ else if (msg.type === "deviceStatus") {
227
+ this.onDeviceStatus?.(msg.data.event, msg.data.device);
228
+ }
229
+ } catch {
230
+ }
231
+ }
232
+ };
233
+ this.ws.onclose = () => this.onClose?.();
234
+ });
235
+ }
236
+ close() {
237
+ try {
238
+ this.ws?.close();
239
+ } catch {
240
+ }
241
+ this.ws = null;
242
+ }
243
+ };
244
+
245
+ // src/core/connection.ts
246
+ function parseSatMouseUri(uri) {
247
+ const url = new URL(uri);
248
+ const host = url.searchParams.get("host") ?? "localhost";
249
+ const wsPort = url.searchParams.get("wsPort") ?? "4444";
250
+ const wtPort = url.searchParams.get("wtPort") ?? "4443";
251
+ return {
252
+ tdUrl: `http://${host}:${wsPort}/td.json`,
253
+ wsUrl: `ws://${host}:${wsPort}/spatial`,
254
+ wtUrl: `https://${host}:${wtPort}`
255
+ };
256
+ }
257
+ var DEFAULT_OPTIONS = {
258
+ transports: ["webtransport", "websocket"],
259
+ reconnectDelay: 2e3,
260
+ wsSubprotocol: "satmouse-json"
261
+ };
262
+ var SatMouseConnection = class extends TypedEmitter {
263
+ options;
264
+ transport = null;
265
+ reconnectTimer = null;
266
+ intentionalClose = false;
267
+ deviceInfoUrl = null;
268
+ _state = "disconnected";
269
+ _protocol = "none";
270
+ get state() {
271
+ return this._state;
272
+ }
273
+ get protocol() {
274
+ return this._protocol;
275
+ }
276
+ constructor(options) {
277
+ super();
278
+ this.options = { ...DEFAULT_OPTIONS, ...options };
279
+ }
280
+ async connect() {
281
+ this.intentionalClose = false;
282
+ this.setState("connecting", "none");
283
+ let wtUrl = this.options.wtUrl;
284
+ let wsUrl = this.options.wsUrl;
285
+ let certHash = this.options.certHash;
286
+ if (this.options.uri) {
287
+ const parsed = parseSatMouseUri(this.options.uri);
288
+ wtUrl = wtUrl ?? parsed.wtUrl;
289
+ wsUrl = wsUrl ?? parsed.wsUrl;
290
+ this.options.tdUrl = this.options.tdUrl ?? parsed.tdUrl;
291
+ }
292
+ if (!wtUrl && !wsUrl) {
293
+ const tdUrl = this.options.tdUrl ?? new URL("/td.json", globalThis.location?.origin ?? "http://localhost:4444").href;
294
+ try {
295
+ const td = await fetchThingDescription(tdUrl);
296
+ const endpoints = resolveEndpoints(td);
297
+ wtUrl = endpoints.webtransport?.url;
298
+ wsUrl = endpoints.websocket?.url;
299
+ certHash = certHash ?? endpoints.webtransport?.certHash;
300
+ this.deviceInfoUrl = endpoints.deviceInfoUrl ?? null;
301
+ } catch (err) {
302
+ this.emit("error", err instanceof Error ? err : new Error(String(err)));
303
+ wsUrl = `ws://${globalThis.location?.hostname ?? "localhost"}:${globalThis.location?.port ?? "4444"}/spatial`;
304
+ }
305
+ }
306
+ for (const proto of this.options.transports) {
307
+ if (proto === "webtransport" && wtUrl) {
308
+ try {
309
+ if (typeof globalThis.WebTransport === "undefined") continue;
310
+ const adapter = new WebTransportAdapter(wtUrl, certHash);
311
+ if (await this.tryTransport(adapter)) return;
312
+ } catch {
313
+ continue;
314
+ }
315
+ }
316
+ if (proto === "websocket" && wsUrl) {
317
+ try {
318
+ const adapter = new WebSocketAdapter(wsUrl, this.options.wsSubprotocol);
319
+ if (await this.tryTransport(adapter)) return;
320
+ } catch {
321
+ continue;
322
+ }
323
+ }
324
+ }
325
+ this.setState("disconnected", "none");
326
+ this.scheduleReconnect();
327
+ }
328
+ disconnect() {
329
+ this.intentionalClose = true;
330
+ this.clearReconnect();
331
+ this.transport?.close();
332
+ this.transport = null;
333
+ this.setState("disconnected", "none");
334
+ }
335
+ async fetchDeviceInfo() {
336
+ if (!this.deviceInfoUrl) return [];
337
+ const res = await globalThis.fetch(this.deviceInfoUrl);
338
+ if (!res.ok) return [];
339
+ const data = await res.json();
340
+ return data.devices ?? [];
341
+ }
342
+ async tryTransport(adapter) {
343
+ adapter.onSpatialData = (data) => this.emit("spatialData", data);
344
+ adapter.onButtonEvent = (data) => this.emit("buttonEvent", data);
345
+ adapter.onError = (err) => this.emit("error", err);
346
+ if ("onDeviceStatus" in adapter) {
347
+ adapter.onDeviceStatus = (event, device) => {
348
+ this.emit("deviceStatus", event, device);
349
+ };
350
+ }
351
+ adapter.onClose = () => {
352
+ this.transport = null;
353
+ this.setState("disconnected", "none");
354
+ if (!this.intentionalClose) this.scheduleReconnect();
355
+ };
356
+ try {
357
+ await adapter.connect();
358
+ this.transport = adapter;
359
+ this.setState("connected", adapter.protocol);
360
+ return true;
361
+ } catch (err) {
362
+ this.emit("error", err instanceof Error ? err : new Error(String(err)));
363
+ return false;
364
+ }
365
+ }
366
+ setState(state, protocol) {
367
+ if (this._state === state && this._protocol === protocol) return;
368
+ this._state = state;
369
+ this._protocol = protocol;
370
+ this.emit("stateChange", state, protocol);
371
+ }
372
+ scheduleReconnect() {
373
+ if (this.options.reconnectDelay <= 0 || this.intentionalClose) return;
374
+ this.clearReconnect();
375
+ this.reconnectTimer = setTimeout(() => {
376
+ this.reconnectTimer = null;
377
+ this.connect();
378
+ }, this.options.reconnectDelay);
379
+ }
380
+ clearReconnect() {
381
+ if (this.reconnectTimer) {
382
+ clearTimeout(this.reconnectTimer);
383
+ this.reconnectTimer = null;
384
+ }
385
+ }
386
+ };
387
+
388
+ // src/core/launch.ts
389
+ var SCHEME_URL = "satmouse://launch";
390
+ var PROJECT_URL = "https://github.com/kelnishi/SatMouse/releases/latest";
391
+ function launchSatMouse(options) {
392
+ const schemeUrl = SCHEME_URL;
393
+ const fallbackUrl = PROJECT_URL;
394
+ const timeout = 2500;
395
+ return new Promise((resolve) => {
396
+ let launched = false;
397
+ const onBlur = () => {
398
+ launched = true;
399
+ };
400
+ globalThis.addEventListener("blur", onBlur);
401
+ const iframe = document.createElement("iframe");
402
+ iframe.style.display = "none";
403
+ iframe.src = schemeUrl;
404
+ document.body.appendChild(iframe);
405
+ setTimeout(() => {
406
+ globalThis.removeEventListener("blur", onBlur);
407
+ document.body.removeChild(iframe);
408
+ if (launched || document.hidden) {
409
+ resolve(true);
410
+ } else {
411
+ globalThis.location.href = fallbackUrl;
412
+ resolve(false);
413
+ }
414
+ }, timeout);
415
+ });
416
+ }
417
+
418
+ // src/utils/config.ts
419
+ var DEFAULT_CONFIG = {
420
+ sensitivity: { translation: 1e-3, rotation: 1e-3 },
421
+ flip: { tx: false, ty: true, tz: true, rx: false, ry: true, rz: true },
422
+ deadZone: 0,
423
+ dominant: false,
424
+ axisRemap: { tx: "x", ty: "y", tz: "z", rx: "x", ry: "y", rz: "z" },
425
+ lockPosition: false,
426
+ lockRotation: false
427
+ };
428
+ function mergeConfig(base, partial) {
429
+ return {
430
+ ...base,
431
+ ...partial,
432
+ sensitivity: { ...base.sensitivity, ...partial.sensitivity },
433
+ flip: { ...base.flip, ...partial.flip },
434
+ axisRemap: { ...base.axisRemap, ...partial.axisRemap }
435
+ };
436
+ }
437
+
438
+ // src/utils/persistence.ts
439
+ var STORAGE_KEY = "satmouse:settings";
440
+ function getStorage(storage) {
441
+ if (storage) return storage;
442
+ try {
443
+ return globalThis.localStorage ?? null;
444
+ } catch {
445
+ return null;
446
+ }
447
+ }
448
+ function saveSettings(config, storage) {
449
+ const s = getStorage(storage);
450
+ if (!s) return;
451
+ s.setItem(STORAGE_KEY, JSON.stringify(config));
452
+ }
453
+ function loadSettings(storage) {
454
+ const s = getStorage(storage);
455
+ if (!s) return null;
456
+ const raw = s.getItem(STORAGE_KEY);
457
+ if (!raw) return null;
458
+ try {
459
+ return JSON.parse(raw);
460
+ } catch {
461
+ return null;
462
+ }
463
+ }
464
+
465
+ // src/utils/transforms.ts
466
+ function applyFlip(data, flip) {
467
+ return {
468
+ ...data,
469
+ translation: {
470
+ x: flip.tx ? -data.translation.x : data.translation.x,
471
+ y: flip.ty ? -data.translation.y : data.translation.y,
472
+ z: flip.tz ? -data.translation.z : data.translation.z
473
+ },
474
+ rotation: {
475
+ x: flip.rx ? -data.rotation.x : data.rotation.x,
476
+ y: flip.ry ? -data.rotation.y : data.rotation.y,
477
+ z: flip.rz ? -data.rotation.z : data.rotation.z
478
+ }
479
+ };
480
+ }
481
+ function applySensitivity(data, sens) {
482
+ return {
483
+ ...data,
484
+ translation: {
485
+ x: data.translation.x * sens.translation,
486
+ y: data.translation.y * sens.translation,
487
+ z: data.translation.z * sens.translation
488
+ },
489
+ rotation: {
490
+ x: data.rotation.x * sens.rotation,
491
+ y: data.rotation.y * sens.rotation,
492
+ z: data.rotation.z * sens.rotation
493
+ }
494
+ };
495
+ }
496
+ function applyDominant(data) {
497
+ const axes = [
498
+ { group: "t", key: "x", v: Math.abs(data.translation.x) },
499
+ { group: "t", key: "y", v: Math.abs(data.translation.y) },
500
+ { group: "t", key: "z", v: Math.abs(data.translation.z) },
501
+ { group: "r", key: "x", v: Math.abs(data.rotation.x) },
502
+ { group: "r", key: "y", v: Math.abs(data.rotation.y) },
503
+ { group: "r", key: "z", v: Math.abs(data.rotation.z) }
504
+ ];
505
+ const max = axes.reduce((a, b) => b.v > a.v ? b : a);
506
+ const t = { x: 0, y: 0, z: 0 };
507
+ const r = { x: 0, y: 0, z: 0 };
508
+ if (max.group === "t") t[max.key] = data.translation[max.key];
509
+ else r[max.key] = data.rotation[max.key];
510
+ return { ...data, translation: t, rotation: r };
511
+ }
512
+ function applyDeadZone(data, threshold) {
513
+ const dz = (v) => Math.abs(v) < threshold ? 0 : v;
514
+ return {
515
+ ...data,
516
+ translation: { x: dz(data.translation.x), y: dz(data.translation.y), z: dz(data.translation.z) },
517
+ rotation: { x: dz(data.rotation.x), y: dz(data.rotation.y), z: dz(data.rotation.z) }
518
+ };
519
+ }
520
+ function applyAxisRemap(data, map) {
521
+ return {
522
+ ...data,
523
+ translation: {
524
+ x: 0,
525
+ y: 0,
526
+ z: 0,
527
+ [map.tx]: data.translation.x,
528
+ [map.ty]: data.translation.y,
529
+ [map.tz]: data.translation.z
530
+ },
531
+ rotation: {
532
+ x: 0,
533
+ y: 0,
534
+ z: 0,
535
+ [map.rx]: data.rotation.x,
536
+ [map.ry]: data.rotation.y,
537
+ [map.rz]: data.rotation.z
538
+ }
539
+ };
540
+ }
541
+
542
+ // src/utils/input-manager.ts
543
+ var InputManager = class extends TypedEmitter {
544
+ connections = [];
545
+ storage;
546
+ _config;
547
+ get config() {
548
+ return this._config;
549
+ }
550
+ constructor(config, storage) {
551
+ super();
552
+ this.storage = storage;
553
+ const persisted = loadSettings(storage);
554
+ this._config = mergeConfig(DEFAULT_CONFIG, { ...config, ...persisted });
555
+ }
556
+ /** Add a connection to the managed set */
557
+ addConnection(connection) {
558
+ this.connections.push(connection);
559
+ this.wireConnection(connection);
560
+ }
561
+ /** Remove a connection */
562
+ removeConnection(connection) {
563
+ const idx = this.connections.indexOf(connection);
564
+ if (idx !== -1) this.connections.splice(idx, 1);
565
+ connection.removeAllListeners();
566
+ }
567
+ /** Connect all managed connections */
568
+ async connect() {
569
+ await Promise.all(this.connections.map((c) => c.connect()));
570
+ }
571
+ /** Disconnect all managed connections */
572
+ disconnect() {
573
+ for (const c of this.connections) c.disconnect();
574
+ }
575
+ /** Fetch device info from all connections */
576
+ async fetchDeviceInfo() {
577
+ const results = await Promise.all(this.connections.map((c) => c.fetchDeviceInfo()));
578
+ return results.flat();
579
+ }
580
+ /** Update configuration. Persists by default. */
581
+ updateConfig(partial, persist = true) {
582
+ this._config = mergeConfig(this._config, partial);
583
+ if (persist) saveSettings(this._config, this.storage);
584
+ this.emit("configChange", this._config);
585
+ }
586
+ /** Register a callback for processed spatial data. Returns unsubscribe function. */
587
+ onSpatialData(callback) {
588
+ this.on("spatialData", callback);
589
+ return () => this.off("spatialData", callback);
590
+ }
591
+ /** Register a callback for button events. Returns unsubscribe function. */
592
+ onButtonEvent(callback) {
593
+ this.on("buttonEvent", callback);
594
+ return () => this.off("buttonEvent", callback);
595
+ }
596
+ wireConnection(connection) {
597
+ connection.on("spatialData", (raw) => {
598
+ this.emit("rawSpatialData", raw);
599
+ const processed = this.processSpatialData(raw);
600
+ if (processed) this.emit("spatialData", processed);
601
+ });
602
+ connection.on("buttonEvent", (event) => this.emit("buttonEvent", event));
603
+ connection.on("stateChange", (state, proto) => this.emit("stateChange", state, proto));
604
+ connection.on("deviceStatus", (event, device) => this.emit("deviceStatus", event, device));
605
+ }
606
+ processSpatialData(raw) {
607
+ const cfg = this._config;
608
+ let data = raw;
609
+ if (cfg.deadZone > 0) data = applyDeadZone(data, cfg.deadZone);
610
+ if (cfg.dominant) data = applyDominant(data);
611
+ data = applyFlip(data, cfg.flip);
612
+ data = applyAxisRemap(data, cfg.axisRemap);
613
+ data = applySensitivity(data, cfg.sensitivity);
614
+ if (cfg.lockPosition) {
615
+ data = { ...data, translation: { x: 0, y: 0, z: 0 } };
616
+ }
617
+ if (cfg.lockRotation) {
618
+ data = { ...data, rotation: { x: 0, y: 0, z: 0 } };
619
+ }
620
+ return data;
621
+ }
622
+ };
623
+ var SatMouseContext = createContext(null);
624
+ function SatMouseProvider({
625
+ connectOptions,
626
+ config: configOverrides,
627
+ autoConnect = true,
628
+ children
629
+ }) {
630
+ const managerRef = useRef(null);
631
+ if (!managerRef.current) {
632
+ const connection = new SatMouseConnection(connectOptions);
633
+ const manager2 = new InputManager(configOverrides);
634
+ manager2.addConnection(connection);
635
+ managerRef.current = manager2;
636
+ }
637
+ const manager = managerRef.current;
638
+ const [state, setState] = useState("disconnected");
639
+ const [protocol, setProtocol] = useState("none");
640
+ const [config, setConfig] = useState(manager.config);
641
+ useEffect(() => {
642
+ const onState = (s, p) => {
643
+ setState(s);
644
+ setProtocol(p);
645
+ };
646
+ const onConfig = (c) => setConfig({ ...c });
647
+ manager.on("stateChange", onState);
648
+ manager.on("configChange", onConfig);
649
+ if (autoConnect) manager.connect();
650
+ return () => {
651
+ manager.off("stateChange", onState);
652
+ manager.off("configChange", onConfig);
653
+ manager.disconnect();
654
+ };
655
+ }, [manager, autoConnect]);
656
+ const value = useMemo(
657
+ () => ({
658
+ manager,
659
+ state,
660
+ protocol,
661
+ config,
662
+ updateConfig: (partial) => manager.updateConfig(partial)
663
+ }),
664
+ [manager, state, protocol, config]
665
+ );
666
+ return /* @__PURE__ */ jsx(SatMouseContext, { value, children });
667
+ }
668
+ function useSatMouse() {
669
+ const ctx = useContext(SatMouseContext);
670
+ if (!ctx) throw new Error("useSatMouse must be used within a <SatMouseProvider>");
671
+ return ctx;
672
+ }
673
+ function useSpatialData() {
674
+ const { manager } = useSatMouse();
675
+ const [data, setData] = useState(null);
676
+ const latestRef = useRef(null);
677
+ const rafRef = useRef(0);
678
+ useEffect(() => {
679
+ const unsub = manager.onSpatialData((d) => {
680
+ latestRef.current = d;
681
+ if (!rafRef.current) {
682
+ rafRef.current = requestAnimationFrame(() => {
683
+ rafRef.current = 0;
684
+ setData(latestRef.current);
685
+ });
686
+ }
687
+ });
688
+ return () => {
689
+ unsub();
690
+ if (rafRef.current) cancelAnimationFrame(rafRef.current);
691
+ };
692
+ }, [manager]);
693
+ return data;
694
+ }
695
+ function useRawSpatialData() {
696
+ const { manager } = useSatMouse();
697
+ const [data, setData] = useState(null);
698
+ const latestRef = useRef(null);
699
+ const rafRef = useRef(0);
700
+ useEffect(() => {
701
+ const handler = (d) => {
702
+ latestRef.current = d;
703
+ if (!rafRef.current) {
704
+ rafRef.current = requestAnimationFrame(() => {
705
+ rafRef.current = 0;
706
+ setData(latestRef.current);
707
+ });
708
+ }
709
+ };
710
+ manager.on("rawSpatialData", handler);
711
+ return () => {
712
+ manager.off("rawSpatialData", handler);
713
+ if (rafRef.current) cancelAnimationFrame(rafRef.current);
714
+ };
715
+ }, [manager]);
716
+ return data;
717
+ }
718
+ function useButtonEvent(callback) {
719
+ const { manager } = useSatMouse();
720
+ const callbackRef = useRef(callback);
721
+ callbackRef.current = callback;
722
+ useEffect(() => {
723
+ const handler = (e) => callbackRef.current(e);
724
+ return manager.onButtonEvent(handler);
725
+ }, [manager]);
726
+ }
727
+ function ConnectionStatus({ className }) {
728
+ const { state, protocol } = useSatMouse();
729
+ return /* @__PURE__ */ jsxs("div", { className, "data-state": state, children: [
730
+ /* @__PURE__ */ jsx("span", { "data-role": "dot", "data-state": state }),
731
+ /* @__PURE__ */ jsx("span", { "data-role": "text", children: state === "connected" ? "Connected" : state === "connecting" ? "Connecting..." : "Disconnected" }),
732
+ protocol !== "none" && /* @__PURE__ */ jsx("span", { "data-role": "protocol", children: protocol })
733
+ ] });
734
+ }
735
+ function DeviceInfo({ className, timeout = 5e3 }) {
736
+ const { manager, state } = useSatMouse();
737
+ const [devices, setDevices] = useState([]);
738
+ const [fetchState, setFetchState] = useState("loading");
739
+ const timeoutRef = useRef(void 0);
740
+ const poll = useCallback(
741
+ (bridgeConnected) => {
742
+ if (!bridgeConnected) {
743
+ setFetchState("loading");
744
+ return;
745
+ }
746
+ setFetchState("loading");
747
+ if (timeoutRef.current) clearTimeout(timeoutRef.current);
748
+ timeoutRef.current = setTimeout(() => {
749
+ setFetchState("empty");
750
+ }, timeout);
751
+ manager.fetchDeviceInfo().then((result) => {
752
+ if (timeoutRef.current) clearTimeout(timeoutRef.current);
753
+ setDevices(result);
754
+ setFetchState(result.length > 0 ? "connected" : "empty");
755
+ }).catch(() => {
756
+ if (timeoutRef.current) clearTimeout(timeoutRef.current);
757
+ setFetchState("error");
758
+ });
759
+ },
760
+ [manager, timeout]
761
+ );
762
+ useEffect(() => {
763
+ poll(state === "connected");
764
+ const onStatus = () => poll(true);
765
+ manager.on("deviceStatus", onStatus);
766
+ return () => {
767
+ manager.off("deviceStatus", onStatus);
768
+ if (timeoutRef.current) clearTimeout(timeoutRef.current);
769
+ };
770
+ }, [manager, state, poll]);
771
+ if (fetchState === "loading" && state !== "connected") {
772
+ return /* @__PURE__ */ jsxs("div", { className, "data-state": "loading", children: [
773
+ /* @__PURE__ */ jsx("span", { "data-role": "message", children: "Waiting for bridge..." }),
774
+ /* @__PURE__ */ jsx(
775
+ "span",
776
+ {
777
+ "data-role": "launch",
778
+ onClick: () => launchSatMouse(),
779
+ role: "button",
780
+ tabIndex: 0,
781
+ children: "Launch SatMouse"
782
+ }
783
+ )
784
+ ] });
785
+ }
786
+ if (fetchState === "loading") {
787
+ return /* @__PURE__ */ jsx("div", { className, "data-state": "loading", children: /* @__PURE__ */ jsx("span", { "data-role": "message", children: "Detecting devices..." }) });
788
+ }
789
+ if (fetchState === "error" || fetchState === "empty") {
790
+ return /* @__PURE__ */ jsxs("div", { className, "data-state": "empty", children: [
791
+ /* @__PURE__ */ jsx("span", { "data-role": "message", children: "No device connected" }),
792
+ /* @__PURE__ */ jsx(
793
+ "span",
794
+ {
795
+ "data-role": "hint",
796
+ onClick: () => poll(state === "connected"),
797
+ role: "button",
798
+ tabIndex: 0,
799
+ children: "Click to retry"
800
+ }
801
+ )
802
+ ] });
803
+ }
804
+ return /* @__PURE__ */ jsx("div", { className, "data-state": "connected", children: devices.map((d) => /* @__PURE__ */ jsxs("div", { "data-role": "device", children: [
805
+ /* @__PURE__ */ jsx("span", { "data-role": "model", children: d.model ?? d.name }),
806
+ /* @__PURE__ */ jsx("span", { "data-role": "vendor", children: d.vendor }),
807
+ /* @__PURE__ */ jsx("span", { "data-role": "connection", children: formatConnectionType(d.connectionType) })
808
+ ] }, d.id)) });
809
+ }
810
+ function formatConnectionType(type) {
811
+ switch (type) {
812
+ case "usb":
813
+ return "USB";
814
+ case "wireless":
815
+ return "Wireless";
816
+ case "bluetooth":
817
+ return "Bluetooth";
818
+ default:
819
+ return "";
820
+ }
821
+ }
822
+ var FLIP_AXES = ["tx", "ty", "tz", "rx", "ry", "rz"];
823
+ function mapSlider(v) {
824
+ return 1e-4 * Math.pow(500, v / 100);
825
+ }
826
+ function unmapSlider(v) {
827
+ return 100 * Math.log(v / 1e-4) / Math.log(500);
828
+ }
829
+ function SettingsPanel({ className }) {
830
+ const { config, updateConfig } = useSatMouse();
831
+ const onFlip = useCallback(
832
+ (axis) => {
833
+ updateConfig({ flip: { ...config.flip, [axis]: !config.flip[axis] } });
834
+ },
835
+ [config.flip, updateConfig]
836
+ );
837
+ return /* @__PURE__ */ jsxs("div", { className, children: [
838
+ /* @__PURE__ */ jsxs("section", { "data-section": "sensitivity", children: [
839
+ /* @__PURE__ */ jsxs("label", { children: [
840
+ "Translation",
841
+ /* @__PURE__ */ jsx(
842
+ "input",
843
+ {
844
+ type: "range",
845
+ min: 0,
846
+ max: 100,
847
+ value: Math.round(unmapSlider(config.sensitivity.translation)),
848
+ onChange: (e) => updateConfig({ sensitivity: { ...config.sensitivity, translation: mapSlider(+e.target.value) } })
849
+ }
850
+ ),
851
+ /* @__PURE__ */ jsx("span", { children: config.sensitivity.translation.toFixed(4) })
852
+ ] }),
853
+ /* @__PURE__ */ jsxs("label", { children: [
854
+ "Rotation",
855
+ /* @__PURE__ */ jsx(
856
+ "input",
857
+ {
858
+ type: "range",
859
+ min: 0,
860
+ max: 100,
861
+ value: Math.round(unmapSlider(config.sensitivity.rotation)),
862
+ onChange: (e) => updateConfig({ sensitivity: { ...config.sensitivity, rotation: mapSlider(+e.target.value) } })
863
+ }
864
+ ),
865
+ /* @__PURE__ */ jsx("span", { children: config.sensitivity.rotation.toFixed(4) })
866
+ ] })
867
+ ] }),
868
+ /* @__PURE__ */ jsx("section", { "data-section": "flip", children: FLIP_AXES.map((axis) => /* @__PURE__ */ jsxs("label", { children: [
869
+ /* @__PURE__ */ jsx(
870
+ "input",
871
+ {
872
+ type: "checkbox",
873
+ checked: config.flip[axis],
874
+ onChange: () => onFlip(axis)
875
+ }
876
+ ),
877
+ axis.toUpperCase()
878
+ ] }, axis)) }),
879
+ /* @__PURE__ */ jsxs("section", { "data-section": "toggles", children: [
880
+ /* @__PURE__ */ jsxs("label", { children: [
881
+ /* @__PURE__ */ jsx(
882
+ "input",
883
+ {
884
+ type: "checkbox",
885
+ checked: config.lockPosition,
886
+ onChange: () => updateConfig({ lockPosition: !config.lockPosition })
887
+ }
888
+ ),
889
+ "Lock Position"
890
+ ] }),
891
+ /* @__PURE__ */ jsxs("label", { children: [
892
+ /* @__PURE__ */ jsx(
893
+ "input",
894
+ {
895
+ type: "checkbox",
896
+ checked: config.lockRotation,
897
+ onChange: () => updateConfig({ lockRotation: !config.lockRotation })
898
+ }
899
+ ),
900
+ "Lock Rotation"
901
+ ] }),
902
+ /* @__PURE__ */ jsxs("label", { children: [
903
+ /* @__PURE__ */ jsx(
904
+ "input",
905
+ {
906
+ type: "checkbox",
907
+ checked: config.dominant,
908
+ onChange: () => updateConfig({ dominant: !config.dominant })
909
+ }
910
+ ),
911
+ "Dominant"
912
+ ] })
913
+ ] }),
914
+ /* @__PURE__ */ jsx("section", { "data-section": "deadzone", children: /* @__PURE__ */ jsxs("label", { children: [
915
+ "Dead Zone",
916
+ /* @__PURE__ */ jsx(
917
+ "input",
918
+ {
919
+ type: "range",
920
+ min: 0,
921
+ max: 100,
922
+ value: config.deadZone,
923
+ onChange: (e) => updateConfig({ deadZone: +e.target.value })
924
+ }
925
+ ),
926
+ /* @__PURE__ */ jsx("span", { children: config.deadZone })
927
+ ] }) })
928
+ ] });
929
+ }
930
+ function DebugPanel({ className }) {
931
+ const { state, protocol } = useSatMouse();
932
+ const raw = useRawSpatialData();
933
+ const [fps, setFps] = useState(0);
934
+ const countRef = useRef(0);
935
+ useEffect(() => {
936
+ countRef.current++;
937
+ });
938
+ useEffect(() => {
939
+ const interval = setInterval(() => {
940
+ setFps(countRef.current);
941
+ countRef.current = 0;
942
+ }, 1e3);
943
+ return () => clearInterval(interval);
944
+ }, []);
945
+ return /* @__PURE__ */ jsxs("div", { className, children: [
946
+ /* @__PURE__ */ jsxs("div", { "data-role": "connection", children: [
947
+ /* @__PURE__ */ jsx("span", { "data-role": "label", children: "State" }),
948
+ /* @__PURE__ */ jsx("span", { "data-role": "value", children: state })
949
+ ] }),
950
+ /* @__PURE__ */ jsxs("div", { "data-role": "connection", children: [
951
+ /* @__PURE__ */ jsx("span", { "data-role": "label", children: "Protocol" }),
952
+ /* @__PURE__ */ jsx("span", { "data-role": "value", children: protocol })
953
+ ] }),
954
+ /* @__PURE__ */ jsxs("div", { "data-role": "connection", children: [
955
+ /* @__PURE__ */ jsx("span", { "data-role": "label", children: "Rate" }),
956
+ /* @__PURE__ */ jsxs("span", { "data-role": "value", children: [
957
+ fps,
958
+ " fps"
959
+ ] })
960
+ ] }),
961
+ raw && /* @__PURE__ */ jsxs(Fragment, { children: [
962
+ /* @__PURE__ */ jsxs("div", { "data-role": "axis", children: [
963
+ /* @__PURE__ */ jsx("span", { children: "TX" }),
964
+ /* @__PURE__ */ jsx("span", { children: Math.round(raw.translation.x) })
965
+ ] }),
966
+ /* @__PURE__ */ jsxs("div", { "data-role": "axis", children: [
967
+ /* @__PURE__ */ jsx("span", { children: "TY" }),
968
+ /* @__PURE__ */ jsx("span", { children: Math.round(raw.translation.y) })
969
+ ] }),
970
+ /* @__PURE__ */ jsxs("div", { "data-role": "axis", children: [
971
+ /* @__PURE__ */ jsx("span", { children: "TZ" }),
972
+ /* @__PURE__ */ jsx("span", { children: Math.round(raw.translation.z) })
973
+ ] }),
974
+ /* @__PURE__ */ jsxs("div", { "data-role": "axis", children: [
975
+ /* @__PURE__ */ jsx("span", { children: "RX" }),
976
+ /* @__PURE__ */ jsx("span", { children: Math.round(raw.rotation.x) })
977
+ ] }),
978
+ /* @__PURE__ */ jsxs("div", { "data-role": "axis", children: [
979
+ /* @__PURE__ */ jsx("span", { children: "RY" }),
980
+ /* @__PURE__ */ jsx("span", { children: Math.round(raw.rotation.y) })
981
+ ] }),
982
+ /* @__PURE__ */ jsxs("div", { "data-role": "axis", children: [
983
+ /* @__PURE__ */ jsx("span", { children: "RZ" }),
984
+ /* @__PURE__ */ jsx("span", { children: Math.round(raw.rotation.z) })
985
+ ] })
986
+ ] })
987
+ ] });
988
+ }
989
+
990
+ export { ConnectionStatus, DebugPanel, DeviceInfo, SatMouseContext, SatMouseProvider, SettingsPanel, useButtonEvent, useRawSpatialData, useSatMouse, useSpatialData };
991
+ //# sourceMappingURL=index.js.map
992
+ //# sourceMappingURL=index.js.map