@realitycollective/webxr-interactions 0.1.0-preview.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/CHANGELOG.md +21 -0
- package/LICENSE +21 -0
- package/README.md +42 -0
- package/dist/behaviours/behaviour.d.ts +63 -0
- package/dist/behaviours/behaviour.js +5 -0
- package/dist/behaviours/behaviour.js.map +1 -0
- package/dist/behaviours/dial.d.ts +36 -0
- package/dist/behaviours/dial.js +88 -0
- package/dist/behaviours/dial.js.map +1 -0
- package/dist/behaviours/factory.d.ts +24 -0
- package/dist/behaviours/factory.js +29 -0
- package/dist/behaviours/factory.js.map +1 -0
- package/dist/behaviours/grab.d.ts +21 -0
- package/dist/behaviours/grab.js +64 -0
- package/dist/behaviours/grab.js.map +1 -0
- package/dist/behaviours/hinge.d.ts +42 -0
- package/dist/behaviours/hinge.js +70 -0
- package/dist/behaviours/hinge.js.map +1 -0
- package/dist/behaviours/press.d.ts +57 -0
- package/dist/behaviours/press.js +109 -0
- package/dist/behaviours/press.js.map +1 -0
- package/dist/behaviours/pulse.d.ts +21 -0
- package/dist/behaviours/pulse.js +37 -0
- package/dist/behaviours/pulse.js.map +1 -0
- package/dist/behaviours/slide.d.ts +47 -0
- package/dist/behaviours/slide.js +104 -0
- package/dist/behaviours/slide.js.map +1 -0
- package/dist/behaviours/toss-score.d.ts +33 -0
- package/dist/behaviours/toss-score.js +58 -0
- package/dist/behaviours/toss-score.js.map +1 -0
- package/dist/descriptor.d.ts +21 -0
- package/dist/descriptor.js +2 -0
- package/dist/descriptor.js.map +1 -0
- package/dist/events.d.ts +32 -0
- package/dist/events.js +16 -0
- package/dist/events.js.map +1 -0
- package/dist/feedback.d.ts +31 -0
- package/dist/feedback.js +15 -0
- package/dist/feedback.js.map +1 -0
- package/dist/gaze.d.ts +44 -0
- package/dist/gaze.js +61 -0
- package/dist/gaze.js.map +1 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +35 -0
- package/dist/index.js.map +1 -0
- package/dist/math.d.ts +68 -0
- package/dist/math.js +164 -0
- package/dist/math.js.map +1 -0
- package/dist/pointer-bridge.d.ts +24 -0
- package/dist/pointer-bridge.js +60 -0
- package/dist/pointer-bridge.js.map +1 -0
- package/dist/ports.d.ts +50 -0
- package/dist/ports.js +2 -0
- package/dist/ports.js.map +1 -0
- package/dist/runtime.d.ts +97 -0
- package/dist/runtime.js +511 -0
- package/dist/runtime.js.map +1 -0
- package/package.json +51 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Press - a mechanical button. `driven`: the behaviour owns the transform,
|
|
3
|
+
* integrating an underdamped spring toward the logical target so the button
|
|
4
|
+
* visibly depresses, overshoots and settles (the pale-signal bounce).
|
|
5
|
+
* Supports momentary (default) and latching modes - latching is finally
|
|
6
|
+
* reachable from data, unlike the research prototype.
|
|
7
|
+
*/
|
|
8
|
+
import type { Vec3Tuple } from "@realitycollective/webxr-input";
|
|
9
|
+
import type { Behaviour, BehaviourContext, InteractorInfo } from "./behaviour.js";
|
|
10
|
+
export interface PressConfig {
|
|
11
|
+
kind: "press";
|
|
12
|
+
/** Local press axis; the button depresses along −axis. */
|
|
13
|
+
axis?: Vec3Tuple;
|
|
14
|
+
/** Full travel in meters. */
|
|
15
|
+
travel?: number;
|
|
16
|
+
/** Fraction of travel reached at full press. */
|
|
17
|
+
depthFraction?: number;
|
|
18
|
+
stiffness?: number;
|
|
19
|
+
damping?: number;
|
|
20
|
+
/** Latching: press toggles and holds; release is a no-op. */
|
|
21
|
+
latching?: boolean;
|
|
22
|
+
}
|
|
23
|
+
export declare class PressBehaviour implements Behaviour {
|
|
24
|
+
readonly kind = "press";
|
|
25
|
+
readonly ownership: "driven";
|
|
26
|
+
readonly requires: readonly [];
|
|
27
|
+
readonly requiresAnyOf: readonly [readonly ["rays", "pokes", "pointer2d", "gaze"]];
|
|
28
|
+
readonly grabbable = false;
|
|
29
|
+
private readonly axis;
|
|
30
|
+
private travel;
|
|
31
|
+
private depthFraction;
|
|
32
|
+
private stiffness;
|
|
33
|
+
private damping;
|
|
34
|
+
private readonly latching;
|
|
35
|
+
private target;
|
|
36
|
+
private latched;
|
|
37
|
+
private progress;
|
|
38
|
+
private velocity;
|
|
39
|
+
private lastEmitted;
|
|
40
|
+
private readonly spring;
|
|
41
|
+
constructor(config?: Omit<PressConfig, "kind">);
|
|
42
|
+
onPressStart(ctx: BehaviourContext, interactor: InteractorInfo): void;
|
|
43
|
+
onPressEnd(ctx: BehaviourContext, interactor: InteractorInfo): void;
|
|
44
|
+
update(ctx: BehaviourContext): void;
|
|
45
|
+
getValue(): number;
|
|
46
|
+
/** Latched state (latching mode). */
|
|
47
|
+
get isLatched(): boolean;
|
|
48
|
+
getTravel(): number;
|
|
49
|
+
setTravel(travel: number): void;
|
|
50
|
+
getDepthFraction(): number;
|
|
51
|
+
setDepthFraction(fraction: number): void;
|
|
52
|
+
getSpring(): {
|
|
53
|
+
stiffness: number;
|
|
54
|
+
damping: number;
|
|
55
|
+
};
|
|
56
|
+
setSpring(stiffness: number, damping: number): void;
|
|
57
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { springStep, vScale } from "../math.js";
|
|
2
|
+
const VALUE_EPSILON = 1e-3;
|
|
3
|
+
export class PressBehaviour {
|
|
4
|
+
kind = "press";
|
|
5
|
+
ownership = "driven";
|
|
6
|
+
requires = [];
|
|
7
|
+
requiresAnyOf = [["rays", "pokes", "pointer2d", "gaze"]];
|
|
8
|
+
grabbable = false;
|
|
9
|
+
axis;
|
|
10
|
+
travel;
|
|
11
|
+
depthFraction;
|
|
12
|
+
stiffness;
|
|
13
|
+
damping;
|
|
14
|
+
latching;
|
|
15
|
+
target = 0; // logical 0/1
|
|
16
|
+
latched = false;
|
|
17
|
+
progress = 0; // spring output 0..1
|
|
18
|
+
velocity = 0;
|
|
19
|
+
lastEmitted = -1;
|
|
20
|
+
spring = new Float32Array(2);
|
|
21
|
+
constructor(config = {}) {
|
|
22
|
+
this.axis = config.axis ?? [0, 1, 0];
|
|
23
|
+
this.travel = config.travel ?? 0.04;
|
|
24
|
+
this.depthFraction = config.depthFraction ?? 0.5;
|
|
25
|
+
this.stiffness = config.stiffness ?? 220;
|
|
26
|
+
this.damping = config.damping ?? 12;
|
|
27
|
+
this.latching = config.latching ?? false;
|
|
28
|
+
}
|
|
29
|
+
onPressStart(ctx, interactor) {
|
|
30
|
+
if (this.latching) {
|
|
31
|
+
this.latched = !this.latched;
|
|
32
|
+
this.target = this.latched ? 1 : 0;
|
|
33
|
+
ctx.emit({
|
|
34
|
+
type: this.latched ? "actuated" : "released",
|
|
35
|
+
behaviourKind: this.kind,
|
|
36
|
+
interactorId: interactor.id,
|
|
37
|
+
value: this.target,
|
|
38
|
+
});
|
|
39
|
+
ctx.feedback({
|
|
40
|
+
cue: this.latched ? "actuate" : "release",
|
|
41
|
+
behaviourKind: this.kind,
|
|
42
|
+
sourceId: interactor.id,
|
|
43
|
+
intensity: 0.8,
|
|
44
|
+
durationMs: 40,
|
|
45
|
+
});
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
this.target = 1;
|
|
49
|
+
ctx.emit({ type: "actuated", behaviourKind: this.kind, interactorId: interactor.id, value: 1 });
|
|
50
|
+
ctx.feedback({
|
|
51
|
+
cue: "actuate",
|
|
52
|
+
behaviourKind: this.kind,
|
|
53
|
+
sourceId: interactor.id,
|
|
54
|
+
intensity: 0.8,
|
|
55
|
+
durationMs: 40,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
onPressEnd(ctx, interactor) {
|
|
59
|
+
if (this.latching)
|
|
60
|
+
return;
|
|
61
|
+
this.target = 0;
|
|
62
|
+
ctx.emit({ type: "released", behaviourKind: this.kind, interactorId: interactor.id, value: 0 });
|
|
63
|
+
ctx.feedback({
|
|
64
|
+
cue: "release",
|
|
65
|
+
behaviourKind: this.kind,
|
|
66
|
+
sourceId: interactor.id,
|
|
67
|
+
intensity: 0.3,
|
|
68
|
+
durationMs: 20,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
update(ctx) {
|
|
72
|
+
springStep(this.progress, this.velocity, this.target, this.stiffness, this.damping, ctx.dt, this.spring);
|
|
73
|
+
this.progress = this.spring[0] ?? 0;
|
|
74
|
+
this.velocity = this.spring[1] ?? 0;
|
|
75
|
+
ctx.transform?.setLocalOffset(vScale(this.axis, -this.travel * this.depthFraction * this.progress));
|
|
76
|
+
if (Math.abs(this.progress - this.lastEmitted) > VALUE_EPSILON) {
|
|
77
|
+
this.lastEmitted = this.progress;
|
|
78
|
+
ctx.emit({ type: "valueChanged", behaviourKind: this.kind, value: this.getValue() });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
getValue() {
|
|
82
|
+
return this.target;
|
|
83
|
+
}
|
|
84
|
+
/** Latched state (latching mode). */
|
|
85
|
+
get isLatched() {
|
|
86
|
+
return this.latched;
|
|
87
|
+
}
|
|
88
|
+
// Live-tuning accessors (playground steppers).
|
|
89
|
+
getTravel() {
|
|
90
|
+
return this.travel;
|
|
91
|
+
}
|
|
92
|
+
setTravel(travel) {
|
|
93
|
+
this.travel = travel;
|
|
94
|
+
}
|
|
95
|
+
getDepthFraction() {
|
|
96
|
+
return this.depthFraction;
|
|
97
|
+
}
|
|
98
|
+
setDepthFraction(fraction) {
|
|
99
|
+
this.depthFraction = fraction;
|
|
100
|
+
}
|
|
101
|
+
getSpring() {
|
|
102
|
+
return { stiffness: this.stiffness, damping: this.damping };
|
|
103
|
+
}
|
|
104
|
+
setSpring(stiffness, damping) {
|
|
105
|
+
this.stiffness = stiffness;
|
|
106
|
+
this.damping = damping;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=press.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"press.js","sourceRoot":"","sources":["../../src/behaviours/press.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Press - a mechanical button. `driven`: the behaviour owns the transform,\n * integrating an underdamped spring toward the logical target so the button\n * visibly depresses, overshoots and settles (the pale-signal bounce).\n * Supports momentary (default) and latching modes - latching is finally\n * reachable from data, unlike the research prototype.\n */\nimport type { Vec3Tuple } from \"@realitycollective/webxr-input\";\nimport { springStep, vScale } from \"../math.js\";\nimport type { Behaviour, BehaviourContext, InteractorInfo } from \"./behaviour.js\";\n\nexport interface PressConfig {\n kind: \"press\";\n /** Local press axis; the button depresses along −axis. */\n axis?: Vec3Tuple;\n /** Full travel in meters. */\n travel?: number;\n /** Fraction of travel reached at full press. */\n depthFraction?: number;\n stiffness?: number;\n damping?: number;\n /** Latching: press toggles and holds; release is a no-op. */\n latching?: boolean;\n}\n\nconst VALUE_EPSILON = 1e-3;\n\nexport class PressBehaviour implements Behaviour {\n readonly kind = \"press\";\n readonly ownership = \"driven\" as const;\n readonly requires = [] as const;\n readonly requiresAnyOf = [[\"rays\", \"pokes\", \"pointer2d\", \"gaze\"]] as const;\n readonly grabbable = false;\n\n private readonly axis: Vec3Tuple;\n private travel: number;\n private depthFraction: number;\n private stiffness: number;\n private damping: number;\n private readonly latching: boolean;\n\n private target = 0; // logical 0/1\n private latched = false;\n private progress = 0; // spring output 0..1\n private velocity = 0;\n private lastEmitted = -1;\n private readonly spring = new Float32Array(2);\n\n constructor(config: Omit<PressConfig, \"kind\"> = {}) {\n this.axis = config.axis ?? [0, 1, 0];\n this.travel = config.travel ?? 0.04;\n this.depthFraction = config.depthFraction ?? 0.5;\n this.stiffness = config.stiffness ?? 220;\n this.damping = config.damping ?? 12;\n this.latching = config.latching ?? false;\n }\n\n onPressStart(ctx: BehaviourContext, interactor: InteractorInfo): void {\n if (this.latching) {\n this.latched = !this.latched;\n this.target = this.latched ? 1 : 0;\n ctx.emit({\n type: this.latched ? \"actuated\" : \"released\",\n behaviourKind: this.kind,\n interactorId: interactor.id,\n value: this.target,\n });\n ctx.feedback({\n cue: this.latched ? \"actuate\" : \"release\",\n behaviourKind: this.kind,\n sourceId: interactor.id,\n intensity: 0.8,\n durationMs: 40,\n });\n return;\n }\n this.target = 1;\n ctx.emit({ type: \"actuated\", behaviourKind: this.kind, interactorId: interactor.id, value: 1 });\n ctx.feedback({\n cue: \"actuate\",\n behaviourKind: this.kind,\n sourceId: interactor.id,\n intensity: 0.8,\n durationMs: 40,\n });\n }\n\n onPressEnd(ctx: BehaviourContext, interactor: InteractorInfo): void {\n if (this.latching) return;\n this.target = 0;\n ctx.emit({ type: \"released\", behaviourKind: this.kind, interactorId: interactor.id, value: 0 });\n ctx.feedback({\n cue: \"release\",\n behaviourKind: this.kind,\n sourceId: interactor.id,\n intensity: 0.3,\n durationMs: 20,\n });\n }\n\n update(ctx: BehaviourContext): void {\n springStep(\n this.progress,\n this.velocity,\n this.target,\n this.stiffness,\n this.damping,\n ctx.dt,\n this.spring,\n );\n this.progress = this.spring[0] ?? 0;\n this.velocity = this.spring[1] ?? 0;\n ctx.transform?.setLocalOffset(\n vScale(this.axis, -this.travel * this.depthFraction * this.progress),\n );\n if (Math.abs(this.progress - this.lastEmitted) > VALUE_EPSILON) {\n this.lastEmitted = this.progress;\n ctx.emit({ type: \"valueChanged\", behaviourKind: this.kind, value: this.getValue() });\n }\n }\n\n getValue(): number {\n return this.target;\n }\n\n /** Latched state (latching mode). */\n get isLatched(): boolean {\n return this.latched;\n }\n\n // Live-tuning accessors (playground steppers).\n getTravel(): number {\n return this.travel;\n }\n setTravel(travel: number): void {\n this.travel = travel;\n }\n getDepthFraction(): number {\n return this.depthFraction;\n }\n setDepthFraction(fraction: number): void {\n this.depthFraction = fraction;\n }\n getSpring(): { stiffness: number; damping: number } {\n return { stiffness: this.stiffness, damping: this.damping };\n }\n setSpring(stiffness: number, damping: number): void {\n this.stiffness = stiffness;\n this.damping = damping;\n }\n}\n"]}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Behaviour, BehaviourContext, InteractorInfo } from "./behaviour.js";
|
|
2
|
+
export interface PulseConfig {
|
|
3
|
+
kind: "pulse";
|
|
4
|
+
scaleAmount?: number;
|
|
5
|
+
emissiveAmount?: number;
|
|
6
|
+
decayRate?: number;
|
|
7
|
+
}
|
|
8
|
+
export declare class PulseBehaviour implements Behaviour {
|
|
9
|
+
readonly kind = "pulse";
|
|
10
|
+
readonly ownership: "driven";
|
|
11
|
+
readonly requires: readonly [];
|
|
12
|
+
readonly grabbable = false;
|
|
13
|
+
private readonly scaleAmount;
|
|
14
|
+
private readonly emissiveAmount;
|
|
15
|
+
private readonly decayRate;
|
|
16
|
+
private level;
|
|
17
|
+
constructor(config?: Omit<PulseConfig, "kind">);
|
|
18
|
+
onPressStart(_ctx: BehaviourContext, _interactor: InteractorInfo): void;
|
|
19
|
+
update(ctx: BehaviourContext): void;
|
|
20
|
+
getValue(): number;
|
|
21
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse - an additive press effect: a decaying scale + emissive flash on
|
|
3
|
+
* actuation. Composes with any motion behaviour on the same interactable
|
|
4
|
+
* (multiple behaviours per object, the pale-signal beacon pattern).
|
|
5
|
+
*/
|
|
6
|
+
import { decay } from "../math.js";
|
|
7
|
+
export class PulseBehaviour {
|
|
8
|
+
kind = "pulse";
|
|
9
|
+
ownership = "driven";
|
|
10
|
+
requires = [];
|
|
11
|
+
grabbable = false;
|
|
12
|
+
scaleAmount;
|
|
13
|
+
emissiveAmount;
|
|
14
|
+
decayRate;
|
|
15
|
+
level = 0;
|
|
16
|
+
constructor(config = {}) {
|
|
17
|
+
this.scaleAmount = config.scaleAmount ?? 0.25;
|
|
18
|
+
this.emissiveAmount = config.emissiveAmount ?? 1.5;
|
|
19
|
+
this.decayRate = config.decayRate ?? 6;
|
|
20
|
+
}
|
|
21
|
+
onPressStart(_ctx, _interactor) {
|
|
22
|
+
this.level = 1;
|
|
23
|
+
}
|
|
24
|
+
update(ctx) {
|
|
25
|
+
if (this.level === 0)
|
|
26
|
+
return;
|
|
27
|
+
this.level = decay(this.level, this.decayRate, ctx.dt);
|
|
28
|
+
ctx.transform?.setEffect?.({
|
|
29
|
+
scale: 1 + this.scaleAmount * this.level,
|
|
30
|
+
emissive: this.emissiveAmount * this.level,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
getValue() {
|
|
34
|
+
return this.level;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=pulse.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pulse.js","sourceRoot":"","sources":["../../src/behaviours/pulse.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAUnC,MAAM,OAAO,cAAc;IAChB,IAAI,GAAG,OAAO,CAAC;IACf,SAAS,GAAG,QAAiB,CAAC;IAC9B,QAAQ,GAAG,EAAW,CAAC;IACvB,SAAS,GAAG,KAAK,CAAC;IAEV,WAAW,CAAS;IACpB,cAAc,CAAS;IACvB,SAAS,CAAS;IAC3B,KAAK,GAAG,CAAC,CAAC;IAElB,YAAY,SAAoC,EAAE;QAChD,IAAI,CAAC,WAAW,GAAG,MAAM,CAAC,WAAW,IAAI,IAAI,CAAC;QAC9C,IAAI,CAAC,cAAc,GAAG,MAAM,CAAC,cAAc,IAAI,GAAG,CAAC;QACnD,IAAI,CAAC,SAAS,GAAG,MAAM,CAAC,SAAS,IAAI,CAAC,CAAC;IACzC,CAAC;IAED,YAAY,CAAC,IAAsB,EAAE,WAA2B;QAC9D,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC;IACjB,CAAC;IAED,MAAM,CAAC,GAAqB;QAC1B,IAAI,IAAI,CAAC,KAAK,KAAK,CAAC;YAAE,OAAO;QAC7B,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,SAAS,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;QACvD,GAAG,CAAC,SAAS,EAAE,SAAS,EAAE,CAAC;YACzB,KAAK,EAAE,CAAC,GAAG,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,KAAK;YACxC,QAAQ,EAAE,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,KAAK;SAC3C,CAAC,CAAC;IACL,CAAC;IAED,QAAQ;QACN,OAAO,IAAI,CAAC,KAAK,CAAC;IACpB,CAAC;CACF","sourcesContent":["/**\n * Pulse - an additive press effect: a decaying scale + emissive flash on\n * actuation. Composes with any motion behaviour on the same interactable\n * (multiple behaviours per object, the pale-signal beacon pattern).\n */\nimport { decay } from \"../math.js\";\nimport type { Behaviour, BehaviourContext, InteractorInfo } from \"./behaviour.js\";\n\nexport interface PulseConfig {\n kind: \"pulse\";\n scaleAmount?: number;\n emissiveAmount?: number;\n decayRate?: number;\n}\n\nexport class PulseBehaviour implements Behaviour {\n readonly kind = \"pulse\";\n readonly ownership = \"driven\" as const;\n readonly requires = [] as const;\n readonly grabbable = false;\n\n private readonly scaleAmount: number;\n private readonly emissiveAmount: number;\n private readonly decayRate: number;\n private level = 0;\n\n constructor(config: Omit<PulseConfig, \"kind\"> = {}) {\n this.scaleAmount = config.scaleAmount ?? 0.25;\n this.emissiveAmount = config.emissiveAmount ?? 1.5;\n this.decayRate = config.decayRate ?? 6;\n }\n\n onPressStart(_ctx: BehaviourContext, _interactor: InteractorInfo): void {\n this.level = 1;\n }\n\n update(ctx: BehaviourContext): void {\n if (this.level === 0) return;\n this.level = decay(this.level, this.decayRate, ctx.dt);\n ctx.transform?.setEffect?.({\n scale: 1 + this.scaleAmount * this.level,\n emissive: this.emissiveAmount * this.level,\n });\n }\n\n getValue(): number {\n return this.level;\n }\n}\n"]}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slide - a pulley/handle pulled along one local axis. `handDriven` while
|
|
3
|
+
* held (the pull follows the hand's displacement along −axis), `driven`
|
|
4
|
+
* on release: a spring returns the handle home while the value keeps
|
|
5
|
+
* publishing. Value = pull / travel.
|
|
6
|
+
*/
|
|
7
|
+
import type { Vec3Tuple } from "@realitycollective/webxr-input";
|
|
8
|
+
import { type Behaviour, type BehaviourContext, type InteractorInfo } from "./behaviour.js";
|
|
9
|
+
export interface SlideConfig {
|
|
10
|
+
kind: "slide";
|
|
11
|
+
/** Local rail axis; the handle is pulled along −axis. */
|
|
12
|
+
axis?: Vec3Tuple;
|
|
13
|
+
/** Full pull travel in meters. */
|
|
14
|
+
travel?: number;
|
|
15
|
+
stiffness?: number;
|
|
16
|
+
damping?: number;
|
|
17
|
+
}
|
|
18
|
+
export declare class SlideBehaviour implements Behaviour {
|
|
19
|
+
readonly kind = "slide";
|
|
20
|
+
readonly ownership: "handDriven";
|
|
21
|
+
readonly requires: readonly ["grabs"];
|
|
22
|
+
readonly grabbable = true;
|
|
23
|
+
private readonly axis;
|
|
24
|
+
private travel;
|
|
25
|
+
private stiffness;
|
|
26
|
+
private damping;
|
|
27
|
+
private pull;
|
|
28
|
+
private velocity;
|
|
29
|
+
private pullAtGrab;
|
|
30
|
+
private handAlongAtGrab;
|
|
31
|
+
private held;
|
|
32
|
+
private lastEmitted;
|
|
33
|
+
private readonly spring;
|
|
34
|
+
constructor(config?: Omit<SlideConfig, "kind">);
|
|
35
|
+
private handAlong;
|
|
36
|
+
onGrabStart(ctx: BehaviourContext, interactor: InteractorInfo): void;
|
|
37
|
+
onGrabEnd(ctx: BehaviourContext, interactor: InteractorInfo): void;
|
|
38
|
+
update(ctx: BehaviourContext, holder?: InteractorInfo): void;
|
|
39
|
+
getValue(): number;
|
|
40
|
+
setTravel(travel: number): void;
|
|
41
|
+
getTravel(): number;
|
|
42
|
+
setSpring(stiffness: number, damping: number): void;
|
|
43
|
+
getSpring(): {
|
|
44
|
+
stiffness: number;
|
|
45
|
+
damping: number;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { springStep, vDot, vScale, worldToLocal } from "../math.js";
|
|
2
|
+
import { holderPoint } from "./behaviour.js";
|
|
3
|
+
const VALUE_EPSILON = 1e-3;
|
|
4
|
+
export class SlideBehaviour {
|
|
5
|
+
kind = "slide";
|
|
6
|
+
ownership = "handDriven";
|
|
7
|
+
requires = ["grabs"];
|
|
8
|
+
grabbable = true;
|
|
9
|
+
axis;
|
|
10
|
+
travel;
|
|
11
|
+
stiffness;
|
|
12
|
+
damping;
|
|
13
|
+
pull = 0; // meters along −axis, 0..travel
|
|
14
|
+
velocity = 0;
|
|
15
|
+
pullAtGrab = 0;
|
|
16
|
+
handAlongAtGrab = 0;
|
|
17
|
+
held = false;
|
|
18
|
+
lastEmitted = -1;
|
|
19
|
+
spring = new Float32Array(2);
|
|
20
|
+
constructor(config = {}) {
|
|
21
|
+
this.axis = config.axis ?? [0, 1, 0];
|
|
22
|
+
this.travel = config.travel ?? 0.3;
|
|
23
|
+
this.stiffness = config.stiffness ?? 180;
|
|
24
|
+
this.damping = config.damping ?? 14;
|
|
25
|
+
}
|
|
26
|
+
handAlong(ctx, holder) {
|
|
27
|
+
const hand = holderPoint(holder);
|
|
28
|
+
const transform = ctx.transform;
|
|
29
|
+
if (!hand || !transform)
|
|
30
|
+
return null;
|
|
31
|
+
const rest = transform.getWorldPose();
|
|
32
|
+
const local = worldToLocal(hand, rest.position, rest.quaternion);
|
|
33
|
+
// Pull is measured along −axis.
|
|
34
|
+
return -vDot(local, this.axis);
|
|
35
|
+
}
|
|
36
|
+
onGrabStart(ctx, interactor) {
|
|
37
|
+
this.held = true;
|
|
38
|
+
this.pullAtGrab = this.pull;
|
|
39
|
+
this.handAlongAtGrab = this.handAlong(ctx, interactor) ?? 0;
|
|
40
|
+
this.velocity = 0;
|
|
41
|
+
ctx.emit({ type: "grabStart", behaviourKind: this.kind, interactorId: interactor.id });
|
|
42
|
+
ctx.feedback({
|
|
43
|
+
cue: "grab",
|
|
44
|
+
behaviourKind: this.kind,
|
|
45
|
+
sourceId: interactor.id,
|
|
46
|
+
intensity: 0.4,
|
|
47
|
+
durationMs: 25,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
onGrabEnd(ctx, interactor) {
|
|
51
|
+
this.held = false;
|
|
52
|
+
ctx.emit({ type: "grabEnd", behaviourKind: this.kind, interactorId: interactor.id });
|
|
53
|
+
ctx.feedback({
|
|
54
|
+
cue: "drop",
|
|
55
|
+
behaviourKind: this.kind,
|
|
56
|
+
sourceId: interactor.id,
|
|
57
|
+
intensity: 0.2,
|
|
58
|
+
durationMs: 15,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
update(ctx, holder) {
|
|
62
|
+
if (this.held && holder) {
|
|
63
|
+
const along = this.handAlong(ctx, holder);
|
|
64
|
+
if (along !== null) {
|
|
65
|
+
const next = this.pullAtGrab + (along - this.handAlongAtGrab);
|
|
66
|
+
this.pull = Math.min(this.travel, Math.max(0, next));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
else if (this.pull > 0 || Math.abs(this.velocity) > 1e-4) {
|
|
70
|
+
springStep(this.pull, this.velocity, 0, this.stiffness, this.damping, ctx.dt, this.spring);
|
|
71
|
+
this.pull = Math.max(0, this.spring[0] ?? 0);
|
|
72
|
+
this.velocity = this.spring[1] ?? 0;
|
|
73
|
+
if (this.pull < 1e-4 && Math.abs(this.velocity) < 1e-3) {
|
|
74
|
+
this.pull = 0;
|
|
75
|
+
this.velocity = 0;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
ctx.transform?.setLocalOffset(vScale(this.axis, -this.pull));
|
|
79
|
+
const value = this.getValue();
|
|
80
|
+
if (Math.abs(value - this.lastEmitted) > VALUE_EPSILON) {
|
|
81
|
+
this.lastEmitted = value;
|
|
82
|
+
ctx.emit({ type: "valueChanged", behaviourKind: this.kind, value });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
getValue() {
|
|
86
|
+
return this.travel < 1e-9 ? 0 : Math.min(1, this.pull / this.travel);
|
|
87
|
+
}
|
|
88
|
+
setTravel(travel) {
|
|
89
|
+
this.travel = travel;
|
|
90
|
+
if (this.pull > travel)
|
|
91
|
+
this.pull = travel;
|
|
92
|
+
}
|
|
93
|
+
getTravel() {
|
|
94
|
+
return this.travel;
|
|
95
|
+
}
|
|
96
|
+
setSpring(stiffness, damping) {
|
|
97
|
+
this.stiffness = stiffness;
|
|
98
|
+
this.damping = damping;
|
|
99
|
+
}
|
|
100
|
+
getSpring() {
|
|
101
|
+
return { stiffness: this.stiffness, damping: this.damping };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=slide.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slide.js","sourceRoot":"","sources":["../../src/behaviours/slide.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Slide - a pulley/handle pulled along one local axis. `handDriven` while\n * held (the pull follows the hand's displacement along −axis), `driven`\n * on release: a spring returns the handle home while the value keeps\n * publishing. Value = pull / travel.\n */\nimport type { Vec3Tuple } from \"@realitycollective/webxr-input\";\nimport { springStep, vDot, vScale, worldToLocal } from \"../math.js\";\nimport { holderPoint, type Behaviour, type BehaviourContext, type InteractorInfo } from \"./behaviour.js\";\n\nexport interface SlideConfig {\n kind: \"slide\";\n /** Local rail axis; the handle is pulled along −axis. */\n axis?: Vec3Tuple;\n /** Full pull travel in meters. */\n travel?: number;\n stiffness?: number;\n damping?: number;\n}\n\nconst VALUE_EPSILON = 1e-3;\n\nexport class SlideBehaviour implements Behaviour {\n readonly kind = \"slide\";\n readonly ownership = \"handDriven\" as const;\n readonly requires = [\"grabs\"] as const;\n readonly grabbable = true;\n\n private readonly axis: Vec3Tuple;\n private travel: number;\n private stiffness: number;\n private damping: number;\n\n private pull = 0; // meters along −axis, 0..travel\n private velocity = 0;\n private pullAtGrab = 0;\n private handAlongAtGrab = 0;\n private held = false;\n private lastEmitted = -1;\n private readonly spring = new Float32Array(2);\n\n constructor(config: Omit<SlideConfig, \"kind\"> = {}) {\n this.axis = config.axis ?? [0, 1, 0];\n this.travel = config.travel ?? 0.3;\n this.stiffness = config.stiffness ?? 180;\n this.damping = config.damping ?? 14;\n }\n\n private handAlong(ctx: BehaviourContext, holder: InteractorInfo): number | null {\n const hand = holderPoint(holder);\n const transform = ctx.transform;\n if (!hand || !transform) return null;\n const rest = transform.getWorldPose();\n const local = worldToLocal(hand, rest.position, rest.quaternion);\n // Pull is measured along −axis.\n return -vDot(local, this.axis);\n }\n\n onGrabStart(ctx: BehaviourContext, interactor: InteractorInfo): void {\n this.held = true;\n this.pullAtGrab = this.pull;\n this.handAlongAtGrab = this.handAlong(ctx, interactor) ?? 0;\n this.velocity = 0;\n ctx.emit({ type: \"grabStart\", behaviourKind: this.kind, interactorId: interactor.id });\n ctx.feedback({\n cue: \"grab\",\n behaviourKind: this.kind,\n sourceId: interactor.id,\n intensity: 0.4,\n durationMs: 25,\n });\n }\n\n onGrabEnd(ctx: BehaviourContext, interactor: InteractorInfo): void {\n this.held = false;\n ctx.emit({ type: \"grabEnd\", behaviourKind: this.kind, interactorId: interactor.id });\n ctx.feedback({\n cue: \"drop\",\n behaviourKind: this.kind,\n sourceId: interactor.id,\n intensity: 0.2,\n durationMs: 15,\n });\n }\n\n update(ctx: BehaviourContext, holder?: InteractorInfo): void {\n if (this.held && holder) {\n const along = this.handAlong(ctx, holder);\n if (along !== null) {\n const next = this.pullAtGrab + (along - this.handAlongAtGrab);\n this.pull = Math.min(this.travel, Math.max(0, next));\n }\n } else if (this.pull > 0 || Math.abs(this.velocity) > 1e-4) {\n springStep(this.pull, this.velocity, 0, this.stiffness, this.damping, ctx.dt, this.spring);\n this.pull = Math.max(0, this.spring[0] ?? 0);\n this.velocity = this.spring[1] ?? 0;\n if (this.pull < 1e-4 && Math.abs(this.velocity) < 1e-3) {\n this.pull = 0;\n this.velocity = 0;\n }\n }\n ctx.transform?.setLocalOffset(vScale(this.axis, -this.pull));\n const value = this.getValue();\n if (Math.abs(value - this.lastEmitted) > VALUE_EPSILON) {\n this.lastEmitted = value;\n ctx.emit({ type: \"valueChanged\", behaviourKind: this.kind, value });\n }\n }\n\n getValue(): number {\n return this.travel < 1e-9 ? 0 : Math.min(1, this.pull / this.travel);\n }\n\n setTravel(travel: number): void {\n this.travel = travel;\n if (this.pull > travel) this.pull = travel;\n }\n\n getTravel(): number {\n return this.travel;\n }\n\n setSpring(stiffness: number, damping: number): void {\n this.stiffness = stiffness;\n this.damping = damping;\n }\n\n getSpring(): { stiffness: number; damping: number } {\n return { stiffness: this.stiffness, damping: this.damping };\n }\n}\n"]}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TossScore - attach to a hoop/goal; scores registered target objects that
|
|
3
|
+
* pass through its plane in the scoring direction. Scoring is geometric
|
|
4
|
+
* and engine-free; how the object gets thrown (native physics, poseOnly
|
|
5
|
+
* carry + client ballistics) is not this behaviour's concern.
|
|
6
|
+
*/
|
|
7
|
+
import type { Vec3Tuple } from "@realitycollective/webxr-input";
|
|
8
|
+
import type { Behaviour, BehaviourContext } from "./behaviour.js";
|
|
9
|
+
export interface TossScoreConfig {
|
|
10
|
+
kind: "tossScore";
|
|
11
|
+
/** Interactable ids of the objects that can score. */
|
|
12
|
+
targetIds: string[];
|
|
13
|
+
/** Ring radius in meters. */
|
|
14
|
+
radius?: number;
|
|
15
|
+
/** Hoop plane normal (local "up" of the ring). */
|
|
16
|
+
axis?: Vec3Tuple;
|
|
17
|
+
}
|
|
18
|
+
export declare class TossScoreBehaviour implements Behaviour {
|
|
19
|
+
readonly kind = "tossScore";
|
|
20
|
+
readonly ownership: "driven";
|
|
21
|
+
readonly requires: readonly [];
|
|
22
|
+
readonly grabbable = false;
|
|
23
|
+
private readonly targetIds;
|
|
24
|
+
private radius;
|
|
25
|
+
private readonly axis;
|
|
26
|
+
private readonly prevAlong;
|
|
27
|
+
private scoredCount;
|
|
28
|
+
constructor(config: Omit<TossScoreConfig, "kind">);
|
|
29
|
+
update(ctx: BehaviourContext): void;
|
|
30
|
+
getValue(): number;
|
|
31
|
+
setRadius(radius: number): void;
|
|
32
|
+
getRadius(): number;
|
|
33
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { passedThroughHoop, vDot, vSub } from "../math.js";
|
|
2
|
+
export class TossScoreBehaviour {
|
|
3
|
+
kind = "tossScore";
|
|
4
|
+
ownership = "driven";
|
|
5
|
+
requires = [];
|
|
6
|
+
grabbable = false;
|
|
7
|
+
targetIds;
|
|
8
|
+
radius;
|
|
9
|
+
axis;
|
|
10
|
+
prevAlong = new Map();
|
|
11
|
+
scoredCount = 0;
|
|
12
|
+
constructor(config) {
|
|
13
|
+
this.targetIds = [...config.targetIds];
|
|
14
|
+
this.radius = config.radius ?? 0.2;
|
|
15
|
+
this.axis = config.axis ?? [0, 1, 0];
|
|
16
|
+
}
|
|
17
|
+
update(ctx) {
|
|
18
|
+
const transform = ctx.transform;
|
|
19
|
+
if (!transform)
|
|
20
|
+
return;
|
|
21
|
+
const hoop = transform.getWorldPose();
|
|
22
|
+
for (const targetId of this.targetIds) {
|
|
23
|
+
const target = ctx.getWorldPose(targetId);
|
|
24
|
+
if (!target)
|
|
25
|
+
continue;
|
|
26
|
+
const offset = vSub(target.position, hoop.position);
|
|
27
|
+
const along = vDot(offset, this.axis);
|
|
28
|
+
const radialVec = vSub(offset, [
|
|
29
|
+
this.axis[0] * along,
|
|
30
|
+
this.axis[1] * along,
|
|
31
|
+
this.axis[2] * along,
|
|
32
|
+
]);
|
|
33
|
+
const radial = Math.hypot(radialVec[0], radialVec[1], radialVec[2]);
|
|
34
|
+
const prev = this.prevAlong.get(targetId);
|
|
35
|
+
if (prev !== undefined && passedThroughHoop(prev, along, 0, radial, this.radius)) {
|
|
36
|
+
this.scoredCount += 1;
|
|
37
|
+
ctx.emit({ type: "scored", behaviourKind: this.kind, value: this.scoredCount });
|
|
38
|
+
ctx.feedback({
|
|
39
|
+
cue: "score",
|
|
40
|
+
behaviourKind: this.kind,
|
|
41
|
+
intensity: 1,
|
|
42
|
+
durationMs: 60,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
this.prevAlong.set(targetId, along);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
getValue() {
|
|
49
|
+
return this.scoredCount;
|
|
50
|
+
}
|
|
51
|
+
setRadius(radius) {
|
|
52
|
+
this.radius = radius;
|
|
53
|
+
}
|
|
54
|
+
getRadius() {
|
|
55
|
+
return this.radius;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=toss-score.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"toss-score.js","sourceRoot":"","sources":["../../src/behaviours/toss-score.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * TossScore - attach to a hoop/goal; scores registered target objects that\n * pass through its plane in the scoring direction. Scoring is geometric\n * and engine-free; how the object gets thrown (native physics, poseOnly\n * carry + client ballistics) is not this behaviour's concern.\n */\nimport type { Vec3Tuple } from \"@realitycollective/webxr-input\";\nimport { passedThroughHoop, vDot, vSub } from \"../math.js\";\nimport type { Behaviour, BehaviourContext } from \"./behaviour.js\";\n\nexport interface TossScoreConfig {\n kind: \"tossScore\";\n /** Interactable ids of the objects that can score. */\n targetIds: string[];\n /** Ring radius in meters. */\n radius?: number;\n /** Hoop plane normal (local \"up\" of the ring). */\n axis?: Vec3Tuple;\n}\n\nexport class TossScoreBehaviour implements Behaviour {\n readonly kind = \"tossScore\";\n readonly ownership = \"driven\" as const;\n readonly requires = [] as const;\n readonly grabbable = false;\n\n private readonly targetIds: string[];\n private radius: number;\n private readonly axis: Vec3Tuple;\n private readonly prevAlong = new Map<string, number>();\n private scoredCount = 0;\n\n constructor(config: Omit<TossScoreConfig, \"kind\">) {\n this.targetIds = [...config.targetIds];\n this.radius = config.radius ?? 0.2;\n this.axis = config.axis ?? [0, 1, 0];\n }\n\n update(ctx: BehaviourContext): void {\n const transform = ctx.transform;\n if (!transform) return;\n const hoop = transform.getWorldPose();\n for (const targetId of this.targetIds) {\n const target = ctx.getWorldPose(targetId);\n if (!target) continue;\n const offset = vSub(target.position, hoop.position);\n const along = vDot(offset, this.axis);\n const radialVec = vSub(offset, [\n this.axis[0] * along,\n this.axis[1] * along,\n this.axis[2] * along,\n ]);\n const radial = Math.hypot(radialVec[0], radialVec[1], radialVec[2]);\n const prev = this.prevAlong.get(targetId);\n if (prev !== undefined && passedThroughHoop(prev, along, 0, radial, this.radius)) {\n this.scoredCount += 1;\n ctx.emit({ type: \"scored\", behaviourKind: this.kind, value: this.scoredCount });\n ctx.feedback({\n cue: \"score\",\n behaviourKind: this.kind,\n intensity: 1,\n durationMs: 60,\n });\n }\n this.prevAlong.set(targetId, along);\n }\n }\n\n getValue(): number {\n return this.scoredCount;\n }\n\n setRadius(radius: number): void {\n this.radius = radius;\n }\n\n getRadius(): number {\n return this.radius;\n }\n}\n"]}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* InteractionDescriptor - the portable scene-interaction format. Plain
|
|
3
|
+
* JSON-able data: which interactables exist, which behaviours they carry
|
|
4
|
+
* (with tuning), and their gaze policy. Every adapter builds the SAME
|
|
5
|
+
* playground from the same descriptor - the portability proof inherited
|
|
6
|
+
* from the UI Extensions' SceneDescriptor.
|
|
7
|
+
*/
|
|
8
|
+
import type { BehaviourConfig } from "./behaviours/factory.js";
|
|
9
|
+
import type { GazeConfig } from "./gaze.js";
|
|
10
|
+
export interface InteractableDescriptor {
|
|
11
|
+
id: string;
|
|
12
|
+
behaviours: BehaviourConfig[];
|
|
13
|
+
gaze?: GazeConfig;
|
|
14
|
+
/** Registered disabled when false (enable later via the runtime). */
|
|
15
|
+
enabled?: boolean;
|
|
16
|
+
/** Poke/proximity trigger radius in meters (default 0.05). */
|
|
17
|
+
pokeRadius?: number;
|
|
18
|
+
}
|
|
19
|
+
export interface InteractionDescriptor {
|
|
20
|
+
interactables: InteractableDescriptor[];
|
|
21
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"descriptor.js","sourceRoot":"","sources":["../src/descriptor.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * InteractionDescriptor - the portable scene-interaction format. Plain\n * JSON-able data: which interactables exist, which behaviours they carry\n * (with tuning), and their gaze policy. Every adapter builds the SAME\n * playground from the same descriptor - the portability proof inherited\n * from the UI Extensions' SceneDescriptor.\n */\nimport type { BehaviourConfig } from \"./behaviours/factory.js\";\nimport type { GazeConfig } from \"./gaze.js\";\n\nexport interface InteractableDescriptor {\n id: string;\n behaviours: BehaviourConfig[];\n gaze?: GazeConfig;\n /** Registered disabled when false (enable later via the runtime). */\n enabled?: boolean;\n /** Poke/proximity trigger radius in meters (default 0.05). */\n pokeRadius?: number;\n}\n\nexport interface InteractionDescriptor {\n interactables: InteractableDescriptor[];\n}\n"]}
|
package/dist/events.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interaction events - THE only outbound pathway from the interaction core.
|
|
3
|
+
*
|
|
4
|
+
* The Pale Signal research showed what happens when events are optional:
|
|
5
|
+
* consumers bypass them and subscribe to raw engine tags, and the same
|
|
6
|
+
* input ends up with two independent listeners. Here, adapters feed input
|
|
7
|
+
* IN through `@realitycollective/webxr-input`, and everything downstream
|
|
8
|
+
* (gameplay, UI, feedback) subscribes to these events OUT. No side doors.
|
|
9
|
+
*/
|
|
10
|
+
import type { Unsubscribe } from "@realitycollective/webxr-input";
|
|
11
|
+
export type InteractionEventType = "hoverEnter" | "hoverExit" | "dwellProgress" | "pressStart" | "pressEnd" | "actuated" | "released" | "valueChanged" | "grabStart" | "grabEnd" | "scored" | "behaviourEnabled" | "behaviourDisabled" | "interactableEnabled" | "interactableDisabled";
|
|
12
|
+
export interface InteractionEvent {
|
|
13
|
+
type: InteractionEventType;
|
|
14
|
+
/** The target interactable. */
|
|
15
|
+
interactableId: string;
|
|
16
|
+
/** The behaviour that produced the event, when behaviour-scoped. */
|
|
17
|
+
behaviourKind?: string;
|
|
18
|
+
/** The input source/interactor involved, when input-scoped. */
|
|
19
|
+
interactorId?: string;
|
|
20
|
+
/** Analog payload (valueChanged, dwellProgress) or 1/0 for digital. */
|
|
21
|
+
value?: number;
|
|
22
|
+
/** Why a behaviour was disabled (unmet capability requirements). */
|
|
23
|
+
reason?: string;
|
|
24
|
+
}
|
|
25
|
+
export type InteractionEventListener = (event: InteractionEvent) => void;
|
|
26
|
+
/** Minimal synchronous emitter (insertion-ordered, unsubscribe-safe). */
|
|
27
|
+
export declare class Emitter<T> {
|
|
28
|
+
private listeners;
|
|
29
|
+
subscribe(listener: (value: T) => void): Unsubscribe;
|
|
30
|
+
emit(value: T): void;
|
|
31
|
+
get size(): number;
|
|
32
|
+
}
|
package/dist/events.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Minimal synchronous emitter (insertion-ordered, unsubscribe-safe). */
|
|
2
|
+
export class Emitter {
|
|
3
|
+
listeners = new Set();
|
|
4
|
+
subscribe(listener) {
|
|
5
|
+
this.listeners.add(listener);
|
|
6
|
+
return () => this.listeners.delete(listener);
|
|
7
|
+
}
|
|
8
|
+
emit(value) {
|
|
9
|
+
for (const listener of [...this.listeners])
|
|
10
|
+
listener(value);
|
|
11
|
+
}
|
|
12
|
+
get size() {
|
|
13
|
+
return this.listeners.size;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=events.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"events.js","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"AA4CA,yEAAyE;AACzE,MAAM,OAAO,OAAO;IACV,SAAS,GAAG,IAAI,GAAG,EAAsB,CAAC;IAElD,SAAS,CAAC,QAA4B;QACpC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC7B,OAAO,GAAG,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IAC/C,CAAC;IAED,IAAI,CAAC,KAAQ;QACX,KAAK,MAAM,QAAQ,IAAI,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC;YAAE,QAAQ,CAAC,KAAK,CAAC,CAAC;IAC9D,CAAC;IAED,IAAI,IAAI;QACN,OAAO,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;IAC7B,CAAC;CACF","sourcesContent":["/**\n * Interaction events - THE only outbound pathway from the interaction core.\n *\n * The Pale Signal research showed what happens when events are optional:\n * consumers bypass them and subscribe to raw engine tags, and the same\n * input ends up with two independent listeners. Here, adapters feed input\n * IN through `@realitycollective/webxr-input`, and everything downstream\n * (gameplay, UI, feedback) subscribes to these events OUT. No side doors.\n */\nimport type { Unsubscribe } from \"@realitycollective/webxr-input\";\n\nexport type InteractionEventType =\n | \"hoverEnter\"\n | \"hoverExit\"\n | \"dwellProgress\" // gaze-dwell fill 0..1 (value); 1 fires the press\n | \"pressStart\"\n | \"pressEnd\"\n | \"actuated\" // logical value crossed into the actuated state\n | \"released\"\n | \"valueChanged\" // analog 0..1 (dead-banded)\n | \"grabStart\"\n | \"grabEnd\"\n | \"scored\" // toss passed through a hoop\n | \"behaviourEnabled\"\n | \"behaviourDisabled\"\n | \"interactableEnabled\"\n | \"interactableDisabled\";\n\nexport interface InteractionEvent {\n type: InteractionEventType;\n /** The target interactable. */\n interactableId: string;\n /** The behaviour that produced the event, when behaviour-scoped. */\n behaviourKind?: string;\n /** The input source/interactor involved, when input-scoped. */\n interactorId?: string;\n /** Analog payload (valueChanged, dwellProgress) or 1/0 for digital. */\n value?: number;\n /** Why a behaviour was disabled (unmet capability requirements). */\n reason?: string;\n}\n\nexport type InteractionEventListener = (event: InteractionEvent) => void;\n\n/** Minimal synchronous emitter (insertion-ordered, unsubscribe-safe). */\nexport class Emitter<T> {\n private listeners = new Set<(value: T) => void>();\n\n subscribe(listener: (value: T) => void): Unsubscribe {\n this.listeners.add(listener);\n return () => this.listeners.delete(listener);\n }\n\n emit(value: T): void {\n for (const listener of [...this.listeners]) listener(value);\n }\n\n get size(): number {\n return this.listeners.size;\n }\n}\n"]}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feedback hooks - haptics/audio stay OUT of the framework; the client
|
|
3
|
+
* implements them. Behaviours emit *intents* describing the physical
|
|
4
|
+
* moment ("this press just actuated, a short strong pulse would fit");
|
|
5
|
+
* the client subscribes and realises them however it likes (haptic pulse
|
|
6
|
+
* on controllers, an audio cue, both, neither).
|
|
7
|
+
*
|
|
8
|
+
* `routeHapticsToProvider` is the one convenience the framework offers -
|
|
9
|
+
* an explicit opt-in the client calls, never an automatic behaviour.
|
|
10
|
+
*/
|
|
11
|
+
import type { InputProvider, Unsubscribe } from "@realitycollective/webxr-input";
|
|
12
|
+
/** A named moment a client may want to sonify/hapticise. */
|
|
13
|
+
export type FeedbackCue = "hover" | "press" | "actuate" | "release" | "grab" | "drop" | "score" | "dwellComplete" | "valueTick";
|
|
14
|
+
export interface FeedbackIntent {
|
|
15
|
+
cue: FeedbackCue;
|
|
16
|
+
interactableId: string;
|
|
17
|
+
behaviourKind?: string;
|
|
18
|
+
/** The input source that caused the moment - haptics target this. */
|
|
19
|
+
sourceId?: string;
|
|
20
|
+
/** Suggested haptic intensity 0..1 (clients may remap freely). */
|
|
21
|
+
intensity: number;
|
|
22
|
+
/** Suggested haptic duration in ms. */
|
|
23
|
+
durationMs: number;
|
|
24
|
+
}
|
|
25
|
+
export type FeedbackListener = (intent: FeedbackIntent) => void;
|
|
26
|
+
/**
|
|
27
|
+
* Client opt-in: forward the haptic half of feedback intents to the input
|
|
28
|
+
* provider's `pulse` (which no-ops on sources without actuators - hands).
|
|
29
|
+
* Returns the unsubscribe. Audio remains entirely the client's concern.
|
|
30
|
+
*/
|
|
31
|
+
export declare function routeHapticsToProvider(onFeedback: (listener: FeedbackListener) => Unsubscribe, provider: InputProvider, scale?: number): Unsubscribe;
|