@realitycollective/babylon-interactions 0.1.0-preview.6 → 0.1.1-preview.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,7 +4,25 @@ Change log for the Reality Collective WebXR Interaction Extensions packages. All
4
4
 
5
5
  The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Preview builds are not listed separately. The entry for a version accumulates while its previews are published, and is dated when that version is released.
6
6
 
7
- ## [0.1.0]
7
+ ## [0.1.1]
8
+
9
+ ### Added
10
+
11
+ - `@realitycollective/webxr-interactions` - shared conformance suites for `HitTester` and `TransformPort`, `hitTesterContractCases()` and `transformPortContractCases()`, alongside the existing `inputProviderContractCases()` from `@realitycollective/webxr-input`. Runner-free data, the same pattern as WebXR-UIExtensions' `windowHostContractCases()`: each case throws a plain `Error` naming what a platform's ray/proximity query or transform read/write broke, and asserts only what `ports.ts` documents - a ray or proximity query answers null or an id with a positive distance and a point near the query, the nearer of two targets wins, an offset round-trips, `getWorldPose()` is finite with a unit quaternion, `getRestWorldPose()` is the registration pose and stays put, `getWorldPose()` follows `setLocalOffset`, `setLocalRotation` and `setWorldPose` from that rest frame, every returned tuple is a fresh value the caller owns and no tuple passed in is kept, and the optional `setEffect` accepts its shape without throwing. Each platform's subject starts away from the origin and turned, so a port that ignores its rest frame fails. Run against all five platforms in `packages/iwsdk-interactions/test/port-parity.test.ts`, next to the provider parity suite; every platform passes every case.
12
+ - `@realitycollective/native-interactions` - a native host adapter, the fifth platform. It reads the `input` and `interactions` slices a native app (OpenXR on Quest, CompositorServices on visionOS) installs on `globalThis.__rcHost`, or hands in directly, and maps them onto `InputProvider`, `HitTester` and `TransformPort` with no engine dependency at all - assets and rendering stay in the native app, and only tuples and target ids cross the boundary. Every snapshot, hit and pose read across that boundary is copied, so a host that reuses its own buffers cannot reach an object the app is still holding, and optional members (`getHeadPose`, `sampleHints`, `pulse`, `setPresenceVisible`, `setPresenceModality`, `setWorldPose`, `setEffect`) are present only when the host slice itself carries them. `createNativeInteractions()` is the one-call setup, with the same shape as the other adapters' setups; `attachToHost` drives updates from the app's frame callback. The public surface matches the other adapters, and slice reading and tuple copying stay internal. Covered headlessly by an in-memory fake of both slices, including the shared `InputProvider` contract suite.
13
+
14
+ ### Changed
15
+
16
+ - `@realitycollective/webxr-interactions` - `TransformPort.getWorldPose()` is the LIVE world pose on every platform, and the new required `getRestWorldPose()` returns the rest pose captured at registration, in world space. The core only said "current world pose", and the platforms disagreed: the three.js, IWSDK and XR Blocks ports returned the rest pose, while the Babylon.js port returned the live one. The same behaviour therefore acted differently by platform. The hinge, slide and dial behaviours measure the hand against the rest pose, so they now call `getRestWorldPose()`. Grab and toss scoring keep `getWorldPose()` and now see where the object really is. `ports.ts` also states tuple ownership for `TransformPort` and `HitTester`: every returned tuple is fresh and owned by the caller, and a tuple passed in is read during the call and not kept. Every platform already met the ownership rule; the suites now hold them to it. The native `interactions` slice gains `getRestWorldPose(targetId)`, which the native app must implement. This adds a required member to `TransformPort`, so a port written outside this repository must implement it.
17
+ - `@realitycollective/xrblocks-interactions` - the structural slices `XBVec3Like`, `XBQuatLike`, `XBRayLike` and `XBControllerLike` are exported, under the same `XB` prefix as `XBRaySourceLike`, `XBDirectTouchLike` and `XBFrameLike` whose fields they type. They were module-private, so a consumer building a fake frame for a test had nothing to name, and TypeDoc reported each as referenced but undocumented.
18
+
19
+ ### Fixed
20
+
21
+ - `@realitycollective/threejs-interactions`, `@realitycollective/iwsdk-interactions` and `@realitycollective/xrblocks-interactions` - `getWorldPose()` returned the pose captured at registration, not where the object was. Toss scoring therefore read a thrown ball at its registered position until the app called `recaptureRest()`, and a second grab of an object moved without a recapture took its hold offset from the old position. `getWorldPose()` now returns the live pose, as the Babylon.js port already did.
22
+ - `@realitycollective/webxr-interactions` - `InteractableDescriptor.pokeRadius` had no effect. The value was stored on the registration and documented as the poke trigger radius, but the proximity query always used the 5 cm default, so an interactable asking for a wider or narrower poke got the default. The runtime now queries once per source at the largest radius any enabled interactable registered, and accepts the nearest hit only when it lies inside that interactable's own radius. One query per source is kept deliberately; the trade is that a farther interactable with a larger radius is not found behind a nearer one with a smaller radius, because the hit tester returns only the nearest. Covered by a runtime test that failed before the change.
23
+ - CI - the staging deploy published under `--branch=pr-<number>` while the `-test` Pages project's production branch is `staging`, so only `pr-<n>.webxr-interactions-test.pages.dev` aliases were ever created and the project's root URL was a 404. It now deploys as `staging` on every pull request and on every push to `development`, so `webxr-interactions-test.pages.dev` serves the newest preview build, the same arrangement WebXR-UIExtensions already had.
24
+
25
+ ## [0.1.0] - 2026-09-17
8
26
 
