dualsense-ts 2.2.0 → 3.0.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.
- package/README.md +9 -2
- package/dist/comparators.d.ts +13 -0
- package/dist/comparators.d.ts.map +1 -0
- package/dist/comparators.js +27 -0
- package/dist/comparators.js.map +1 -0
- package/dist/dualsense.d.ts +10 -1
- package/dist/dualsense.d.ts.map +1 -1
- package/dist/dualsense.js +46 -45
- package/dist/dualsense.js.map +1 -1
- package/dist/elements/analog.d.ts +1 -1
- package/dist/elements/analog.d.ts.map +1 -1
- package/dist/elements/analog.js +13 -3
- package/dist/elements/analog.js.map +1 -1
- package/dist/elements/dpad.d.ts +1 -1
- package/dist/elements/dpad.d.ts.map +1 -1
- package/dist/elements/dpad.js +6 -5
- package/dist/elements/dpad.js.map +1 -1
- package/dist/elements/touch.d.ts +1 -1
- package/dist/elements/touch.d.ts.map +1 -1
- package/dist/elements/touch.js.map +1 -1
- package/dist/elements/touchpad.d.ts +1 -1
- package/dist/elements/touchpad.d.ts.map +1 -1
- package/dist/elements/touchpad.js.map +1 -1
- package/dist/elements/trigger.d.ts.map +1 -1
- package/dist/elements/trigger.js +1 -1
- package/dist/elements/trigger.js.map +1 -1
- package/dist/elements/unisense.d.ts +1 -1
- package/dist/elements/unisense.d.ts.map +1 -1
- package/dist/elements/unisense.js +12 -5
- package/dist/elements/unisense.js.map +1 -1
- package/dist/hid/dualsense_hid.d.ts +16 -58
- package/dist/hid/dualsense_hid.d.ts.map +1 -1
- package/dist/hid/dualsense_hid.js +25 -101
- package/dist/hid/dualsense_hid.js.map +1 -1
- package/dist/hid/hid_provider.d.ts +86 -0
- package/dist/hid/hid_provider.d.ts.map +1 -0
- package/dist/hid/hid_provider.js +45 -0
- package/dist/hid/hid_provider.js.map +1 -0
- package/dist/hid/index.d.ts +4 -1
- package/dist/hid/index.d.ts.map +1 -1
- package/dist/hid/index.js +4 -1
- package/dist/hid/index.js.map +1 -1
- package/dist/hid/node_hid_provider.d.ts +11 -0
- package/dist/hid/node_hid_provider.d.ts.map +1 -0
- package/dist/hid/node_hid_provider.js +97 -0
- package/dist/hid/node_hid_provider.js.map +1 -0
- package/dist/hid/platform_hid_provider.d.ts +4 -0
- package/dist/hid/platform_hid_provider.d.ts.map +1 -0
- package/dist/hid/platform_hid_provider.js +7 -0
- package/dist/hid/platform_hid_provider.js.map +1 -0
- package/dist/hid/web_hid_provider.d.ts +10 -0
- package/dist/hid/web_hid_provider.d.ts.map +1 -0
- package/dist/hid/web_hid_provider.js +102 -0
- package/dist/hid/web_hid_provider.js.map +1 -0
- package/dist/{hid/ids.d.ts → id.d.ts} +6 -2
- package/dist/id.d.ts.map +1 -0
- package/dist/{hid/ids.js → id.js} +1 -1
- package/dist/id.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/input.d.ts +81 -36
- package/dist/input.d.ts.map +1 -1
- package/dist/input.js +128 -89
- package/dist/input.js.map +1 -1
- package/docs/Analog.md +81 -131
- package/docs/AnalogParams.md +7 -9
- package/docs/Axis.md +72 -79
- package/docs/Brightness.md +8 -8
- package/docs/CommandScopeA.md +16 -16
- package/docs/CommandScopeB.md +14 -14
- package/docs/Dpad.md +74 -81
- package/docs/DpadParams.md +8 -8
- package/docs/DualSenseCommand.md +45 -45
- package/docs/Dualsense.md +83 -90
- package/docs/DualsenseHID.md +47 -42
- package/docs/DualsenseHIDState.md +41 -41
- package/docs/DualsenseParams.md +17 -17
- package/docs/Exports.md +18 -21
- package/docs/Haptic.md +1 -1
- package/docs/Home.md +29 -31
- package/docs/Indicator.md +2 -2
- package/docs/Input.md +70 -79
- package/docs/InputId.md +84 -84
- package/docs/InputParams.md +4 -4
- package/docs/LedOptions.md +10 -10
- package/docs/Momentary.md +70 -77
- package/docs/Motion.md +2 -2
- package/docs/Mute.md +71 -89
- package/docs/PlayerID.md +12 -12
- package/docs/PulseOptions.md +8 -8
- package/docs/Touchpad.md +120 -94
- package/docs/Trigger.md +75 -82
- package/docs/TriggerMode.md +22 -22
- package/docs/Unisense.md +74 -81
- package/docs/UnisenseParams.md +7 -7
- package/package.json +16 -10
- package/src/comparators.ts +26 -0
- package/src/dualsense.ts +61 -58
- package/src/elements/analog.spec.ts +17 -0
- package/src/elements/analog.ts +14 -8
- package/src/elements/axis.spec.ts +31 -0
- package/src/elements/dpad.ts +7 -6
- package/src/elements/touch.ts +1 -1
- package/src/elements/touchpad.ts +1 -1
- package/src/elements/trigger.ts +1 -1
- package/src/elements/unisense.ts +16 -15
- package/src/hid/dualsense_hid.ts +25 -156
- package/src/hid/{dualsense_hid.spec.ts → hid_provider.spec.ts} +1 -1
- package/src/hid/hid_provider.ts +100 -0
- package/src/hid/index.ts +4 -1
- package/src/hid/node_hid_provider.ts +108 -0
- package/src/hid/platform_hid_provider.ts +4 -0
- package/src/hid/web_hid_provider.ts +116 -0
- package/src/{hid/ids.ts → id.ts} +6 -1
- package/src/index.ts +2 -0
- package/src/input.ts +178 -142
- package/src/readme.spec.ts +6 -8
- package/webpack.config.js +42 -0
- package/dist/hid/ids.d.ts.map +0 -1
- package/dist/hid/ids.js.map +0 -1
- package/docs/Increment.md +0 -1554
- package/docs/Touch.md +0 -1824
package/src/hid/index.ts
CHANGED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { HID, devices } from "node-hid";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
HIDProvider,
|
|
5
|
+
DualsenseHIDState,
|
|
6
|
+
InputId,
|
|
7
|
+
mapAxis,
|
|
8
|
+
mapTrigger,
|
|
9
|
+
} from "./hid_provider";
|
|
10
|
+
|
|
11
|
+
export class NodeHIDProvider extends HIDProvider {
|
|
12
|
+
private device?: HID;
|
|
13
|
+
public wireless: boolean = false;
|
|
14
|
+
|
|
15
|
+
connect(): void {
|
|
16
|
+
this.disconnect();
|
|
17
|
+
|
|
18
|
+
const controllers = devices(HIDProvider.vendorId, HIDProvider.productId);
|
|
19
|
+
if (controllers.length === 0 || !controllers[0].path) {
|
|
20
|
+
return this.onError(
|
|
21
|
+
new Error(`No controllers (${devices().length} other devices)`)
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (controllers[0].interface === -1) this.wireless = true;
|
|
26
|
+
|
|
27
|
+
this.device = new HID(controllers[0].path);
|
|
28
|
+
this.device.on("data", (arg: Buffer) => {
|
|
29
|
+
this.onData(this.process(arg));
|
|
30
|
+
});
|
|
31
|
+
this.device.on("error", (err: Error) => {
|
|
32
|
+
this.onError(err);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
get connected(): boolean {
|
|
37
|
+
return this.device !== undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
disconnect(): void {
|
|
41
|
+
if (this.device) {
|
|
42
|
+
this.device.removeAllListeners();
|
|
43
|
+
this.device.close();
|
|
44
|
+
this.device = undefined;
|
|
45
|
+
this.wireless = false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
process(buffer: Buffer): DualsenseHIDState {
|
|
50
|
+
// Bluetooth buffer starts with an extra byte
|
|
51
|
+
const report = buffer.subarray(this.wireless ? 2 : 1);
|
|
52
|
+
|
|
53
|
+
const mainButtons = report.readUint8(7);
|
|
54
|
+
const miscButtons = report.readUint8(8);
|
|
55
|
+
const lastButtons = report.readUint8(9);
|
|
56
|
+
const dpad = (mainButtons << 4) >> 4;
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
[InputId.LeftAnalogX]: mapAxis(report.readUint8(0)),
|
|
60
|
+
[InputId.LeftAnalogY]: -mapAxis(report.readUint8(1)),
|
|
61
|
+
[InputId.RightAnalogX]: mapAxis(report.readUint8(2)),
|
|
62
|
+
[InputId.RightAnalogY]: -mapAxis(report.readUint8(3)),
|
|
63
|
+
[InputId.LeftTrigger]: mapTrigger(report.readUint8(4)),
|
|
64
|
+
[InputId.RightTrigger]: mapTrigger(report.readUint8(5)),
|
|
65
|
+
// 6 is a sequence byte
|
|
66
|
+
[InputId.Triangle]: (mainButtons & 128) > 0,
|
|
67
|
+
[InputId.Circle]: (mainButtons & 64) > 0,
|
|
68
|
+
[InputId.Cross]: (mainButtons & 32) > 0,
|
|
69
|
+
[InputId.Square]: (mainButtons & 16) > 0,
|
|
70
|
+
[InputId.Dpad]: dpad,
|
|
71
|
+
[InputId.Up]: dpad < 2 || dpad === 7,
|
|
72
|
+
[InputId.Down]: dpad > 2 && dpad < 6,
|
|
73
|
+
[InputId.Left]: dpad > 4 && dpad < 8,
|
|
74
|
+
[InputId.Right]: dpad > 0 && dpad < 4,
|
|
75
|
+
[InputId.LeftTriggerButton]: (miscButtons & 4) > 0,
|
|
76
|
+
[InputId.RightTriggerButton]: (miscButtons & 8) > 0,
|
|
77
|
+
[InputId.LeftBumper]: (miscButtons & 1) > 0,
|
|
78
|
+
[InputId.RightBumper]: (miscButtons & 2) > 0,
|
|
79
|
+
[InputId.Create]: (miscButtons & 16) > 0,
|
|
80
|
+
[InputId.Options]: (miscButtons & 32) > 0,
|
|
81
|
+
[InputId.LeftAnalogButton]: (miscButtons & 64) > 0,
|
|
82
|
+
[InputId.RightAnalogButton]: (miscButtons & 128) > 0,
|
|
83
|
+
[InputId.Playstation]: (lastButtons & 1) > 0,
|
|
84
|
+
[InputId.TouchButton]: (lastButtons & 2) > 0,
|
|
85
|
+
[InputId.Mute]: (lastButtons & 4) > 0,
|
|
86
|
+
// The other 5 bits are unused
|
|
87
|
+
// 5 reserved bytes
|
|
88
|
+
[InputId.GyroX]: report.readUint16LE(15),
|
|
89
|
+
[InputId.GyroY]: report.readUint16LE(17),
|
|
90
|
+
[InputId.GyroZ]: report.readUint16LE(19),
|
|
91
|
+
[InputId.AccelX]: report.readUint16LE(21),
|
|
92
|
+
[InputId.AccelY]: report.readUint16LE(23),
|
|
93
|
+
[InputId.AccelZ]: report.readUint16LE(25),
|
|
94
|
+
// 4 bytes for sensor timestamp (32LE)
|
|
95
|
+
// 1 reserved byte
|
|
96
|
+
[InputId.TouchId0]: report.readUint8(32) & 0x7f,
|
|
97
|
+
[InputId.TouchContact0]: (report.readUint8(32) & 0x80) === 0,
|
|
98
|
+
[InputId.TouchX0]: mapAxis((report.readUint16LE(33) << 20) >> 20, 1920),
|
|
99
|
+
[InputId.TouchY0]: mapAxis(report.readUint16LE(34) >> 4, 1080),
|
|
100
|
+
[InputId.TouchId1]: report.readUint8(36) & 0x7f,
|
|
101
|
+
[InputId.TouchContact1]: (report.readUint8(36) & 0x80) === 0,
|
|
102
|
+
[InputId.TouchX1]: mapAxis((report.readUint16LE(37) << 20) >> 20, 1920),
|
|
103
|
+
[InputId.TouchY1]: mapAxis(report.readUint16LE(38) >> 4, 1080),
|
|
104
|
+
// 12 reserved bytes
|
|
105
|
+
[InputId.Status]: (report.readUint8(53) & 4) > 0,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import {
|
|
2
|
+
HIDProvider,
|
|
3
|
+
DualsenseHIDState,
|
|
4
|
+
InputId,
|
|
5
|
+
mapAxis,
|
|
6
|
+
mapTrigger,
|
|
7
|
+
} from "./hid_provider";
|
|
8
|
+
|
|
9
|
+
export class WebHIDProvider extends HIDProvider {
|
|
10
|
+
private device?: HIDDevice;
|
|
11
|
+
public wireless: boolean = true; // TODO: Not sure what to check
|
|
12
|
+
|
|
13
|
+
connect(): void {
|
|
14
|
+
this.disconnect();
|
|
15
|
+
|
|
16
|
+
navigator.hid
|
|
17
|
+
.requestDevice({
|
|
18
|
+
filters: [
|
|
19
|
+
{ vendorId: HIDProvider.vendorId, productId: HIDProvider.productId },
|
|
20
|
+
],
|
|
21
|
+
})
|
|
22
|
+
.then((devices: HIDDevice[]) => {
|
|
23
|
+
if (devices.length === 0) {
|
|
24
|
+
return this.onError(new Error(`No controllers available`));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
devices[0]
|
|
28
|
+
.open()
|
|
29
|
+
.then(() => {
|
|
30
|
+
this.device = devices[0];
|
|
31
|
+
this.device.addEventListener("inputreport", ({ data }) => {
|
|
32
|
+
this.onData(this.process(data));
|
|
33
|
+
});
|
|
34
|
+
})
|
|
35
|
+
.catch((err: Error) => {
|
|
36
|
+
this.onError(err);
|
|
37
|
+
});
|
|
38
|
+
})
|
|
39
|
+
.catch((err: Error) => {
|
|
40
|
+
this.onError(err);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
get connected(): boolean {
|
|
45
|
+
return this.device !== undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
disconnect(): void {
|
|
49
|
+
if (this.device) {
|
|
50
|
+
this.device.close().finally(() => {
|
|
51
|
+
this.device = undefined;
|
|
52
|
+
this.wireless = false;
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
process(buffer: DataView): DualsenseHIDState {
|
|
58
|
+
// Bluetooth buffer starts with an extra byte
|
|
59
|
+
const report = new DataView(buffer.buffer, this.wireless ? 2 : 1);
|
|
60
|
+
|
|
61
|
+
const mainButtons = report.getUint8(7);
|
|
62
|
+
const miscButtons = report.getUint8(8);
|
|
63
|
+
const lastButtons = report.getUint8(9);
|
|
64
|
+
const dpad = (mainButtons << 4) >> 4;
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
[InputId.LeftAnalogX]: mapAxis(report.getUint8(0)),
|
|
68
|
+
[InputId.LeftAnalogY]: -mapAxis(report.getUint8(1)),
|
|
69
|
+
[InputId.RightAnalogX]: mapAxis(report.getUint8(2)),
|
|
70
|
+
[InputId.RightAnalogY]: -mapAxis(report.getUint8(3)),
|
|
71
|
+
[InputId.LeftTrigger]: mapTrigger(report.getUint8(4)),
|
|
72
|
+
[InputId.RightTrigger]: mapTrigger(report.getUint8(5)),
|
|
73
|
+
[InputId.Triangle]: (mainButtons & 128) > 0,
|
|
74
|
+
[InputId.Circle]: (mainButtons & 64) > 0,
|
|
75
|
+
[InputId.Cross]: (mainButtons & 32) > 0,
|
|
76
|
+
[InputId.Square]: (mainButtons & 16) > 0,
|
|
77
|
+
[InputId.Dpad]: dpad,
|
|
78
|
+
[InputId.Up]: dpad < 2 || dpad === 7,
|
|
79
|
+
[InputId.Down]: dpad > 2 && dpad < 6,
|
|
80
|
+
[InputId.Left]: dpad > 4 && dpad < 8,
|
|
81
|
+
[InputId.Right]: dpad > 0 && dpad < 4,
|
|
82
|
+
[InputId.LeftTriggerButton]: (miscButtons & 4) > 0,
|
|
83
|
+
[InputId.RightTriggerButton]: (miscButtons & 8) > 0,
|
|
84
|
+
[InputId.LeftBumper]: (miscButtons & 1) > 0,
|
|
85
|
+
[InputId.RightBumper]: (miscButtons & 2) > 0,
|
|
86
|
+
[InputId.Create]: (miscButtons & 16) > 0,
|
|
87
|
+
[InputId.Options]: (miscButtons & 32) > 0,
|
|
88
|
+
[InputId.LeftAnalogButton]: (miscButtons & 64) > 0,
|
|
89
|
+
[InputId.RightAnalogButton]: (miscButtons & 128) > 0,
|
|
90
|
+
[InputId.Playstation]: (lastButtons & 1) > 0,
|
|
91
|
+
[InputId.TouchButton]: (lastButtons & 2) > 0,
|
|
92
|
+
[InputId.Mute]: (lastButtons & 4) > 0,
|
|
93
|
+
[InputId.GyroX]: report.getUint16(15, true),
|
|
94
|
+
[InputId.GyroY]: report.getUint16(17, true),
|
|
95
|
+
[InputId.GyroZ]: report.getUint16(19, true),
|
|
96
|
+
[InputId.AccelX]: report.getUint16(21, true),
|
|
97
|
+
[InputId.AccelY]: report.getUint16(23, true),
|
|
98
|
+
[InputId.AccelZ]: report.getUint16(25, true),
|
|
99
|
+
[InputId.TouchId0]: report.getUint8(32) & 0x7f,
|
|
100
|
+
[InputId.TouchContact0]: (report.getUint8(32) & 0x80) === 0,
|
|
101
|
+
[InputId.TouchX0]: mapAxis(
|
|
102
|
+
(report.getUint16(33, true) << 20) >> 20,
|
|
103
|
+
1920
|
|
104
|
+
),
|
|
105
|
+
[InputId.TouchY0]: mapAxis(report.getUint16(34, true) >> 4, 1080),
|
|
106
|
+
[InputId.TouchId1]: report.getUint8(36) & 0x7f,
|
|
107
|
+
[InputId.TouchContact1]: (report.getUint8(36) & 0x80) === 0,
|
|
108
|
+
[InputId.TouchX1]: mapAxis(
|
|
109
|
+
(report.getUint16(37, true) << 20) >> 20,
|
|
110
|
+
1920
|
|
111
|
+
),
|
|
112
|
+
[InputId.TouchY1]: mapAxis(report.getUint16(38, true) >> 4, 1080),
|
|
113
|
+
[InputId.Status]: (report.getUint8(53) & 4) > 0,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
}
|
package/src/{hid/ids.ts → id.ts}
RENAMED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* IDs for real and virtual controller inputs.
|
|
3
|
+
*/
|
|
2
4
|
export const enum InputId {
|
|
3
5
|
Options = "Options",
|
|
4
6
|
Create = "Create",
|
|
@@ -52,4 +54,7 @@ export const enum InputId {
|
|
|
52
54
|
AccelX = "AccelX",
|
|
53
55
|
AccelY = "AccelY",
|
|
54
56
|
AccelZ = "AccelZ",
|
|
57
|
+
|
|
58
|
+
// For placeholder inputs
|
|
59
|
+
Unknown = "Unknown",
|
|
55
60
|
}
|
package/src/index.ts
CHANGED
package/src/input.ts
CHANGED
|
@@ -1,83 +1,177 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { InputId } from "./id";
|
|
2
|
+
import {
|
|
3
|
+
VirtualComparator,
|
|
4
|
+
ThresholdComparator,
|
|
5
|
+
BasicComparator,
|
|
6
|
+
} from "./comparators";
|
|
7
|
+
|
|
8
|
+
export { InputId } from "./id";
|
|
3
9
|
|
|
4
10
|
export interface InputParams {
|
|
5
11
|
name?: string;
|
|
6
12
|
icon?: string;
|
|
7
13
|
threshold?: number;
|
|
8
|
-
parent?: Input<unknown>;
|
|
9
14
|
}
|
|
10
15
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
icon: "???",
|
|
14
|
-
threshold: 0,
|
|
15
|
-
};
|
|
16
|
+
export type InputChangeType = "change" | "press" | "release";
|
|
17
|
+
export type InputEventType = InputChangeType | "input";
|
|
16
18
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const InputSetComparison = Symbol("InputSetComparison");
|
|
22
|
-
const InputChangedPrimitive = Symbol("InputChangedPrimitive");
|
|
23
|
-
const InputChangedThreshold = Symbol("InputChangedThreshold");
|
|
24
|
-
const InputChangedVirtual = Symbol("InputChangedVirtual");
|
|
25
|
-
|
|
26
|
-
// Optional abstract properties
|
|
27
|
-
export const InputChanged = Symbol("InputChanged");
|
|
19
|
+
export type InputCallback<Instance> = (
|
|
20
|
+
input: Instance,
|
|
21
|
+
changed: Instance | Input<unknown>
|
|
22
|
+
) => unknown | Promise<unknown>;
|
|
28
23
|
|
|
29
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Private utilities
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
export const InputSetComparator = Symbol("InputSetComparator");
|
|
29
|
+
export const InputChanged = Symbol("InputChanged");
|
|
30
30
|
export const InputSet = Symbol("InputSet");
|
|
31
31
|
export const InputName = Symbol("InputName");
|
|
32
32
|
export const InputIcon = Symbol("InputIcon");
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
): this;
|
|
44
|
-
emit(
|
|
45
|
-
event: InputEvent,
|
|
46
|
-
...args: [Input<Type>, Input<unknown> | Input<Type>]
|
|
47
|
-
): boolean;
|
|
48
|
-
}
|
|
34
|
+
/**
|
|
35
|
+
* Private properties
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
const InputOns = Symbol("InputOns");
|
|
39
|
+
const InputOnces = Symbol("InputOnces");
|
|
40
|
+
const InputAdopt = Symbol("InputAdopt");
|
|
41
|
+
const InputParents = Symbol("InputParents");
|
|
42
|
+
const InputComparator = Symbol("InputComparator");
|
|
49
43
|
|
|
50
44
|
/**
|
|
51
45
|
* Input manages the state of a single device input,
|
|
52
46
|
* a virtual input, or a group of Input children.
|
|
53
47
|
*/
|
|
54
|
-
export abstract class Input<Type>
|
|
55
|
-
|
|
56
|
-
implements AsyncIterator<Input<Type>>
|
|
57
|
-
{
|
|
58
|
-
public readonly id: symbol;
|
|
59
|
-
|
|
60
|
-
// Timestamp of the last received input that changed the state.
|
|
61
|
-
public lastChange: number = Date.now();
|
|
48
|
+
export abstract class Input<Type> implements AsyncIterator<Input<Type>> {
|
|
49
|
+
public readonly id: InputId = InputId.Unknown;
|
|
62
50
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
// For numeric inputs, ignore state changes smaller than this threshold.
|
|
51
|
+
/**
|
|
52
|
+
* For numeric inputs, ignore state changes smaller than this threshold.
|
|
53
|
+
*/
|
|
67
54
|
public threshold: number = 0;
|
|
68
55
|
|
|
69
|
-
|
|
56
|
+
/**
|
|
57
|
+
* Provide the type and default value for the input.
|
|
58
|
+
*/
|
|
70
59
|
public abstract state: Type;
|
|
71
60
|
|
|
72
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Stores event listeners.
|
|
63
|
+
*/
|
|
64
|
+
private [InputOns] = new Map<InputEventType, InputCallback<this>[]>();
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Stores callbacks waiting for one-time events.
|
|
68
|
+
*/
|
|
69
|
+
private [InputOnces] = new Map<InputChangeType, InputCallback<this>[]>();
|
|
70
|
+
|
|
71
|
+
constructor(params: InputParams = {}) {
|
|
72
|
+
const { name, icon, threshold } = {
|
|
73
|
+
icon: "???",
|
|
74
|
+
name: "Nameless Input",
|
|
75
|
+
threshold: 0,
|
|
76
|
+
...params,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
this[InputName] = name;
|
|
80
|
+
this[InputIcon] = icon;
|
|
81
|
+
this.threshold = threshold;
|
|
82
|
+
|
|
83
|
+
setTimeout(() => {
|
|
84
|
+
this[InputSetComparator]();
|
|
85
|
+
Object.values(this).forEach((value) => {
|
|
86
|
+
if (value === this) return;
|
|
87
|
+
if (value instanceof Input) value[InputAdopt](this as Input<unknown>);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Implement a function that returns true if the user is actively engaged with the input.
|
|
94
|
+
*/
|
|
73
95
|
public abstract get active(): boolean;
|
|
74
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Register a callback to recieve state updates from this Input.
|
|
99
|
+
*/
|
|
100
|
+
public on(event: InputEventType, listener: InputCallback<this>): this {
|
|
101
|
+
const listeners = this[InputOns].get(event);
|
|
102
|
+
if (!listeners) {
|
|
103
|
+
this[InputOns].set(event, []);
|
|
104
|
+
return this.on(event, listener);
|
|
105
|
+
}
|
|
106
|
+
listeners.push(listener);
|
|
107
|
+
return this;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Register a callback to recieve the next specified update.
|
|
112
|
+
*/
|
|
113
|
+
public once(event: InputChangeType, listener: InputCallback<this>): this {
|
|
114
|
+
const listeners = this[InputOnces].get(event);
|
|
115
|
+
if (!listeners) {
|
|
116
|
+
this[InputOnces].set(event, []);
|
|
117
|
+
return this.once(event, listener);
|
|
118
|
+
}
|
|
119
|
+
listeners.push(listener);
|
|
120
|
+
return this;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Notify listeners and parents of a state change.
|
|
125
|
+
*/
|
|
126
|
+
private emit(event: InputEventType, changed: Input<unknown> | this): void {
|
|
127
|
+
const listeners = this[InputOns].get(event) || [];
|
|
128
|
+
listeners.forEach((callback) => {
|
|
129
|
+
callback(this, changed);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
if (event !== "input") {
|
|
133
|
+
this.emitOnce(event, changed);
|
|
134
|
+
this[InputParents].forEach((input) => {
|
|
135
|
+
input.emit(event, changed as Input<unknown>);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Notify one-time listeners of a state change.
|
|
142
|
+
*/
|
|
143
|
+
private emitOnce(
|
|
144
|
+
event: InputChangeType,
|
|
145
|
+
changed: this | Input<unknown> = this
|
|
146
|
+
): void {
|
|
147
|
+
const listeners = this[InputOnces].get(event) || [];
|
|
148
|
+
this[InputOnces].set(event, []);
|
|
149
|
+
listeners.forEach((callback) => {
|
|
150
|
+
callback(this, changed as Input<unknown>);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Register a callback to recieve state updates from this Input.
|
|
156
|
+
*/
|
|
157
|
+
public addEventListener(
|
|
158
|
+
event: InputEventType,
|
|
159
|
+
listener: InputCallback<this>,
|
|
160
|
+
{ once }: { once: boolean } = { once: false }
|
|
161
|
+
): this {
|
|
162
|
+
if (once) {
|
|
163
|
+
if (event === "input") {
|
|
164
|
+
throw new Error("Can't listen once to `input` events");
|
|
165
|
+
}
|
|
166
|
+
return this.once(event, listener);
|
|
167
|
+
}
|
|
168
|
+
return this.on(event, listener);
|
|
169
|
+
}
|
|
170
|
+
|
|
75
171
|
/**
|
|
76
172
|
* Resolves on the next change to this input's state.
|
|
77
173
|
*/
|
|
78
|
-
public next(
|
|
79
|
-
type: "press" | "release" | "change" = "change"
|
|
80
|
-
): Promise<IteratorResult<this>> {
|
|
174
|
+
public next(type: InputChangeType = "change"): Promise<IteratorResult<this>> {
|
|
81
175
|
return new Promise<IteratorResult<this>>((resolve) => {
|
|
82
176
|
this.once(type, () => {
|
|
83
177
|
resolve({ value: this, done: false });
|
|
@@ -97,44 +191,16 @@ export abstract class Input<Type>
|
|
|
97
191
|
}
|
|
98
192
|
|
|
99
193
|
/**
|
|
100
|
-
* Render a
|
|
194
|
+
* Render a debugging string.
|
|
101
195
|
*/
|
|
102
196
|
public toString(): string {
|
|
103
197
|
return `${this[InputIcon]} [${this.active ? "X" : "_"}]`;
|
|
104
198
|
}
|
|
105
199
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
...InputDefaults,
|
|
111
|
-
...(params || {}),
|
|
112
|
-
};
|
|
113
|
-
this[InputName] = name;
|
|
114
|
-
this[InputIcon] = icon;
|
|
115
|
-
this[InputParent] = parent;
|
|
116
|
-
|
|
117
|
-
this.threshold = threshold;
|
|
118
|
-
this.id = Symbol(this[InputName]);
|
|
119
|
-
|
|
120
|
-
this[InputChanged] = this[InputSetComparison]();
|
|
121
|
-
setImmediate(() => {
|
|
122
|
-
this[InputAdopt]();
|
|
123
|
-
this[InputChanged] = this[InputSetComparison]();
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Optionally, implement a function that returns true if the provided state is worth an event
|
|
128
|
-
[InputChanged]: (state: Type, newState: Type) => boolean;
|
|
129
|
-
|
|
130
|
-
// TODO Support params for nested inputs
|
|
131
|
-
[inspect.custom](): string {
|
|
132
|
-
return `${this[InputName]} ${this[InputIcon]}: ${JSON.stringify(
|
|
133
|
-
this.state instanceof Input && this.state.id === this.id
|
|
134
|
-
? "virtual"
|
|
135
|
-
: this.state
|
|
136
|
-
)}`;
|
|
137
|
-
}
|
|
200
|
+
/**
|
|
201
|
+
* Returns true if the provided state is worth an event
|
|
202
|
+
*/
|
|
203
|
+
[InputComparator]: (state: Type, newState: Type) => boolean = BasicComparator;
|
|
138
204
|
|
|
139
205
|
[Symbol.asyncIterator](): AsyncIterator<this> {
|
|
140
206
|
return this;
|
|
@@ -150,62 +216,38 @@ export abstract class Input<Type>
|
|
|
150
216
|
return this.toString();
|
|
151
217
|
}
|
|
152
218
|
|
|
153
|
-
|
|
219
|
+
/**
|
|
220
|
+
* The name of this input.
|
|
221
|
+
*/
|
|
154
222
|
readonly [InputName]: string;
|
|
155
223
|
|
|
156
|
-
|
|
224
|
+
/**
|
|
225
|
+
* A short name for this input.
|
|
226
|
+
*/
|
|
157
227
|
readonly [InputIcon]: string;
|
|
158
228
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
[
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Cascade events from nested Inputs.
|
|
166
|
-
* And decide if this is the root Input.
|
|
167
|
-
*/
|
|
168
|
-
[InputAdopt](): void {
|
|
169
|
-
Object.values(this).forEach((value) => {
|
|
170
|
-
if (value === this) return;
|
|
171
|
-
if (value instanceof Input) {
|
|
172
|
-
this[InputChildless] = false;
|
|
173
|
-
if (!value[InputChildless]) return;
|
|
174
|
-
value.on("change", (that, value) => {
|
|
175
|
-
that;
|
|
176
|
-
this.emit("change", this, value);
|
|
177
|
-
});
|
|
178
|
-
value.on("input", (that, value) => {
|
|
179
|
-
that;
|
|
180
|
-
this.emit("input", this, value);
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
[InputChangedVirtual](): boolean {
|
|
187
|
-
return true;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
[InputChangedPrimitive](state: Type, newState: Type): boolean {
|
|
191
|
-
return state !== newState;
|
|
192
|
-
}
|
|
229
|
+
/**
|
|
230
|
+
* Other Inputs that contain this one.
|
|
231
|
+
*/
|
|
232
|
+
private [InputParents] = new Set<Input<unknown>>();
|
|
193
233
|
|
|
194
|
-
|
|
195
|
-
|
|
234
|
+
/**
|
|
235
|
+
* Links Inputs to bubble up events.
|
|
236
|
+
*/
|
|
237
|
+
[InputAdopt](parent: Input<unknown>): void {
|
|
238
|
+
this[InputParents].add(parent);
|
|
196
239
|
}
|
|
197
240
|
|
|
198
|
-
|
|
199
|
-
|
|
241
|
+
/**
|
|
242
|
+
* Sets a default comparison type for the Input by discovering the generic type.
|
|
243
|
+
*/
|
|
244
|
+
[InputSetComparator](): void {
|
|
200
245
|
if (typeof this.state === "number") {
|
|
201
|
-
|
|
202
|
-
state: Type,
|
|
203
|
-
newState: Type
|
|
204
|
-
) => boolean;
|
|
246
|
+
this[InputComparator] = ThresholdComparator.bind(this, this.threshold);
|
|
205
247
|
} else if (this.state instanceof Input) {
|
|
206
|
-
|
|
248
|
+
this[InputComparator] = VirtualComparator;
|
|
207
249
|
} else {
|
|
208
|
-
|
|
250
|
+
this[InputComparator] = BasicComparator;
|
|
209
251
|
}
|
|
210
252
|
}
|
|
211
253
|
|
|
@@ -213,18 +255,12 @@ export abstract class Input<Type>
|
|
|
213
255
|
* Update the input's state and trigger all necessary callbacks.
|
|
214
256
|
*/
|
|
215
257
|
[InputSet](state: Type): void {
|
|
216
|
-
this.
|
|
217
|
-
if (this[InputChanged](this.state, state)) {
|
|
258
|
+
if (this[InputComparator](this.state, state)) {
|
|
218
259
|
this.state = state;
|
|
219
|
-
this.
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
state
|
|
223
|
-
? this.emit("press", this, this)
|
|
224
|
-
: this.emit("release", this, this);
|
|
225
|
-
}
|
|
260
|
+
this.emit("change", this);
|
|
261
|
+
if (typeof state === "boolean")
|
|
262
|
+
this.emit(state ? "press" : "release", this);
|
|
226
263
|
}
|
|
227
|
-
|
|
228
|
-
this.emit("input", this, this);
|
|
264
|
+
this.emit("input", this);
|
|
229
265
|
}
|
|
230
266
|
}
|