@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.
@@ -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
- /** Current world pose. */
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
- /** Follow a world pose while grabbed (poseOnly grab fulfilment). */
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 /** Current world pose. */\n getWorldPose(): 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 /** Follow a world pose while grabbed (poseOnly grab fulfilment). */\n setWorldPose?(pose: PoseTuple): void;\n /** Additive presentation effect (pulse): scale multiplier & emissive boost. */\n setEffect?(effect: { scale?: number; emissive?: number }): void;\n}\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
- const poke = this.hitTester.hitProximity(source.indexTip, DEFAULT_POKE_RADIUS);
253
- if (poke && this.targetable(poke.interactableId))
254
- return poke.interactableId;
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;