9
27
  ### Added
10
28
 
@@ -57,4 +75,5 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
57
75
  - All five packages depend on `@realitycollective/webxr-input`, released independently from the [WebXR-Input](https://github.com/realitycollective/WebXR-Input) repository. That package must be published before this one.
58
76
  - The interaction packages themselves carry no IWSDK coupling beyond the adapter: `webxr-interactions` is engine-free, and `threejs-interactions` and `xrblocks-interactions` peer only on three.js.
59
77
 
60
- [0.1.0]: https://github.com/realitycollective/WebXR-Interactions/commits/main
78
+ [0.1.1]: https://github.com/realitycollective/WebXR-Interactions/compare/v0.1.0...development
79
+ [0.1.0]: https://github.com/realitycollective/WebXR-Interactions/releases/tag/v0.1.0
@@ -36,15 +36,14 @@ export declare class BabylonTransformPort implements TransformPort {
36
36
  constructor(node: BabylonTransformNodeLike, options?: BabylonTransformPortOptions);
37
37
  /** Re-read the node's current local transform as the new rest. */
38
38
  recaptureRest(): void;
39
+ /** The node's LIVE world pose, from its absolute position and rotation. */
40
+ getWorldPose(): PoseTuple;
39
41
  /**
40
- * The node's LIVE world pose, from its absolute position and rotation.
41
- *
42
- * This differs from the three.js port, which reports the rest pose in
43
- * world space. Babylon computes absolutes for its own render anyway, so
44
- * reading them costs nothing and a grab starts from where the object
45
- * actually is rather than from where it was registered.
42
+ * The captured rest pose in world space, resolved through the parent's
43
+ * absolute position and rotation. Parent SCALE is ignored, the same
44
+ * simplification as {@link setWorldPose}.
46
45
  */
47
- getWorldPose(): PoseTuple;
46
+ getRestWorldPose(): PoseTuple;
48
47
  getLocalOffset(): Vec3Tuple;
49
48
  setLocalOffset(offset: Vec3Tuple): void;
50
49
  setLocalRotation(quaternion: QuatTuple): void;
@@ -17,18 +17,28 @@ export class BabylonTransformPort {
17
17
  this.restQuaternion = toQuat(this.node.rotationQuaternion);
18
18
  this.restScale = toVec3(this.node.scaling) ?? [1, 1, 1];
19
19
  }
20
- /**
21
- * The node's LIVE world pose, from its absolute position and rotation.
22
- *
23
- * This differs from the three.js port, which reports the rest pose in
24
- * world space. Babylon computes absolutes for its own render anyway, so
25
- * reading them costs nothing and a grab starts from where the object
26
- * actually is rather than from where it was registered.
27
- */
20
+ /** The node's LIVE world pose, from its absolute position and rotation. */
28
21
  getWorldPose() {
29
22
  this.node.computeWorldMatrix?.(true);
30
23
  return nodeWorldPose(this.node);
31
24
  }
25
+ /**
26
+ * The captured rest pose in world space, resolved through the parent's
27
+ * absolute position and rotation. Parent SCALE is ignored, the same
28
+ * simplification as {@link setWorldPose}.
29
+ */
30
+ getRestWorldPose() {
31
+ const parent = parentOf(this.node);
32
+ if (!parent) {
33
+ return { position: [...this.restPosition], quaternion: [...this.restQuaternion] };
34
+ }
35
+ parent.computeWorldMatrix?.(true);
36
+ const parentPose = nodeWorldPose(parent);
37
+ return {
38
+ position: vAdd(parentPose.position, vApplyQuat(this.restPosition, parentPose.quaternion)),
39
+ quaternion: quatMultiply(parentPose.quaternion, this.restQuaternion),
40
+ };
41
+ }
32
42
  getLocalOffset() {
33
43
  const delta = vSub(toVec3(this.node.position) ?? [0, 0, 0], this.restPosition);
34
44
  return vApplyQuat(delta, quatConjugate(this.restQuaternion));
@@ -1 +1 @@
1
- {"version":3,"file":"transform-port.js","sourceRoot":"","sources":["../src/transform-port.ts"],"names":[],"mappings":"AAaA,OAAO,EACL,aAAa,EACb,YAAY,EACZ,IAAI,EACJ,UAAU,EACV,IAAI,GAEL,MAAM,uCAAuC,CAAC;AAC/C,OAAO,EACL,aAAa,EACb,QAAQ,EACR,MAAM,EACN,MAAM,EACN,SAAS,EACT,SAAS,GAGV,MAAM,oBAAoB,CAAC;AAiB5B,MAAM,OAAO,oBAAoB;IACd,IAAI,CAA2B;IAC/B,gBAAgB,CAA8B;IACvD,YAAY,GAAc,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;IACpC,cAAc,GAAc,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;IACzC,SAAS,GAAc,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;IAEzC,YAAY,IAA8B,EAAE,UAAuC,EAAE;QACnF,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;QACzF,IAAI,CAAC,aAAa,EAAE,CAAC;IACvB,CAAC;IAED,kEAAkE;IAClE,aAAa;QACX,IAAI,CAAC,YAAY,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;QAC5D,IAAI,CAAC,cAAc,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC;QAC3D,IAAI,CAAC,SAAS,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;IAC1D,CAAC;IAED;;;;;;;OAOG;IACH,YAAY;QACV,IAAI,CAAC,IAAI,CAAC,kBAAkB,EAAE,CAAC,IAAI,CAAC,CAAC;QACrC,OAAO,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAClC,CAAC;IAED,cAAc;QACZ,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC;QAC/E,OAAO,UAAU,CAAC,KAAK,EAAE,aAAa,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC;IAC/D,CAAC;IAED,cAAc,CAAC,MAAiB;QAC9B,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,UAAU,CAAC,MAAM,EAAE,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC;IAClG,CAAC;IAED,gBAAgB,CAAC,UAAqB;QACpC,IAAI,CAAC,aAAa,CAAC,YAAY,CAAC,IAAI,CAAC,cAAc,EAAE,UAAU,CAAC,CAAC,CAAC;IACpE,CAAC;IAED;;;;;;;OAOG;IACH,YAAY,CAAC,IAAe;QAC1B,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACnC,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;YAC7C,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YACpC,OAAO;QACT,CAAC;QACD,MAAM,UAAU,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC;QACzC,MAAM,OAAO,GAAG,aAAa,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC;QACrD,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,UAAU,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC;QAC7F,IAAI,CAAC,aAAa,CAAC,YAAY,CAAC,OAAO,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC;IAC7D,CAAC;IAED;;;;;OAKG;IACH,SAAS,CAAC,MAA6C;QACrD,IAAI,MAAM,CAAC,KAAK,KAAK,SAAS;YAAE,OAAO;QACvC,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC;QAClC,IAAI,CAAC,OAAO;YAAE,OAAO;QACrB,SAAS,CAAC,OAAO,EAAE;YACjB,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,KAAK;YAChC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,KAAK;YAChC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,KAAK;SACjC,CAAC,CAAC;IACL,CAAC;IAED,8EAA8E;IACtE,aAAa,CAAC,KAAgB;QACpC,IAAI,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,kBAAkB,CAAC;QAC1C,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,MAAM,GAAG,IAAI,CAAC,gBAAgB,EAAE,CAAC;YACjC,IAAI,CAAC,IAAI,CAAC,kBAAkB,GAAG,MAAM,CAAC;QACxC,CAAC;QACD,SAAS,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IAC3B,CAAC;CACF","sourcesContent":["/**\n * BabylonTransformPort - the read/write surface of one interactable's\n * Babylon node, honouring the core's FROM-REST semantics: the rest pose is\n * captured at construction and offsets and rotations apply relative to it,\n * so a behaviour composes with anything else animating the same node.\n *\n * Every write mutates the node's existing `Vector3`/`Quaternion` in place.\n * Babylon compares the live values against its cache to decide whether the\n * world matrix needs recomputing, so in-place writes are seen; assigning a\n * plain object in their place would break Babylon, which calls methods on\n * them.\n */\nimport type { PoseTuple, QuatTuple, Vec3Tuple } from \"@realitycollective/webxr-input\";\nimport {\n quatConjugate,\n quatMultiply,\n vAdd,\n vApplyQuat,\n vSub,\n type TransformPort,\n} from \"@realitycollective/webxr-interactions\";\nimport {\n nodeWorldPose,\n parentOf,\n toQuat,\n toVec3,\n writeQuat,\n writeVec3,\n type BabylonQuaternionLike,\n type BabylonTransformNodeLike,\n} from \"./babylon-types.js\";\n\nexport interface BabylonTransformPortOptions {\n /**\n * Builds the `Quaternion` written to a node whose `rotationQuaternion` is\n * null - a node still driven by Euler `rotation`, which is Babylon's\n * default. Pass `() => Quaternion.Identity()` from `@babylonjs/core`.\n *\n * The fallback is a plain `{ x, y, z, w }` object, which carries the\n * numbers correctly but is NOT a Babylon `Quaternion` and will fail as\n * soon as Babylon calls a method on it. So: either give a node a real\n * `rotationQuaternion` before registering it, or supply this factory.\n * A node that already has one needs neither - it is mutated in place.\n */\n createQuaternion?: () => BabylonQuaternionLike;\n}\n\nexport class BabylonTransformPort implements TransformPort {\n private readonly node: BabylonTransformNodeLike;\n private readonly createQuaternion: () => BabylonQuaternionLike;\n private restPosition: Vec3Tuple = [0, 0, 0];\n private restQuaternion: QuatTuple = [0, 0, 0, 1];\n private restScale: Vec3Tuple = [1, 1, 1];\n\n constructor(node: BabylonTransformNodeLike, options: BabylonTransformPortOptions = {}) {\n this.node = node;\n this.createQuaternion = options.createQuaternion ?? (() => ({ x: 0, y: 0, z: 0, w: 1 }));\n this.recaptureRest();\n }\n\n /** Re-read the node's current local transform as the new rest. */\n recaptureRest(): void {\n this.restPosition = toVec3(this.node.position) ?? [0, 0, 0];\n this.restQuaternion = toQuat(this.node.rotationQuaternion);\n this.restScale = toVec3(this.node.scaling) ?? [1, 1, 1];\n }\n\n /**\n * The node's LIVE world pose, from its absolute position and rotation.\n *\n * This differs from the three.js port, which reports the rest pose in\n * world space. Babylon computes absolutes for its own render anyway, so\n * reading them costs nothing and a grab starts from where the object\n * actually is rather than from where it was registered.\n */\n getWorldPose(): PoseTuple {\n this.node.computeWorldMatrix?.(true);\n return nodeWorldPose(this.node);\n }\n\n getLocalOffset(): Vec3Tuple {\n const delta = vSub(toVec3(this.node.position) ?? [0, 0, 0], this.restPosition);\n return vApplyQuat(delta, quatConjugate(this.restQuaternion));\n }\n\n setLocalOffset(offset: Vec3Tuple): void {\n writeVec3(this.node.position, vAdd(this.restPosition, vApplyQuat(offset, this.restQuaternion)));\n }\n\n setLocalRotation(quaternion: QuatTuple): void {\n this.writeRotation(quatMultiply(this.restQuaternion, quaternion));\n }\n\n /**\n * Follow a world pose while grabbed. With a parent, the pose is resolved\n * into the parent's frame through its absolute position and rotation.\n *\n * Simplification: parent SCALE is ignored. A grabbed object under a scaled\n * parent tracks the hand at the wrong distance. Grabbables are expected to\n * sit under an unscaled parent, which is how the demos build them.\n */\n setWorldPose(pose: PoseTuple): void {\n const parent = parentOf(this.node);\n if (!parent) {\n writeVec3(this.node.position, pose.position);\n this.writeRotation(pose.quaternion);\n return;\n }\n const parentPose = nodeWorldPose(parent);\n const inverse = quatConjugate(parentPose.quaternion);\n writeVec3(this.node.position, vApplyQuat(vSub(pose.position, parentPose.position), inverse));\n this.writeRotation(quatMultiply(inverse, pose.quaternion));\n }\n\n /**\n * Uniform scale about the rest scale. The emissive part of the intent is\n * not applied: reaching a material's emissive colour means knowing which\n * Babylon material the node carries, and this package holds no Babylon\n * types. Apps that want the glow subscribe to the core's feedback intents.\n */\n setEffect(effect: { scale?: number; emissive?: number }): void {\n if (effect.scale === undefined) return;\n const scaling = this.node.scaling;\n if (!scaling) return;\n writeVec3(scaling, [\n this.restScale[0] * effect.scale,\n this.restScale[1] * effect.scale,\n this.restScale[2] * effect.scale,\n ]);\n }\n\n /** Write a quaternion, creating the node's `rotationQuaternion` if needed. */\n private writeRotation(value: QuatTuple): void {\n let target = this.node.rotationQuaternion;\n if (!target) {\n target = this.createQuaternion();\n this.node.rotationQuaternion = target;\n }\n writeQuat(target, value);\n }\n}\n"]}
1
+ {"version":3,"file":"transform-port.js","sourceRoot":"","sources":["../src/transform-port.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * BabylonTransformPort - the read/write surface of one interactable's\n * Babylon node, honouring the core's FROM-REST semantics: the rest pose is\n * captured at construction and offsets and rotations apply relative to it,\n * so a behaviour composes with anything else animating the same node.\n *\n * Every write mutates the node's existing `Vector3`/`Quaternion` in place.\n * Babylon compares the live values against its cache to decide whether the\n * world matrix needs recomputing, so in-place writes are seen; assigning a\n * plain object in their place would break Babylon, which calls methods on\n * them.\n */\nimport type { PoseTuple, QuatTuple, Vec3Tuple } from \"@realitycollective/webxr-input\";\nimport {\n quatConjugate,\n quatMultiply,\n vAdd,\n vApplyQuat,\n vSub,\n type TransformPort,\n} from \"@realitycollective/webxr-interactions\";\nimport {\n nodeWorldPose,\n parentOf,\n toQuat,\n toVec3,\n writeQuat,\n writeVec3,\n type BabylonQuaternionLike,\n type BabylonTransformNodeLike,\n} from \"./babylon-types.js\";\n\nexport interface BabylonTransformPortOptions {\n /**\n * Builds the `Quaternion` written to a node whose `rotationQuaternion` is\n * null - a node still driven by Euler `rotation`, which is Babylon's\n * default. Pass `() => Quaternion.Identity()` from `@babylonjs/core`.\n *\n * The fallback is a plain `{ x, y, z, w }` object, which carries the\n * numbers correctly but is NOT a Babylon `Quaternion` and will fail as\n * soon as Babylon calls a method on it. So: either give a node a real\n * `rotationQuaternion` before registering it, or supply this factory.\n * A node that already has one needs neither - it is mutated in place.\n */\n createQuaternion?: () => BabylonQuaternionLike;\n}\n\nexport class BabylonTransformPort implements TransformPort {\n private readonly node: BabylonTransformNodeLike;\n private readonly createQuaternion: () => BabylonQuaternionLike;\n private restPosition: Vec3Tuple = [0, 0, 0];\n private restQuaternion: QuatTuple = [0, 0, 0, 1];\n private restScale: Vec3Tuple = [1, 1, 1];\n\n constructor(node: BabylonTransformNodeLike, options: BabylonTransformPortOptions = {}) {\n this.node = node;\n this.createQuaternion = options.createQuaternion ?? (() => ({ x: 0, y: 0, z: 0, w: 1 }));\n this.recaptureRest();\n }\n\n /** Re-read the node's current local transform as the new rest. */\n recaptureRest(): void {\n this.restPosition = toVec3(this.node.position) ?? [0, 0, 0];\n this.restQuaternion = toQuat(this.node.rotationQuaternion);\n this.restScale = toVec3(this.node.scaling) ?? [1, 1, 1];\n }\n\n /** The node's LIVE world pose, from its absolute position and rotation. */\n getWorldPose(): PoseTuple {\n this.node.computeWorldMatrix?.(true);\n return nodeWorldPose(this.node);\n }\n\n /**\n * The captured rest pose in world space, resolved through the parent's\n * absolute position and rotation. Parent SCALE is ignored, the same\n * simplification as {@link setWorldPose}.\n */\n getRestWorldPose(): PoseTuple {\n const parent = parentOf(this.node);\n if (!parent) {\n return { position: [...this.restPosition], quaternion: [...this.restQuaternion] };\n }\n parent.computeWorldMatrix?.(true);\n const parentPose = nodeWorldPose(parent);\n return {\n position: vAdd(parentPose.position, vApplyQuat(this.restPosition, parentPose.quaternion)),\n quaternion: quatMultiply(parentPose.quaternion, this.restQuaternion),\n };\n }\n\n getLocalOffset(): Vec3Tuple {\n const delta = vSub(toVec3(this.node.position) ?? [0, 0, 0], this.restPosition);\n return vApplyQuat(delta, quatConjugate(this.restQuaternion));\n }\n\n setLocalOffset(offset: Vec3Tuple): void {\n writeVec3(this.node.position, vAdd(this.restPosition, vApplyQuat(offset, this.restQuaternion)));\n }\n\n setLocalRotation(quaternion: QuatTuple): void {\n this.writeRotation(quatMultiply(this.restQuaternion, quaternion));\n }\n\n /**\n * Follow a world pose while grabbed. With a parent, the pose is resolved\n * into the parent's frame through its absolute position and rotation.\n *\n * Simplification: parent SCALE is ignored. A grabbed object under a scaled\n * parent tracks the hand at the wrong distance. Grabbables are expected to\n * sit under an unscaled parent, which is how the demos build them.\n */\n setWorldPose(pose: PoseTuple): void {\n const parent = parentOf(this.node);\n if (!parent) {\n writeVec3(this.node.position, pose.position);\n this.writeRotation(pose.quaternion);\n return;\n }\n const parentPose = nodeWorldPose(parent);\n const inverse = quatConjugate(parentPose.quaternion);\n writeVec3(this.node.position, vApplyQuat(vSub(pose.position, parentPose.position), inverse));\n this.writeRotation(quatMultiply(inverse, pose.quaternion));\n }\n\n /**\n * Uniform scale about the rest scale. The emissive part of the intent is\n * not applied: reaching a material's emissive colour means knowing which\n * Babylon material the node carries, and this package holds no Babylon\n * types. Apps that want the glow subscribe to the core's feedback intents.\n */\n setEffect(effect: { scale?: number; emissive?: number }): void {\n if (effect.scale === undefined) return;\n const scaling = this.node.scaling;\n if (!scaling) return;\n writeVec3(scaling, [\n this.restScale[0] * effect.scale,\n this.restScale[1] * effect.scale,\n this.restScale[2] * effect.scale,\n ]);\n }\n\n /** Write a quaternion, creating the node's `rotationQuaternion` if needed. */\n private writeRotation(value: QuatTuple): void {\n let target = this.node.rotationQuaternion;\n if (!target) {\n target = this.createQuaternion();\n this.node.rotationQuaternion = target;\n }\n writeQuat(target, value);\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@realitycollective/babylon-interactions",
3
- "version": "0.1.0-preview.6",
3
+ "version": "0.1.1-preview.0",
4
4
  "description": "Babylon.js adapter for the Reality Collective Interaction Extensions - maps a Babylon WebXR experience (WebXRDefaultExperience controllers, motion controller components, hand-tracking joints) into the engine-free @realitycollective/webxr-interactions core, with a scene pointer fallback on desktop. Structurally typed: no @babylonjs/core import. Re-exports the core.",
5
5
  "keywords": [
6
6
  "realitycollective",
@@ -33,8 +33,8 @@
33
33
  "typecheck": "tsc -p tsconfig.json --noEmit"
34
34
  },
35
35
  "dependencies": {
36
- "@realitycollective/webxr-input": "^0.1.1",
37
- "@realitycollective/webxr-interactions": "^0.1.0-preview.6"
36
+ "@realitycollective/webxr-input": "^0.1.4",
37
+ "@realitycollective/webxr-interactions": "^0.1.1-preview.0"
38
38
  },
39
39
  "repository": {
40
40
  "type": "git",