@realitycollective/webxr-interactions 0.1.0 → 0.1.1-preview.1
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 +24 -0
- package/README.md +4 -0
- package/dist/behaviours/behaviour.d.ts +10 -1
- package/dist/behaviours/behaviour.js.map +1 -1
- package/dist/behaviours/dial.js +1 -1
- package/dist/behaviours/dial.js.map +1 -1
- package/dist/behaviours/grab.js +19 -6
- package/dist/behaviours/grab.js.map +1 -1
- package/dist/behaviours/hinge.js +1 -1
- package/dist/behaviours/hinge.js.map +1 -1
- package/dist/behaviours/slide.js +1 -1
- package/dist/behaviours/slide.js.map +1 -1
- package/dist/contract-cases.d.ts +88 -0
- package/dist/contract-cases.js +339 -0
- package/dist/contract-cases.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/ports.d.ts +61 -2
- package/dist/ports.js.map +1 -1
- package/dist/runtime.d.ts +2 -0
- package/dist/runtime.js +40 -3
- package/dist/runtime.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import { quatMultiply, vAdd, vApplyQuat } from "./math.js";
|
|
2
|
+
/**
|
|
3
|
+
* The shared `HitTester` conformance suite. An adapter's test file is a loop:
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* for (const contractCase of hitTesterContractCases()) {
|
|
7
|
+
* it(contractCase.name, () => contractCase.run(makeSubject()));
|
|
8
|
+
* }
|
|
9
|
+
* ```
|
|
10
|
+
*/
|
|
11
|
+
export function hitTesterContractCases() {
|
|
12
|
+
return HIT_TESTER_CASES;
|
|
13
|
+
}
|
|
14
|
+
const DOWN_RAY = { origin: [0, 0, 0], direction: [0, 0, -1] };
|
|
15
|
+
const AWAY_RAY = { origin: [0, 0, 0], direction: [0, 0, 1] };
|
|
16
|
+
/**
|
|
17
|
+
* A ray-based query only has to land WITHIN the target's own radius:
|
|
18
|
+
* `ports.ts` documents `point` as "world-space hit or closest point", so a
|
|
19
|
+
* mesh-accurate ray tester (three.js) reporting the surface it actually hit
|
|
20
|
+
* is as conforming as a sphere tester (Babylon, IWSDK) reporting the target's
|
|
21
|
+
* centre. Placing the target exactly on the ray's line makes both answers
|
|
22
|
+
* the same point when a tester has no radius-shaped slack, and this is the
|
|
23
|
+
* tolerance for testers that do.
|
|
24
|
+
*/
|
|
25
|
+
function slack(radius) {
|
|
26
|
+
return radius + 0.05;
|
|
27
|
+
}
|
|
28
|
+
const HIT_TESTER_CASES = [
|
|
29
|
+
{
|
|
30
|
+
name: "an empty scene answers a ray with null",
|
|
31
|
+
run({ hitTester }) {
|
|
32
|
+
assert(hitTester.hitRay(DOWN_RAY) === null, "hitRay over an empty scene must return null");
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: "a ray through a placed target returns its id, a positive distance and a point near the ray",
|
|
37
|
+
run({ hitTester, driver }) {
|
|
38
|
+
const radius = 0.15;
|
|
39
|
+
driver.place("target", [0, 0, -2], radius);
|
|
40
|
+
const hit = hitTester.hitRay(DOWN_RAY);
|
|
41
|
+
assert(hit !== null, "a ray through a placed target must not miss");
|
|
42
|
+
assert(hit.interactableId === "target", `expected interactableId "target", got "${hit.interactableId}"`);
|
|
43
|
+
assert(hit.distance > 0, `distance must be positive, got ${hit.distance}`);
|
|
44
|
+
const eps = slack(radius);
|
|
45
|
+
assert(closeTo(hit.distance, 2, eps), `distance must be close to 2, got ${hit.distance}`);
|
|
46
|
+
assertVec3Close(hit.point, [0, 0, -2], eps, "point");
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: "a ray pointing away from the target returns null",
|
|
51
|
+
run({ hitTester, driver }) {
|
|
52
|
+
driver.place("target", [0, 0, -2], 0.15);
|
|
53
|
+
assert(hitTester.hitRay(AWAY_RAY) === null, "a ray pointing away from every target must return null");
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: "hitProximity finds a target inside its radius and misses outside it",
|
|
58
|
+
run({ hitTester, driver }) {
|
|
59
|
+
driver.place("target", [0, 0, -2], 0.1);
|
|
60
|
+
const inside = hitTester.hitProximity([0, 0, -2], 0.5);
|
|
61
|
+
assert(inside !== null, "hitProximity at the target's own position must find it");
|
|
62
|
+
assert(inside.interactableId === "target", `expected interactableId "target", got "${inside.interactableId}"`);
|
|
63
|
+
const outside = hitTester.hitProximity([5, 5, 5], 0.05);
|
|
64
|
+
assert(outside === null, "hitProximity far from every target must return null");
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: "with two targets on one ray, the nearer one wins",
|
|
69
|
+
run({ hitTester, driver }) {
|
|
70
|
+
const radius = 0.1;
|
|
71
|
+
driver.place("far", [0, 0, -3], radius);
|
|
72
|
+
driver.place("near", [0, 0, -1], radius);
|
|
73
|
+
const hit = hitTester.hitRay(DOWN_RAY);
|
|
74
|
+
assert(hit !== null, "a ray through two targets must not miss");
|
|
75
|
+
assert(hit.interactableId === "near", `the nearer target must win, got "${hit.interactableId}"`);
|
|
76
|
+
assert(closeTo(hit.distance, 1, slack(radius)), `distance must be close to 1, got ${hit.distance}`);
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: "each hit and its point are fresh values the caller owns",
|
|
81
|
+
run({ hitTester, driver }) {
|
|
82
|
+
driver.place("target", [0, 0, -2], 0.15);
|
|
83
|
+
for (const [label, query] of [
|
|
84
|
+
["hitRay", () => hitTester.hitRay(DOWN_RAY)],
|
|
85
|
+
["hitProximity", () => hitTester.hitProximity([0, 0, -2], 0.5)],
|
|
86
|
+
]) {
|
|
87
|
+
const first = query();
|
|
88
|
+
const second = query();
|
|
89
|
+
assert(first !== null && second !== null, `${label} must find the placed target`);
|
|
90
|
+
assert(second !== first && second.point !== first.point, `${label} must return a new hit and point each call`);
|
|
91
|
+
const expected = first.point[2];
|
|
92
|
+
first.point[2] = 1e6;
|
|
93
|
+
const third = query();
|
|
94
|
+
assert(third !== null && closeTo(third.point[2], expected, POSE_EPS), `writing to a returned point must not change the next ${label} answer`);
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
];
|
|
99
|
+
/**
|
|
100
|
+
* The shared `TransformPort` conformance suite. An adapter's test file is a
|
|
101
|
+
* loop, the same shape as {@link hitTesterContractCases}.
|
|
102
|
+
*/
|
|
103
|
+
export function transformPortContractCases() {
|
|
104
|
+
return TRANSFORM_PORT_CASES;
|
|
105
|
+
}
|
|
106
|
+
const POSE_EPS = 1e-3;
|
|
107
|
+
// A quarter turn about Y - a non-identity unit quaternion, so the case
|
|
108
|
+
// cannot pass by accident on a port that ignores its argument.
|
|
109
|
+
const QUARTER_TURN_Y = [0, Math.SQRT1_2, 0, Math.SQRT1_2];
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
// Held / released / reset - the "held pose" rule, checked only when the
|
|
112
|
+
// subject carries a physics driver. One fixed step size and a looser
|
|
113
|
+
// position tolerance than POSE_EPS: real engines differ slightly in their
|
|
114
|
+
// integrator, and a held/reset case allows for up to one step of gravity
|
|
115
|
+
// (see PHYSICS_POSE_EPS's own comment), where the plain pose cases above
|
|
116
|
+
// never touch physics at all.
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
const PHYSICS_DT = 1 / 60;
|
|
119
|
+
const HELD_STEPS = 5;
|
|
120
|
+
/** metres/second - small enough that one step's drift is easy to read, large enough not to vanish into PHYSICS_POSE_EPS. */
|
|
121
|
+
const RELEASE_SPEED = 2;
|
|
122
|
+
/** metres/second - about 25x RELEASE_SPEED, so "kept the old velocity" and "one step of gravity" cannot be confused. */
|
|
123
|
+
const LARGE_PRIOR_SPEED = 50;
|
|
124
|
+
/**
|
|
125
|
+
* 2 cm. One step of gravity at PHYSICS_DT droops roughly
|
|
126
|
+
* `0.5 * 9.8 * PHYSICS_DT^2` ≈ 1.4 mm, well inside this; a full step of
|
|
127
|
+
* LARGE_PRIOR_SPEED (≈ 0.83 m) or RELEASE_SPEED (≈ 33 mm) is well outside it.
|
|
128
|
+
*/
|
|
129
|
+
const PHYSICS_POSE_EPS = 0.02;
|
|
130
|
+
function assertPhysicsPoseClose(value, expected, label) {
|
|
131
|
+
assertVec3Close(value.position, expected.position, PHYSICS_POSE_EPS, `${label}.position`);
|
|
132
|
+
assertQuatClose(value.quaternion, expected.quaternion, `${label}.quaternion`);
|
|
133
|
+
}
|
|
134
|
+
const TRANSFORM_PORT_CASES = [
|
|
135
|
+
{
|
|
136
|
+
name: "setLocalOffset then getLocalOffset returns the same offset",
|
|
137
|
+
run({ port }) {
|
|
138
|
+
const offset = [0.3, -0.1, 0.05];
|
|
139
|
+
port.setLocalOffset(offset);
|
|
140
|
+
assertVec3Close(port.getLocalOffset(), offset, POSE_EPS, "getLocalOffset()");
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
name: "getWorldPose returns finite numbers and a unit quaternion",
|
|
145
|
+
run({ port }) {
|
|
146
|
+
const pose = port.getWorldPose();
|
|
147
|
+
for (const value of [...pose.position, ...pose.quaternion]) {
|
|
148
|
+
assert(Number.isFinite(value), `getWorldPose() must return finite numbers, got ${value}`);
|
|
149
|
+
}
|
|
150
|
+
const length = quatLength(pose.quaternion);
|
|
151
|
+
assert(closeTo(length, 1, POSE_EPS), `getWorldPose().quaternion must be a unit quaternion, length was ${length}`);
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
name: "setLocalRotation with a unit quaternion does not throw",
|
|
156
|
+
run({ port }) {
|
|
157
|
+
try {
|
|
158
|
+
port.setLocalRotation(QUARTER_TURN_Y);
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
throw new Error(`setLocalRotation(unit quaternion) must not throw, it threw: ${String(error)}`);
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
name: "getRestWorldPose returns the pose captured at registration",
|
|
167
|
+
run({ port, rest }) {
|
|
168
|
+
assertPoseClose(port.getRestWorldPose(), rest, "getRestWorldPose()");
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
name: "before any write, the live pose is the rest pose",
|
|
173
|
+
run({ port, rest }) {
|
|
174
|
+
assertPoseClose(port.getWorldPose(), rest, "getWorldPose()");
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
name: "getWorldPose follows setLocalOffset while getRestWorldPose stays put",
|
|
179
|
+
run({ port, rest }) {
|
|
180
|
+
const offset = [0.3, -0.1, 0.05];
|
|
181
|
+
port.setLocalOffset(offset);
|
|
182
|
+
assertVec3Close(port.getWorldPose().position, vAdd(rest.position, vApplyQuat(offset, rest.quaternion)), POSE_EPS, "getWorldPose().position after setLocalOffset");
|
|
183
|
+
assertPoseClose(port.getRestWorldPose(), rest, "getRestWorldPose() after setLocalOffset");
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
name: "getWorldPose follows setLocalRotation while getRestWorldPose stays put",
|
|
188
|
+
run({ port, rest }) {
|
|
189
|
+
port.setLocalRotation(QUARTER_TURN_Y);
|
|
190
|
+
assertQuatClose(port.getWorldPose().quaternion, quatMultiply(rest.quaternion, QUARTER_TURN_Y), "getWorldPose().quaternion after setLocalRotation");
|
|
191
|
+
assertPoseClose(port.getRestWorldPose(), rest, "getRestWorldPose() after setLocalRotation");
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
name: "setWorldPose, when present, is what getWorldPose reads back",
|
|
196
|
+
run({ port, rest }) {
|
|
197
|
+
if (!port.setWorldPose)
|
|
198
|
+
return;
|
|
199
|
+
const target = { position: [0.5, 1.2, -0.4], quaternion: QUARTER_TURN_Y };
|
|
200
|
+
port.setWorldPose(target);
|
|
201
|
+
assertPoseClose(port.getWorldPose(), target, "getWorldPose() after setWorldPose");
|
|
202
|
+
assertPoseClose(port.getRestWorldPose(), rest, "getRestWorldPose() after setWorldPose");
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
name: "every returned tuple is a fresh value the caller owns",
|
|
207
|
+
run({ port }) {
|
|
208
|
+
const pose = port.getWorldPose();
|
|
209
|
+
const restPose = port.getRestWorldPose();
|
|
210
|
+
const offset = port.getLocalOffset();
|
|
211
|
+
pose.position[0] = 1e6;
|
|
212
|
+
pose.quaternion[3] = 1e6;
|
|
213
|
+
restPose.position[0] = 1e6;
|
|
214
|
+
offset[0] = 1e6;
|
|
215
|
+
assert(port.getWorldPose().position[0] !== 1e6, "writing to a returned getWorldPose() tuple must not change the port");
|
|
216
|
+
assert(port.getWorldPose().quaternion[3] !== 1e6, "writing to a returned getWorldPose() quaternion must not change the port");
|
|
217
|
+
assert(port.getRestWorldPose().position[0] !== 1e6, "writing to a returned getRestWorldPose() tuple must not change the port");
|
|
218
|
+
assert(port.getLocalOffset()[0] !== 1e6, "writing to a returned getLocalOffset() tuple must not change the port");
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
name: "a tuple passed in is not kept",
|
|
223
|
+
run({ port }) {
|
|
224
|
+
const offset = [0.3, -0.1, 0.05];
|
|
225
|
+
port.setLocalOffset(offset);
|
|
226
|
+
offset[0] = 9;
|
|
227
|
+
assertVec3Close(port.getLocalOffset(), [0.3, -0.1, 0.05], POSE_EPS, "getLocalOffset() after the caller reused its tuple");
|
|
228
|
+
if (!port.setWorldPose)
|
|
229
|
+
return;
|
|
230
|
+
const target = { position: [0.5, 1.2, -0.4], quaternion: [0, 0, 0, 1] };
|
|
231
|
+
port.setWorldPose(target);
|
|
232
|
+
target.position[0] = 9;
|
|
233
|
+
assertVec3Close(port.getWorldPose().position, [0.5, 1.2, -0.4], POSE_EPS, "getWorldPose() after the caller reused its tuple");
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
name: "setEffect, when present, accepts scale and emissive without throwing",
|
|
238
|
+
run({ port }) {
|
|
239
|
+
if (!port.setEffect)
|
|
240
|
+
return;
|
|
241
|
+
try {
|
|
242
|
+
port.setEffect({ scale: 1.2, emissive: 0.3 });
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
throw new Error(`setEffect({ scale, emissive }) must not throw, it threw: ${String(error)}`);
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
name: "Held: beginHold holds a fixed setWorldPose without falling, when the subject has physics",
|
|
251
|
+
run({ port, rest, physics }) {
|
|
252
|
+
if (!physics || !port.setWorldPose || !port.beginHold)
|
|
253
|
+
return;
|
|
254
|
+
const held = {
|
|
255
|
+
position: [rest.position[0], rest.position[1] + 1, rest.position[2]],
|
|
256
|
+
quaternion: rest.quaternion,
|
|
257
|
+
};
|
|
258
|
+
port.beginHold();
|
|
259
|
+
for (let i = 0; i < HELD_STEPS; i++) {
|
|
260
|
+
port.setWorldPose(held);
|
|
261
|
+
physics.step(PHYSICS_DT);
|
|
262
|
+
}
|
|
263
|
+
// A tight tolerance, not PHYSICS_POSE_EPS: a genuinely held body sees
|
|
264
|
+
// no simulation at all, so it matches the written pose exactly bar
|
|
265
|
+
// floating point noise - the same POSE_EPS the non-physics cases
|
|
266
|
+
// above use. One step of unwanted gravity (~2.7 mm at PHYSICS_DT) is
|
|
267
|
+
// what this case exists to catch, and it must not hide inside the
|
|
268
|
+
// tolerance the way it deliberately may in the Reset case below.
|
|
269
|
+
assertPoseClose(port.getWorldPose(), held, `getWorldPose() after ${HELD_STEPS} held steps`);
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
name: "Released: endHold sends the body away with the release velocity, when the subject has physics",
|
|
274
|
+
run({ port, rest, physics }) {
|
|
275
|
+
if (!physics || !port.beginHold || !port.endHold)
|
|
276
|
+
return;
|
|
277
|
+
port.beginHold();
|
|
278
|
+
const release = { linearVelocity: [RELEASE_SPEED, 0, 0], angularVelocity: [0, 0, 0] };
|
|
279
|
+
port.endHold(release);
|
|
280
|
+
physics.step(PHYSICS_DT);
|
|
281
|
+
const moved = port.getWorldPose().position[0] - rest.position[0];
|
|
282
|
+
assert(moved > RELEASE_SPEED * PHYSICS_DT * 0.5, `endHold({ linearVelocity: [${RELEASE_SPEED}, 0, 0] }) must move the body that way within one step, moved ${moved}`);
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
name: "Reset: setWorldPose while not held teleports to the pose and clears velocity, when the subject has physics",
|
|
287
|
+
run({ port, rest, physics }) {
|
|
288
|
+
if (!physics || !port.setWorldPose || !port.beginHold || !port.endHold)
|
|
289
|
+
return;
|
|
290
|
+
// Give the body a large prior velocity and let it run for a step, so
|
|
291
|
+
// carrying that velocity through the reset below is unmistakable -
|
|
292
|
+
// one step of it (~0.83 m) dwarfs PHYSICS_POSE_EPS (2 cm).
|
|
293
|
+
port.beginHold();
|
|
294
|
+
const priorRelease = { linearVelocity: [LARGE_PRIOR_SPEED, 0, 0], angularVelocity: [0, 0, 0] };
|
|
295
|
+
port.endHold(priorRelease);
|
|
296
|
+
physics.step(PHYSICS_DT);
|
|
297
|
+
const target = {
|
|
298
|
+
position: [rest.position[0] - 1, rest.position[1] + 2, rest.position[2] + 0.5],
|
|
299
|
+
quaternion: rest.quaternion,
|
|
300
|
+
};
|
|
301
|
+
port.setWorldPose(target);
|
|
302
|
+
// One further step: it must be at `target`, not carrying the prior
|
|
303
|
+
// velocity forward - allowing for this one step's worth of gravity.
|
|
304
|
+
physics.step(PHYSICS_DT);
|
|
305
|
+
assertPhysicsPoseClose(port.getWorldPose(), target, "getWorldPose() one step after a reset setWorldPose");
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
];
|
|
309
|
+
// ---------------------------------------------------------------------------
|
|
310
|
+
// Shared assertion helpers
|
|
311
|
+
// ---------------------------------------------------------------------------
|
|
312
|
+
function assert(condition, message) {
|
|
313
|
+
if (!condition)
|
|
314
|
+
throw new Error(message);
|
|
315
|
+
}
|
|
316
|
+
function closeTo(a, b, eps) {
|
|
317
|
+
return Math.abs(a - b) <= eps;
|
|
318
|
+
}
|
|
319
|
+
// Indexed one component at a time, rather than in a loop, so each index is a
|
|
320
|
+
// literal: `noUncheckedIndexedAccess` cannot narrow a tuple read through a
|
|
321
|
+
// variable index back to `number`.
|
|
322
|
+
function assertVec3Close(value, expected, eps, label) {
|
|
323
|
+
assert(closeTo(value[0], expected[0], eps), `${label}[0] must be close to ${expected[0]}, got ${value[0]}`);
|
|
324
|
+
assert(closeTo(value[1], expected[1], eps), `${label}[1] must be close to ${expected[1]}, got ${value[1]}`);
|
|
325
|
+
assert(closeTo(value[2], expected[2], eps), `${label}[2] must be close to ${expected[2]}, got ${value[2]}`);
|
|
326
|
+
}
|
|
327
|
+
// q and -q are the same rotation, so compare by the size of the dot product.
|
|
328
|
+
function assertQuatClose(value, expected, label) {
|
|
329
|
+
const dot = value[0] * expected[0] + value[1] * expected[1] + value[2] * expected[2] + value[3] * expected[3];
|
|
330
|
+
assert(closeTo(Math.abs(dot), 1, POSE_EPS), `${label} must be close to [${expected.join(", ")}], got [${value.join(", ")}]`);
|
|
331
|
+
}
|
|
332
|
+
function assertPoseClose(value, expected, label) {
|
|
333
|
+
assertVec3Close(value.position, expected.position, POSE_EPS, `${label}.position`);
|
|
334
|
+
assertQuatClose(value.quaternion, expected.quaternion, `${label}.quaternion`);
|
|
335
|
+
}
|
|
336
|
+
function quatLength(q) {
|
|
337
|
+
return Math.sqrt(q[0] * q[0] + q[1] * q[1] + q[2] * q[2] + q[3] * q[3]);
|
|
338
|
+
}
|
|
339
|
+
//# sourceMappingURL=contract-cases.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract-cases.js","sourceRoot":"","sources":["../src/contract-cases.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Shared `HitTester` and `TransformPort` conformance suites, shipped as data\n * rather than as tests - the same idea as `windowHostContractCases()` in\n * WebXR-UIExtensions and `inputProviderContractCases()` in\n * `@realitycollective/webxr-input`. Five adapters implement these two ports\n * against five different scene graphs; running the same checks against every\n * one is what stops an adapter quietly dropping a documented rule.\n *\n * The suites are runner-free on purpose. Every adapter repository already has\n * its own test runner, and an adapter written outside this repository cannot\n * reach into this one's `test/` folder, so the checks ship as plain objects\n * that throw an `Error` on failure and the adapter iterates them.\n *\n * Both suites assert only what `ports.ts` documents, including two rules\n * every platform now shares:\n * - `getWorldPose()` is the LIVE pose and `getRestWorldPose()` the rest pose\n * offsets are measured from.\n * - Every tuple a port returns is fresh and owned by the caller, and a tuple\n * passed in is not kept.\n */\nimport type { PoseTuple, QuatTuple, RayTuple, Vec3Tuple } from \"@realitycollective/webxr-input\";\nimport { quatMultiply, vAdd, vApplyQuat } from \"./math.js\";\nimport type { HitTester, HoldRelease, TransformPort } from \"./ports.js\";\n\n// ---------------------------------------------------------------------------\n// HitTester\n// ---------------------------------------------------------------------------\n\n/**\n * Places (or replaces) one target for a case to query against. Build a FRESH\n * subject per case: cases place targets of their own and do not clean up.\n */\nexport interface HitTesterContractDriver {\n /**\n * Register a target at `position` with an interaction radius of `radius`,\n * however the platform registers a target - a mesh, a node, an entity.\n */\n place(id: string, position: Vec3Tuple, radius: number): void;\n}\n\n/** The tester under test, plus the means of putting targets into its scene. */\nexport interface HitTesterContractSubject {\n hitTester: HitTester;\n driver: HitTesterContractDriver;\n}\n\n/** One check a {@link HitTester} implementation must pass. */\nexport interface HitTesterContractCase {\n name: string;\n run(subject: HitTesterContractSubject): void;\n}\n\n/**\n * The shared `HitTester` conformance suite. An adapter's test file is a loop:\n *\n * ```ts\n * for (const contractCase of hitTesterContractCases()) {\n * it(contractCase.name, () => contractCase.run(makeSubject()));\n * }\n * ```\n */\nexport function hitTesterContractCases(): readonly HitTesterContractCase[] {\n return HIT_TESTER_CASES;\n}\n\nconst DOWN_RAY: RayTuple = { origin: [0, 0, 0], direction: [0, 0, -1] };\nconst AWAY_RAY: RayTuple = { origin: [0, 0, 0], direction: [0, 0, 1] };\n\n/**\n * A ray-based query only has to land WITHIN the target's own radius:\n * `ports.ts` documents `point` as \"world-space hit or closest point\", so a\n * mesh-accurate ray tester (three.js) reporting the surface it actually hit\n * is as conforming as a sphere tester (Babylon, IWSDK) reporting the target's\n * centre. Placing the target exactly on the ray's line makes both answers\n * the same point when a tester has no radius-shaped slack, and this is the\n * tolerance for testers that do.\n */\nfunction slack(radius: number): number {\n return radius + 0.05;\n}\n\nconst HIT_TESTER_CASES: readonly HitTesterContractCase[] = [\n {\n name: \"an empty scene answers a ray with null\",\n run({ hitTester }) {\n assert(hitTester.hitRay(DOWN_RAY) === null, \"hitRay over an empty scene must return null\");\n },\n },\n {\n name: \"a ray through a placed target returns its id, a positive distance and a point near the ray\",\n run({ hitTester, driver }) {\n const radius = 0.15;\n driver.place(\"target\", [0, 0, -2], radius);\n const hit = hitTester.hitRay(DOWN_RAY);\n assert(hit !== null, \"a ray through a placed target must not miss\");\n assert(hit.interactableId === \"target\", `expected interactableId \"target\", got \"${hit.interactableId}\"`);\n assert(hit.distance > 0, `distance must be positive, got ${hit.distance}`);\n const eps = slack(radius);\n assert(closeTo(hit.distance, 2, eps), `distance must be close to 2, got ${hit.distance}`);\n assertVec3Close(hit.point, [0, 0, -2], eps, \"point\");\n },\n },\n {\n name: \"a ray pointing away from the target returns null\",\n run({ hitTester, driver }) {\n driver.place(\"target\", [0, 0, -2], 0.15);\n assert(hitTester.hitRay(AWAY_RAY) === null, \"a ray pointing away from every target must return null\");\n },\n },\n {\n name: \"hitProximity finds a target inside its radius and misses outside it\",\n run({ hitTester, driver }) {\n driver.place(\"target\", [0, 0, -2], 0.1);\n const inside = hitTester.hitProximity([0, 0, -2], 0.5);\n assert(inside !== null, \"hitProximity at the target's own position must find it\");\n assert(\n inside.interactableId === \"target\",\n `expected interactableId \"target\", got \"${inside.interactableId}\"`,\n );\n const outside = hitTester.hitProximity([5, 5, 5], 0.05);\n assert(outside === null, \"hitProximity far from every target must return null\");\n },\n },\n {\n name: \"with two targets on one ray, the nearer one wins\",\n run({ hitTester, driver }) {\n const radius = 0.1;\n driver.place(\"far\", [0, 0, -3], radius);\n driver.place(\"near\", [0, 0, -1], radius);\n const hit = hitTester.hitRay(DOWN_RAY);\n assert(hit !== null, \"a ray through two targets must not miss\");\n assert(hit.interactableId === \"near\", `the nearer target must win, got \"${hit.interactableId}\"`);\n assert(closeTo(hit.distance, 1, slack(radius)), `distance must be close to 1, got ${hit.distance}`);\n },\n },\n {\n name: \"each hit and its point are fresh values the caller owns\",\n run({ hitTester, driver }) {\n driver.place(\"target\", [0, 0, -2], 0.15);\n for (const [label, query] of [\n [\"hitRay\", () => hitTester.hitRay(DOWN_RAY)],\n [\"hitProximity\", () => hitTester.hitProximity([0, 0, -2], 0.5)],\n ] as const) {\n const first = query();\n const second = query();\n assert(first !== null && second !== null, `${label} must find the placed target`);\n assert(second !== first && second.point !== first.point, `${label} must return a new hit and point each call`);\n const expected = first.point[2];\n first.point[2] = 1e6;\n const third = query();\n assert(\n third !== null && closeTo(third.point[2], expected, POSE_EPS),\n `writing to a returned point must not change the next ${label} answer`,\n );\n }\n },\n },\n];\n\n// ---------------------------------------------------------------------------\n// TransformPort\n// ---------------------------------------------------------------------------\n\n/**\n * Advances a subject's physics simulation - a real engine step where a\n * platform's physics can run in a test, otherwise a fake that faithfully\n * reproduces the one behaviour these cases check: gravity moves the object\n * unless held, and a release velocity carries it.\n */\nexport interface TransformPortPhysicsDriver {\n step(dtSeconds: number): void;\n}\n\n/** The port under test, over one object. Build a FRESH one per case. */\nexport interface TransformPortContractSubject {\n port: TransformPort;\n /**\n * The world pose the object sat at when the port captured its rest. Place\n * it away from the origin and turned, under no parent or an unscaled one,\n * so a port that ignores its rest frame cannot pass by accident.\n */\n rest: PoseTuple;\n /**\n * Present only when the object has a physics body - the same condition\n * that gives the port `beginHold`/`endHold`. The held/released/reset\n * cases below skip without one, since there is nothing for them to check.\n */\n physics?: TransformPortPhysicsDriver;\n}\n\n/** One check a {@link TransformPort} implementation must pass. */\nexport interface TransformPortContractCase {\n name: string;\n run(subject: TransformPortContractSubject): void;\n}\n\n/**\n * The shared `TransformPort` conformance suite. An adapter's test file is a\n * loop, the same shape as {@link hitTesterContractCases}.\n */\nexport function transformPortContractCases(): readonly TransformPortContractCase[] {\n return TRANSFORM_PORT_CASES;\n}\n\nconst POSE_EPS = 1e-3;\n// A quarter turn about Y - a non-identity unit quaternion, so the case\n// cannot pass by accident on a port that ignores its argument.\nconst QUARTER_TURN_Y: QuatTuple = [0, Math.SQRT1_2, 0, Math.SQRT1_2];\n\n// ---------------------------------------------------------------------------\n// Held / released / reset - the \"held pose\" rule, checked only when the\n// subject carries a physics driver. One fixed step size and a looser\n// position tolerance than POSE_EPS: real engines differ slightly in their\n// integrator, and a held/reset case allows for up to one step of gravity\n// (see PHYSICS_POSE_EPS's own comment), where the plain pose cases above\n// never touch physics at all.\n// ---------------------------------------------------------------------------\n\nconst PHYSICS_DT = 1 / 60;\nconst HELD_STEPS = 5;\n/** metres/second - small enough that one step's drift is easy to read, large enough not to vanish into PHYSICS_POSE_EPS. */\nconst RELEASE_SPEED = 2;\n/** metres/second - about 25x RELEASE_SPEED, so \"kept the old velocity\" and \"one step of gravity\" cannot be confused. */\nconst LARGE_PRIOR_SPEED = 50;\n/**\n * 2 cm. One step of gravity at PHYSICS_DT droops roughly\n * `0.5 * 9.8 * PHYSICS_DT^2` ≈ 1.4 mm, well inside this; a full step of\n * LARGE_PRIOR_SPEED (≈ 0.83 m) or RELEASE_SPEED (≈ 33 mm) is well outside it.\n */\nconst PHYSICS_POSE_EPS = 0.02;\n\nfunction assertPhysicsPoseClose(value: PoseTuple, expected: PoseTuple, label: string): void {\n assertVec3Close(value.position, expected.position, PHYSICS_POSE_EPS, `${label}.position`);\n assertQuatClose(value.quaternion, expected.quaternion, `${label}.quaternion`);\n}\n\nconst TRANSFORM_PORT_CASES: readonly TransformPortContractCase[] = [\n {\n name: \"setLocalOffset then getLocalOffset returns the same offset\",\n run({ port }) {\n const offset: Vec3Tuple = [0.3, -0.1, 0.05];\n port.setLocalOffset(offset);\n assertVec3Close(port.getLocalOffset(), offset, POSE_EPS, \"getLocalOffset()\");\n },\n },\n {\n name: \"getWorldPose returns finite numbers and a unit quaternion\",\n run({ port }) {\n const pose = port.getWorldPose();\n for (const value of [...pose.position, ...pose.quaternion]) {\n assert(Number.isFinite(value), `getWorldPose() must return finite numbers, got ${value}`);\n }\n const length = quatLength(pose.quaternion);\n assert(\n closeTo(length, 1, POSE_EPS),\n `getWorldPose().quaternion must be a unit quaternion, length was ${length}`,\n );\n },\n },\n {\n name: \"setLocalRotation with a unit quaternion does not throw\",\n run({ port }) {\n try {\n port.setLocalRotation(QUARTER_TURN_Y);\n } catch (error) {\n throw new Error(`setLocalRotation(unit quaternion) must not throw, it threw: ${String(error)}`);\n }\n },\n },\n {\n name: \"getRestWorldPose returns the pose captured at registration\",\n run({ port, rest }) {\n assertPoseClose(port.getRestWorldPose(), rest, \"getRestWorldPose()\");\n },\n },\n {\n name: \"before any write, the live pose is the rest pose\",\n run({ port, rest }) {\n assertPoseClose(port.getWorldPose(), rest, \"getWorldPose()\");\n },\n },\n {\n name: \"getWorldPose follows setLocalOffset while getRestWorldPose stays put\",\n run({ port, rest }) {\n const offset: Vec3Tuple = [0.3, -0.1, 0.05];\n port.setLocalOffset(offset);\n assertVec3Close(\n port.getWorldPose().position,\n vAdd(rest.position, vApplyQuat(offset, rest.quaternion)),\n POSE_EPS,\n \"getWorldPose().position after setLocalOffset\",\n );\n assertPoseClose(port.getRestWorldPose(), rest, \"getRestWorldPose() after setLocalOffset\");\n },\n },\n {\n name: \"getWorldPose follows setLocalRotation while getRestWorldPose stays put\",\n run({ port, rest }) {\n port.setLocalRotation(QUARTER_TURN_Y);\n assertQuatClose(\n port.getWorldPose().quaternion,\n quatMultiply(rest.quaternion, QUARTER_TURN_Y),\n \"getWorldPose().quaternion after setLocalRotation\",\n );\n assertPoseClose(port.getRestWorldPose(), rest, \"getRestWorldPose() after setLocalRotation\");\n },\n },\n {\n name: \"setWorldPose, when present, is what getWorldPose reads back\",\n run({ port, rest }) {\n if (!port.setWorldPose) return;\n const target: PoseTuple = { position: [0.5, 1.2, -0.4], quaternion: QUARTER_TURN_Y };\n port.setWorldPose(target);\n assertPoseClose(port.getWorldPose(), target, \"getWorldPose() after setWorldPose\");\n assertPoseClose(port.getRestWorldPose(), rest, \"getRestWorldPose() after setWorldPose\");\n },\n },\n {\n name: \"every returned tuple is a fresh value the caller owns\",\n run({ port }) {\n const pose = port.getWorldPose();\n const restPose = port.getRestWorldPose();\n const offset = port.getLocalOffset();\n pose.position[0] = 1e6;\n pose.quaternion[3] = 1e6;\n restPose.position[0] = 1e6;\n offset[0] = 1e6;\n assert(port.getWorldPose().position[0] !== 1e6, \"writing to a returned getWorldPose() tuple must not change the port\");\n assert(port.getWorldPose().quaternion[3] !== 1e6, \"writing to a returned getWorldPose() quaternion must not change the port\");\n assert(port.getRestWorldPose().position[0] !== 1e6, \"writing to a returned getRestWorldPose() tuple must not change the port\");\n assert(port.getLocalOffset()[0] !== 1e6, \"writing to a returned getLocalOffset() tuple must not change the port\");\n },\n },\n {\n name: \"a tuple passed in is not kept\",\n run({ port }) {\n const offset: Vec3Tuple = [0.3, -0.1, 0.05];\n port.setLocalOffset(offset);\n offset[0] = 9;\n assertVec3Close(port.getLocalOffset(), [0.3, -0.1, 0.05], POSE_EPS, \"getLocalOffset() after the caller reused its tuple\");\n if (!port.setWorldPose) return;\n const target: PoseTuple = { position: [0.5, 1.2, -0.4], quaternion: [0, 0, 0, 1] };\n port.setWorldPose(target);\n target.position[0] = 9;\n assertVec3Close(port.getWorldPose().position, [0.5, 1.2, -0.4], POSE_EPS, \"getWorldPose() after the caller reused its tuple\");\n },\n },\n {\n name: \"setEffect, when present, accepts scale and emissive without throwing\",\n run({ port }) {\n if (!port.setEffect) return;\n try {\n port.setEffect({ scale: 1.2, emissive: 0.3 });\n } catch (error) {\n throw new Error(`setEffect({ scale, emissive }) must not throw, it threw: ${String(error)}`);\n }\n },\n },\n {\n name: \"Held: beginHold holds a fixed setWorldPose without falling, when the subject has physics\",\n run({ port, rest, physics }) {\n if (!physics || !port.setWorldPose || !port.beginHold) return;\n const held: PoseTuple = {\n position: [rest.position[0], rest.position[1] + 1, rest.position[2]],\n quaternion: rest.quaternion,\n };\n port.beginHold();\n for (let i = 0; i < HELD_STEPS; i++) {\n port.setWorldPose(held);\n physics.step(PHYSICS_DT);\n }\n // A tight tolerance, not PHYSICS_POSE_EPS: a genuinely held body sees\n // no simulation at all, so it matches the written pose exactly bar\n // floating point noise - the same POSE_EPS the non-physics cases\n // above use. One step of unwanted gravity (~2.7 mm at PHYSICS_DT) is\n // what this case exists to catch, and it must not hide inside the\n // tolerance the way it deliberately may in the Reset case below.\n assertPoseClose(port.getWorldPose(), held, `getWorldPose() after ${HELD_STEPS} held steps`);\n },\n },\n {\n name: \"Released: endHold sends the body away with the release velocity, when the subject has physics\",\n run({ port, rest, physics }) {\n if (!physics || !port.beginHold || !port.endHold) return;\n port.beginHold();\n const release: HoldRelease = { linearVelocity: [RELEASE_SPEED, 0, 0], angularVelocity: [0, 0, 0] };\n port.endHold(release);\n physics.step(PHYSICS_DT);\n const moved = port.getWorldPose().position[0] - rest.position[0];\n assert(\n moved > RELEASE_SPEED * PHYSICS_DT * 0.5,\n `endHold({ linearVelocity: [${RELEASE_SPEED}, 0, 0] }) must move the body that way within one step, moved ${moved}`,\n );\n },\n },\n {\n name: \"Reset: setWorldPose while not held teleports to the pose and clears velocity, when the subject has physics\",\n run({ port, rest, physics }) {\n if (!physics || !port.setWorldPose || !port.beginHold || !port.endHold) return;\n // Give the body a large prior velocity and let it run for a step, so\n // carrying that velocity through the reset below is unmistakable -\n // one step of it (~0.83 m) dwarfs PHYSICS_POSE_EPS (2 cm).\n port.beginHold();\n const priorRelease: HoldRelease = { linearVelocity: [LARGE_PRIOR_SPEED, 0, 0], angularVelocity: [0, 0, 0] };\n port.endHold(priorRelease);\n physics.step(PHYSICS_DT);\n const target: PoseTuple = {\n position: [rest.position[0] - 1, rest.position[1] + 2, rest.position[2] + 0.5],\n quaternion: rest.quaternion,\n };\n port.setWorldPose(target);\n // One further step: it must be at `target`, not carrying the prior\n // velocity forward - allowing for this one step's worth of gravity.\n physics.step(PHYSICS_DT);\n assertPhysicsPoseClose(port.getWorldPose(), target, \"getWorldPose() one step after a reset setWorldPose\");\n },\n },\n];\n\n// ---------------------------------------------------------------------------\n// Shared assertion helpers\n// ---------------------------------------------------------------------------\n\nfunction assert(condition: boolean, message: string): asserts condition {\n if (!condition) throw new Error(message);\n}\n\nfunction closeTo(a: number, b: number, eps: number): boolean {\n return Math.abs(a - b) <= eps;\n}\n\n// Indexed one component at a time, rather than in a loop, so each index is a\n// literal: `noUncheckedIndexedAccess` cannot narrow a tuple read through a\n// variable index back to `number`.\nfunction assertVec3Close(value: Vec3Tuple, expected: Vec3Tuple, eps: number, label: string): void {\n assert(closeTo(value[0], expected[0], eps), `${label}[0] must be close to ${expected[0]}, got ${value[0]}`);\n assert(closeTo(value[1], expected[1], eps), `${label}[1] must be close to ${expected[1]}, got ${value[1]}`);\n assert(closeTo(value[2], expected[2], eps), `${label}[2] must be close to ${expected[2]}, got ${value[2]}`);\n}\n\n// q and -q are the same rotation, so compare by the size of the dot product.\nfunction assertQuatClose(value: QuatTuple, expected: QuatTuple, label: string): void {\n const dot = value[0] * expected[0] + value[1] * expected[1] + value[2] * expected[2] + value[3] * expected[3];\n assert(\n closeTo(Math.abs(dot), 1, POSE_EPS),\n `${label} must be close to [${expected.join(\", \")}], got [${value.join(\", \")}]`,\n );\n}\n\nfunction assertPoseClose(value: PoseTuple, expected: PoseTuple, label: string): void {\n assertVec3Close(value.position, expected.position, POSE_EPS, `${label}.position`);\n assertQuatClose(value.quaternion, expected.quaternion, `${label}.quaternion`);\n}\n\nfunction quatLength(q: QuatTuple): number {\n return Math.sqrt(q[0] * q[0] + q[1] * q[1] + q[2] * q[2] + q[3] * q[3]);\n}\n"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -19,6 +19,7 @@ export * from "./math.js";
|
|
|
19
19
|
export * from "./events.js";
|
|
20
20
|
export * from "./feedback.js";
|
|
21
21
|
export * from "./ports.js";
|
|
22
|
+
export * from "./contract-cases.js";
|
|
22
23
|
export * from "./gaze.js";
|
|
23
24
|
export * from "./descriptor.js";
|
|
24
25
|
export * from "./velocity-tracker.js";
|
package/dist/index.js
CHANGED
|
@@ -19,6 +19,7 @@ export * from "./math.js";
|
|
|
19
19
|
export * from "./events.js";
|
|
20
20
|
export * from "./feedback.js";
|
|
21
21
|
export * from "./ports.js";
|
|
22
|
+
export * from "./contract-cases.js";
|
|
22
23
|
export * from "./gaze.js";
|
|
23
24
|
export * from "./descriptor.js";
|
|
24
25
|
export * from "./velocity-tracker.js";
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,cAAc,gCAAgC,CAAC;AAE/C,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,iBAAiB,CAAC;AAChC,cAAc,uBAAuB,CAAC;AACtC,cAAc,cAAc,CAAC;AAC7B,cAAc,qBAAqB,CAAC;AAEpC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC;AACtC,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,sBAAsB,CAAC;AACrC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,yBAAyB,CAAC","sourcesContent":["/**\n * @realitycollective/webxr-interactions - the engine-free interaction core.\n *\n * Layers (each strictly on the one below):\n * - behaviours (press, pulse, hinge, dial, slide, grab, tossScore) -\n * pure state machines driven by `update(dt)`;\n * - the runtime/binder - targeting, press/grab transitions, gaze gating\n * + dwell, capability negotiation, events, feedback intents;\n * - the shared input contracts, re-exported from\n * `@realitycollective/webxr-input`.\n *\n * Pair with an engine adapter: `@realitycollective/threejs-interactions`\n * (standalone WebXR + three.js), `@realitycollective/iwsdk-interactions`\n * (Meta IWSDK) or `@realitycollective/xrblocks-interactions` (Google XR\n * Blocks, experimental).\n */\nexport * from \"@realitycollective/webxr-input\";\n\nexport * from \"./math.js\";\nexport * from \"./events.js\";\nexport * from \"./feedback.js\";\nexport * from \"./ports.js\";\nexport * from \"./gaze.js\";\nexport * from \"./descriptor.js\";\nexport * from \"./velocity-tracker.js\";\nexport * from \"./runtime.js\";\nexport * from \"./pointer-bridge.js\";\n\nexport * from \"./behaviours/behaviour.js\";\nexport * from \"./behaviours/press.js\";\nexport * from \"./behaviours/pulse.js\";\nexport * from \"./behaviours/hinge.js\";\nexport * from \"./behaviours/dial.js\";\nexport * from \"./behaviours/slide.js\";\nexport * from \"./behaviours/grab.js\";\nexport * from \"./behaviours/toss-score.js\";\nexport * from \"./behaviours/factory.js\";\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,cAAc,gCAAgC,CAAC;AAE/C,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC;AAC3B,cAAc,qBAAqB,CAAC;AACpC,cAAc,WAAW,CAAC;AAC1B,cAAc,iBAAiB,CAAC;AAChC,cAAc,uBAAuB,CAAC;AACtC,cAAc,cAAc,CAAC;AAC7B,cAAc,qBAAqB,CAAC;AAEpC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC;AACtC,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,sBAAsB,CAAC;AACrC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,yBAAyB,CAAC","sourcesContent":["/**\n * @realitycollective/webxr-interactions - the engine-free interaction core.\n *\n * Layers (each strictly on the one below):\n * - behaviours (press, pulse, hinge, dial, slide, grab, tossScore) -\n * pure state machines driven by `update(dt)`;\n * - the runtime/binder - targeting, press/grab transitions, gaze gating\n * + dwell, capability negotiation, events, feedback intents;\n * - the shared input contracts, re-exported from\n * `@realitycollective/webxr-input`.\n *\n * Pair with an engine adapter: `@realitycollective/threejs-interactions`\n * (standalone WebXR + three.js), `@realitycollective/iwsdk-interactions`\n * (Meta IWSDK) or `@realitycollective/xrblocks-interactions` (Google XR\n * Blocks, experimental).\n */\nexport * from \"@realitycollective/webxr-input\";\n\nexport * from \"./math.js\";\nexport * from \"./events.js\";\nexport * from \"./feedback.js\";\nexport * from \"./ports.js\";\nexport * from \"./contract-cases.js\";\nexport * from \"./gaze.js\";\nexport * from \"./descriptor.js\";\nexport * from \"./velocity-tracker.js\";\nexport * from \"./runtime.js\";\nexport * from \"./pointer-bridge.js\";\n\nexport * from \"./behaviours/behaviour.js\";\nexport * from \"./behaviours/press.js\";\nexport * from \"./behaviours/pulse.js\";\nexport * from \"./behaviours/hinge.js\";\nexport * from \"./behaviours/dial.js\";\nexport * from \"./behaviours/slide.js\";\nexport * from \"./behaviours/grab.js\";\nexport * from \"./behaviours/toss-score.js\";\nexport * from \"./behaviours/factory.js\";\n"]}
|
package/dist/ports.d.ts
CHANGED
|
@@ -16,11 +16,25 @@ export interface InteractableHit {
|
|
|
16
16
|
* Implemented by the adapter (three.js Raycaster, engine BVH, …). A
|
|
17
17
|
* provider that pre-resolves targeting (IWSDK) may make this redundant -
|
|
18
18
|
* provider hints always take precedence over hit-tester results.
|
|
19
|
+
*
|
|
20
|
+
* Each hit, and the `point` tuple in it, is a fresh value the caller owns.
|
|
21
|
+
* A ray or point passed in is read during the call and not kept.
|
|
19
22
|
*/
|
|
20
23
|
export interface HitTester {
|
|
21
24
|
hitRay(ray: RayTuple): InteractableHit | null;
|
|
22
25
|
hitProximity(point: Vec3Tuple, radius: number): InteractableHit | null;
|
|
23
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* The release velocities handed to {@link TransformPort.endHold} - the
|
|
29
|
+
* grabbing source's velocity at the moment of release, so a throw carries
|
|
30
|
+
* through. Linear is metres per second, angular is radians per second,
|
|
31
|
+
* both in world space, both plain tuples: this crosses the same boundary
|
|
32
|
+
* `PoseTuple`/`Vec3Tuple` do, so it carries no engine object either.
|
|
33
|
+
*/
|
|
34
|
+
export interface HoldRelease {
|
|
35
|
+
linearVelocity: Vec3Tuple;
|
|
36
|
+
angularVelocity: Vec3Tuple;
|
|
37
|
+
}
|
|
24
38
|
/**
|
|
25
39
|
* The transform/effect surface of ONE interactable's scene object.
|
|
26
40
|
* Behaviours write through this; the adapter owns how writes land
|
|
@@ -30,21 +44,66 @@ export interface HitTester {
|
|
|
30
44
|
* registration and applies these relative to it, so behaviours compose
|
|
31
45
|
* with anything else animating the same object (the pale-signal
|
|
32
46
|
* non-destructive-offset rule).
|
|
47
|
+
*
|
|
48
|
+
* Every tuple a port returns is a fresh value the caller owns. The port
|
|
49
|
+
* keeps no reference to it and never writes to it again. A tuple passed in
|
|
50
|
+
* is read during the call and not kept, so the caller may reuse it.
|
|
33
51
|
*/
|
|
34
52
|
export interface TransformPort {
|
|
35
|
-
/**
|
|
53
|
+
/**
|
|
54
|
+
* The LIVE world pose: where the object is now, after every offset,
|
|
55
|
+
* rotation and world-pose write, including a write made by anything else.
|
|
56
|
+
*/
|
|
36
57
|
getWorldPose(): PoseTuple;
|
|
58
|
+
/**
|
|
59
|
+
* The rest pose captured at registration, in world space. Offsets and
|
|
60
|
+
* rotations are measured from it, so it stays put while the port writes.
|
|
61
|
+
* It follows a parent that moves.
|
|
62
|
+
*/
|
|
63
|
+
getRestWorldPose(): PoseTuple;
|
|
37
64
|
/** Local-space pose relative to the captured rest (dragged observation). */
|
|
38
65
|
getLocalOffset(): Vec3Tuple;
|
|
39
66
|
/** Additive local-space offset from rest (driven press / slide). */
|
|
40
67
|
setLocalOffset(offset: Vec3Tuple): void;
|
|
41
68
|
/** Local rotation about the rest orientation (hinge / dial output). */
|
|
42
69
|
setLocalRotation(quaternion: QuatTuple): void;
|
|
43
|
-
/**
|
|
70
|
+
/**
|
|
71
|
+
* Follow a world pose while grabbed (poseOnly grab fulfilment), or place
|
|
72
|
+
* the object directly the rest of the time (reset / teleport).
|
|
73
|
+
*
|
|
74
|
+
* On a host whose object has physics, the two situations mean different
|
|
75
|
+
* things and a conforming port tells them apart with {@link beginHold}/
|
|
76
|
+
* {@link endHold}: WHILE HELD - between a `beginHold` and its matching
|
|
77
|
+
* `endHold` - the object follows every write exactly and its velocities
|
|
78
|
+
* are left untouched, because physics is suspended for the hold. NOT
|
|
79
|
+
* HELD, a write teleports the object there and clears its velocities, so
|
|
80
|
+
* it comes to rest at the pose ("back to the tee") rather than carrying
|
|
81
|
+
* whatever it was doing a moment before. A host with no physics has only
|
|
82
|
+
* the one meaning: the object is simply placed there.
|
|
83
|
+
*/
|
|
44
84
|
setWorldPose?(pose: PoseTuple): void;
|
|
45
85
|
/** Additive presentation effect (pulse): scale multiplier & emissive boost. */
|
|
46
86
|
setEffect?(effect: {
|
|
47
87
|
scale?: number;
|
|
48
88
|
emissive?: number;
|
|
49
89
|
}): void;
|
|
90
|
+
/**
|
|
91
|
+
* Suspend physics for this object: from here until the matching
|
|
92
|
+
* `endHold`, it follows `setWorldPose` writes exactly and gravity /
|
|
93
|
+
* collisions stop moving it - see {@link setWorldPose}.
|
|
94
|
+
*
|
|
95
|
+
* Present only on a port whose object carries a physics body; a port
|
|
96
|
+
* with no physics never grows this member, so it behaves exactly as
|
|
97
|
+
* before. A platform that fulfils grabs natively through its own engine
|
|
98
|
+
* (`grabsNative`) must show the same three behaviours (held / released /
|
|
99
|
+
* reset) through that engine - the runtime never calls `beginHold` for
|
|
100
|
+
* one, because the engine, not `webxr-interactions`, already owns the
|
|
101
|
+
* hold.
|
|
102
|
+
*/
|
|
103
|
+
beginHold?(): void;
|
|
104
|
+
/**
|
|
105
|
+
* Resume physics, applying `release` as the body's new velocity so a
|
|
106
|
+
* throw carries through. Present on the same terms as {@link beginHold}.
|
|
107
|
+
*/
|
|
108
|
+
endHold?(release: HoldRelease): void;
|
|
50
109
|
}
|
package/dist/ports.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ports.js","sourceRoot":"","sources":["../src/ports.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * Engine-side ports - the space queries and transform writes the core\n * cannot perform itself. Adapters implement these against their scene\n * graph; the core only ever speaks tuples and interactable ids.\n */\nimport type { PoseTuple, QuatTuple, RayTuple, Vec3Tuple } from \"@realitycollective/webxr-input\";\n\nexport interface InteractableHit {\n interactableId: string;\n /** Distance from the query origin (ray origin / probe point). */\n distance: number;\n /** World-space hit or closest point. */\n point: Vec3Tuple;\n}\n\n/**\n * Resolves which interactable a ray or a proximity probe touches.\n * Implemented by the adapter (three.js Raycaster, engine BVH, …). A\n * provider that pre-resolves targeting (IWSDK) may make this redundant -\n * provider hints always take precedence over hit-tester results.\n */\nexport interface HitTester {\n hitRay(ray: RayTuple): InteractableHit | null;\n hitProximity(point: Vec3Tuple, radius: number): InteractableHit | null;\n}\n\n/**\n * The transform/effect surface of ONE interactable's scene object.\n * Behaviours write through this; the adapter owns how writes land\n * (Object3D fields, ECS components, …).\n *\n * Offsets/rotations are FROM REST: the adapter captures the rest pose at\n * registration and applies these relative to it, so behaviours compose\n * with anything else animating the same object (the pale-signal\n * non-destructive-offset rule).\n */\nexport interface TransformPort {\n
|
|
1
|
+
{"version":3,"file":"ports.js","sourceRoot":"","sources":["../src/ports.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * Engine-side ports - the space queries and transform writes the core\n * cannot perform itself. Adapters implement these against their scene\n * graph; the core only ever speaks tuples and interactable ids.\n */\nimport type { PoseTuple, QuatTuple, RayTuple, Vec3Tuple } from \"@realitycollective/webxr-input\";\n\nexport interface InteractableHit {\n interactableId: string;\n /** Distance from the query origin (ray origin / probe point). */\n distance: number;\n /** World-space hit or closest point. */\n point: Vec3Tuple;\n}\n\n/**\n * Resolves which interactable a ray or a proximity probe touches.\n * Implemented by the adapter (three.js Raycaster, engine BVH, …). A\n * provider that pre-resolves targeting (IWSDK) may make this redundant -\n * provider hints always take precedence over hit-tester results.\n *\n * Each hit, and the `point` tuple in it, is a fresh value the caller owns.\n * A ray or point passed in is read during the call and not kept.\n */\nexport interface HitTester {\n hitRay(ray: RayTuple): InteractableHit | null;\n hitProximity(point: Vec3Tuple, radius: number): InteractableHit | null;\n}\n\n/**\n * The release velocities handed to {@link TransformPort.endHold} - the\n * grabbing source's velocity at the moment of release, so a throw carries\n * through. Linear is metres per second, angular is radians per second,\n * both in world space, both plain tuples: this crosses the same boundary\n * `PoseTuple`/`Vec3Tuple` do, so it carries no engine object either.\n */\nexport interface HoldRelease {\n linearVelocity: Vec3Tuple;\n angularVelocity: Vec3Tuple;\n}\n\n/**\n * The transform/effect surface of ONE interactable's scene object.\n * Behaviours write through this; the adapter owns how writes land\n * (Object3D fields, ECS components, …).\n *\n * Offsets/rotations are FROM REST: the adapter captures the rest pose at\n * registration and applies these relative to it, so behaviours compose\n * with anything else animating the same object (the pale-signal\n * non-destructive-offset rule).\n *\n * Every tuple a port returns is a fresh value the caller owns. The port\n * keeps no reference to it and never writes to it again. A tuple passed in\n * is read during the call and not kept, so the caller may reuse it.\n */\nexport interface TransformPort {\n /**\n * The LIVE world pose: where the object is now, after every offset,\n * rotation and world-pose write, including a write made by anything else.\n */\n getWorldPose(): PoseTuple;\n /**\n * The rest pose captured at registration, in world space. Offsets and\n * rotations are measured from it, so it stays put while the port writes.\n * It follows a parent that moves.\n */\n getRestWorldPose(): PoseTuple;\n /** Local-space pose relative to the captured rest (dragged observation). */\n getLocalOffset(): Vec3Tuple;\n /** Additive local-space offset from rest (driven press / slide). */\n setLocalOffset(offset: Vec3Tuple): void;\n /** Local rotation about the rest orientation (hinge / dial output). */\n setLocalRotation(quaternion: QuatTuple): void;\n /**\n * Follow a world pose while grabbed (poseOnly grab fulfilment), or place\n * the object directly the rest of the time (reset / teleport).\n *\n * On a host whose object has physics, the two situations mean different\n * things and a conforming port tells them apart with {@link beginHold}/\n * {@link endHold}: WHILE HELD - between a `beginHold` and its matching\n * `endHold` - the object follows every write exactly and its velocities\n * are left untouched, because physics is suspended for the hold. NOT\n * HELD, a write teleports the object there and clears its velocities, so\n * it comes to rest at the pose (\"back to the tee\") rather than carrying\n * whatever it was doing a moment before. A host with no physics has only\n * the one meaning: the object is simply placed there.\n */\n setWorldPose?(pose: PoseTuple): void;\n /** Additive presentation effect (pulse): scale multiplier & emissive boost. */\n setEffect?(effect: { scale?: number; emissive?: number }): void;\n /**\n * Suspend physics for this object: from here until the matching\n * `endHold`, it follows `setWorldPose` writes exactly and gravity /\n * collisions stop moving it - see {@link setWorldPose}.\n *\n * Present only on a port whose object carries a physics body; a port\n * with no physics never grows this member, so it behaves exactly as\n * before. A platform that fulfils grabs natively through its own engine\n * (`grabsNative`) must show the same three behaviours (held / released /\n * reset) through that engine - the runtime never calls `beginHold` for\n * one, because the engine, not `webxr-interactions`, already owns the\n * hold.\n */\n beginHold?(): void;\n /**\n * Resume physics, applying `release` as the body's new velocity so a\n * throw carries through. Present on the same terms as {@link beginHold}.\n */\n endHold?(release: HoldRelease): void;\n}\n"]}
|
package/dist/runtime.d.ts
CHANGED
|
@@ -111,6 +111,8 @@ export declare class InteractionRuntime {
|
|
|
111
111
|
/** Resolve this source's target: hints beat poke beats ray. */
|
|
112
112
|
private resolveTarget;
|
|
113
113
|
private targetable;
|
|
114
|
+
/** The largest poke radius among enabled interactables; the default when none is registered. */
|
|
115
|
+
private maxPokeRadius;
|
|
114
116
|
private flags;
|
|
115
117
|
private updateSource;
|
|
116
118
|
private gazeAllows;
|
package/dist/runtime.js
CHANGED
|
@@ -210,7 +210,22 @@ export class InteractionRuntime {
|
|
|
210
210
|
this.tickBehaviours(sources, dt);
|
|
211
211
|
}
|
|
212
212
|
dispose() {
|
|
213
|
+
if (this.disposed)
|
|
214
|
+
return;
|
|
213
215
|
this.disposed = true;
|
|
216
|
+
// A disposed runtime still owes every held grab exactly one `endHold`
|
|
217
|
+
// (zero velocity - nothing threw it away, the scene just went down),
|
|
218
|
+
// so physics never stays suspended once nothing is left to resume it.
|
|
219
|
+
for (const [id, registered] of this.interactables) {
|
|
220
|
+
if (registered.grabbedBy) {
|
|
221
|
+
this.endGrab(id, registered.grabbedBy, {
|
|
222
|
+
id: registered.grabbedBy,
|
|
223
|
+
kind: "other",
|
|
224
|
+
select: 0,
|
|
225
|
+
squeeze: 0,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
214
229
|
this.unsubscribeCaps();
|
|
215
230
|
this.interactables.clear();
|
|
216
231
|
this.sourceStates.clear();
|
|
@@ -239,6 +254,10 @@ export class InteractionRuntime {
|
|
|
239
254
|
info.gripPose = source.gripPose;
|
|
240
255
|
if (source.indexTip)
|
|
241
256
|
info.indexTip = source.indexTip;
|
|
257
|
+
if (source.linearVelocity)
|
|
258
|
+
info.linearVelocity = source.linearVelocity;
|
|
259
|
+
if (source.angularVelocity)
|
|
260
|
+
info.angularVelocity = source.angularVelocity;
|
|
242
261
|
return info;
|
|
243
262
|
}
|
|
244
263
|
/** Resolve this source's target: hints beat poke beats ray. */
|
|
@@ -249,9 +268,18 @@ export class InteractionRuntime {
|
|
|
249
268
|
if (hint && this.targetable(hint.targetId))
|
|
250
269
|
return hint.targetId;
|
|
251
270
|
if (source.indexTip && this.hitTester) {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
271
|
+
// One proximity query at the largest radius any interactable asked for,
|
|
272
|
+
// then the nearest hit must be inside ITS OWN radius. A per-interactable
|
|
273
|
+
// query would be a hit test per registration per source per frame; this
|
|
274
|
+
// keeps it to one. The trade: a farther interactable with a bigger radius
|
|
275
|
+
// is not found behind a nearer one with a smaller radius, because the
|
|
276
|
+
// tester returns only the nearest.
|
|
277
|
+
const poke = this.hitTester.hitProximity(source.indexTip, this.maxPokeRadius());
|
|
278
|
+
if (poke && this.targetable(poke.interactableId)) {
|
|
279
|
+
const registered = this.interactables.get(poke.interactableId);
|
|
280
|
+
if (poke.distance <= registered.pokeRadius)
|
|
281
|
+
return poke.interactableId;
|
|
282
|
+
}
|
|
255
283
|
}
|
|
256
284
|
if (source.ray && this.hitTester) {
|
|
257
285
|
const hit = this.hitTester.hitRay(source.ray);
|
|
@@ -264,6 +292,15 @@ export class InteractionRuntime {
|
|
|
264
292
|
const registered = this.interactables.get(id);
|
|
265
293
|
return registered !== undefined && registered.enabled;
|
|
266
294
|
}
|
|
295
|
+
/** The largest poke radius among enabled interactables; the default when none is registered. */
|
|
296
|
+
maxPokeRadius() {
|
|
297
|
+
let radius = 0;
|
|
298
|
+
for (const registered of this.interactables.values()) {
|
|
299
|
+
if (registered.enabled && registered.pokeRadius > radius)
|
|
300
|
+
radius = registered.pokeRadius;
|
|
301
|
+
}
|
|
302
|
+
return radius > 0 ? radius : DEFAULT_POKE_RADIUS;
|
|
303
|
+
}
|
|
267
304
|
flags(registered) {
|
|
268
305
|
let pressable = false;
|
|
269
306
|
let grabbable = false;
|