@relu-interactives/spatial-ecs 0.3.10 → 0.3.12

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.
Files changed (38) hide show
  1. package/dist/{XRHandPose-hAtj9wm-.d.cts → XRHandPose-DawOR0uR.d.cts} +56 -14
  2. package/dist/{XRHandPose-CjacJ_16.d.ts → XRHandPose-DhAd9Qi6.d.ts} +56 -14
  3. package/dist/{chunk-NJOO7NO6.js → chunk-7ZIN6TGT.js} +3 -3
  4. package/dist/{chunk-NJOO7NO6.js.map → chunk-7ZIN6TGT.js.map} +1 -1
  5. package/dist/{chunk-5UVBEAGW.js → chunk-ID7U4FER.js} +4 -4
  6. package/dist/{chunk-5UVBEAGW.js.map → chunk-ID7U4FER.js.map} +1 -1
  7. package/dist/{chunk-O2HSIZFC.js → chunk-LCJ6IH2O.js} +324 -47
  8. package/dist/chunk-LCJ6IH2O.js.map +1 -0
  9. package/dist/{chunk-ELSZ5U3F.js → chunk-OMDYLFTN.js} +7 -4
  10. package/dist/chunk-OMDYLFTN.js.map +1 -0
  11. package/dist/components/index.cjs +5 -2
  12. package/dist/components/index.cjs.map +1 -1
  13. package/dist/components/index.d.cts +2 -2
  14. package/dist/components/index.d.ts +2 -2
  15. package/dist/components/index.js +1 -1
  16. package/dist/docs/guide/vr-and-hand-poses.md +7 -3
  17. package/dist/factories/index.cjs +5 -2
  18. package/dist/factories/index.cjs.map +1 -1
  19. package/dist/factories/index.d.cts +1 -1
  20. package/dist/factories/index.d.ts +1 -1
  21. package/dist/factories/index.js +3 -3
  22. package/dist/{index-B7JDsx4a.d.ts → index-B49322Q3.d.ts} +2 -3
  23. package/dist/{index-DU-CF-3m.d.cts → index-DJ9bn9SF.d.cts} +70 -1
  24. package/dist/{index-B-KhN8Ox.d.ts → index-M0I5B3JA.d.ts} +70 -1
  25. package/dist/{index-B9JqYr3Q.d.cts → index-Xm3im4Pt.d.cts} +2 -3
  26. package/dist/index.cjs +326 -44
  27. package/dist/index.cjs.map +1 -1
  28. package/dist/index.d.cts +5 -5
  29. package/dist/index.d.ts +5 -5
  30. package/dist/index.js +8 -8
  31. package/dist/systems/index.cjs +326 -44
  32. package/dist/systems/index.cjs.map +1 -1
  33. package/dist/systems/index.d.cts +1 -1
  34. package/dist/systems/index.d.ts +1 -1
  35. package/dist/systems/index.js +3 -3
  36. package/package.json +1 -1
  37. package/dist/chunk-ELSZ5U3F.js.map +0 -1
  38. package/dist/chunk-O2HSIZFC.js.map +0 -1
package/dist/index.cjs CHANGED
@@ -2830,7 +2830,6 @@ var _XRInteractableComponent = class _XRInteractableComponent extends Component
2830
2830
  this.enabled = options?.enabled ?? true;
2831
2831
  this.grabbable = options?.grabbable ?? true;
2832
2832
  this.pointable = options?.pointable ?? true;
2833
- this.grabMode = options?.grabMode ?? "kinematic";
2834
2833
  this.grabReach = options?.grabReach ?? "both";
2835
2834
  this.distanceBehavior = options?.distanceBehavior ?? "snap";
2836
2835
  this.grabGesture = options?.grabGesture ?? "any";
@@ -2855,7 +2854,9 @@ var _XRInteractableComponent = class _XRInteractableComponent extends Component
2855
2854
  );
2856
2855
  this.throwOnRelease = options?.throwOnRelease ?? true;
2857
2856
  this.throwStrength = options?.throwStrength ?? 1;
2858
- this.highlightOnHover = options?.highlightOnHover ?? true;
2857
+ this.hoverBehavior = options?.hoverBehavior ?? "color";
2858
+ this.hoverColor = options?.hoverColor ?? "#4da3ff";
2859
+ this.hoverScale = options?.hoverScale ?? 1.06;
2859
2860
  }
2860
2861
  };
2861
2862
  __name(_XRInteractableComponent, "XRInteractableComponent");
@@ -2890,6 +2891,8 @@ var _XRInteractorComponent = class _XRInteractorComponent extends Component {
2890
2891
  this.handOpacity = options?.handOpacity ?? 1;
2891
2892
  this.handEmissiveIntensity = options?.handEmissiveIntensity ?? 0;
2892
2893
  this.showRay = options?.showRay ?? true;
2894
+ this.rayColor = options?.rayColor ?? "#4da3ff";
2895
+ this.rayThickness = options?.rayThickness ?? 6e-3;
2893
2896
  }
2894
2897
  };
2895
2898
  __name(_XRInteractorComponent, "XRInteractorComponent");
@@ -4866,9 +4869,6 @@ function restorePostCreateComponents(world, objectManagementSystem, targetEntity
4866
4869
  if (typeof sourceInteractable.pointable === "boolean") {
4867
4870
  interactable.pointable = sourceInteractable.pointable;
4868
4871
  }
4869
- if (typeof sourceInteractable.grabMode === "string") {
4870
- interactable.grabMode = sourceInteractable.grabMode;
4871
- }
4872
4872
  if (typeof sourceInteractable.grabReach === "string") {
4873
4873
  interactable.grabReach = sourceInteractable.grabReach;
4874
4874
  }
@@ -4927,8 +4927,16 @@ function restorePostCreateComponents(world, objectManagementSystem, targetEntity
4927
4927
  z: typeof grabRot.z === "number" ? grabRot.z : 0
4928
4928
  };
4929
4929
  }
4930
- if (typeof sourceInteractable.highlightOnHover === "boolean") {
4931
- interactable.highlightOnHover = sourceInteractable.highlightOnHover;
4930
+ if (typeof sourceInteractable.hoverBehavior === "string") {
4931
+ interactable.hoverBehavior = sourceInteractable.hoverBehavior;
4932
+ } else if (sourceInteractable.highlightOnHover === false) {
4933
+ interactable.hoverBehavior = "none";
4934
+ }
4935
+ if (typeof sourceInteractable.hoverColor === "string") {
4936
+ interactable.hoverColor = sourceInteractable.hoverColor;
4937
+ }
4938
+ if (typeof sourceInteractable.hoverScale === "number") {
4939
+ interactable.hoverScale = sourceInteractable.hoverScale;
4932
4940
  }
4933
4941
  }
4934
4942
  }
@@ -4980,6 +4988,12 @@ function restorePostCreateComponents(world, objectManagementSystem, targetEntity
4980
4988
  if (typeof sourceInteractor.showRay === "boolean") {
4981
4989
  interactor.showRay = sourceInteractor.showRay;
4982
4990
  }
4991
+ if (typeof sourceInteractor.rayColor === "string") {
4992
+ interactor.rayColor = sourceInteractor.rayColor;
4993
+ }
4994
+ if (typeof sourceInteractor.rayThickness === "number") {
4995
+ interactor.rayThickness = sourceInteractor.rayThickness;
4996
+ }
4983
4997
  }
4984
4998
  }
4985
4999
  {
@@ -7445,6 +7459,20 @@ var _PostprocessingSyncSystem = class _PostprocessingSyncSystem {
7445
7459
  __name(_PostprocessingSyncSystem, "PostprocessingSyncSystem");
7446
7460
  var PostprocessingSyncSystem = _PostprocessingSyncSystem;
7447
7461
  var MIN_SIZE = 1e-4;
7462
+ function toBodyType(RAPIER, bodyType) {
7463
+ switch (bodyType) {
7464
+ case "fixed":
7465
+ return RAPIER.RigidBodyType.Fixed;
7466
+ case "kinematicPosition":
7467
+ return RAPIER.RigidBodyType.KinematicPositionBased;
7468
+ case "kinematicVelocity":
7469
+ return RAPIER.RigidBodyType.KinematicVelocityBased;
7470
+ case "dynamic":
7471
+ default:
7472
+ return RAPIER.RigidBodyType.Dynamic;
7473
+ }
7474
+ }
7475
+ __name(toBodyType, "toBodyType");
7448
7476
  function createRigidBodyDesc(RAPIER, bodyType) {
7449
7477
  if (bodyType === "fixed") {
7450
7478
  return RAPIER.RigidBodyDesc.fixed();
@@ -7743,7 +7771,8 @@ var _PhysicsSyncSystem = class _PhysicsSyncSystem {
7743
7771
  setPhysicsLiveRef(physics, "collider", null);
7744
7772
  continue;
7745
7773
  }
7746
- const rigidBodySignature = JSON.stringify(physics.values.rigidbody);
7774
+ const { type: _desiredBodyType, ...rigidBodyRebuildValues } = physics.values.rigidbody;
7775
+ const rigidBodySignature = JSON.stringify(rigidBodyRebuildValues);
7747
7776
  const previousRigidBodySignature = this.rigidBodySignatures.get(entityId);
7748
7777
  const hasRigidBody = this.rigidBodyHandles.has(entityId);
7749
7778
  const shouldRecreateBody = !hasRigidBody || previousRigidBodySignature !== rigidBodySignature;
@@ -7798,6 +7827,25 @@ var _PhysicsSyncSystem = class _PhysicsSyncSystem {
7798
7827
  continue;
7799
7828
  }
7800
7829
  setPhysicsLiveRef(physics, "rigidBody", body);
7830
+ const desiredBodyType = this.isEditorMode ? "fixed" : physics.values.rigidbody.type;
7831
+ const desiredRapierType = toBodyType(this.rapier, desiredBodyType);
7832
+ if (body.bodyType() !== desiredRapierType) {
7833
+ body.setBodyType(desiredRapierType, true);
7834
+ if (!this.isEditorMode && desiredBodyType === "dynamic") {
7835
+ body.setAdditionalMass(physics.values.rigidbody.mass, true);
7836
+ }
7837
+ }
7838
+ const pending = physics.pendingVelocity;
7839
+ if (pending) {
7840
+ setPhysicsLiveRef(physics, "pendingVelocity", null);
7841
+ if (!this.isEditorMode && desiredBodyType === "dynamic") {
7842
+ try {
7843
+ body.setLinvel(pending.linear, true);
7844
+ body.setAngvel(pending.angular, true);
7845
+ } catch {
7846
+ }
7847
+ }
7848
+ }
7801
7849
  if (this.isEditorMode || physics.values.rigidbody.type !== "dynamic") {
7802
7850
  syncBodyFromTransform(body, transform);
7803
7851
  }
@@ -11054,6 +11102,8 @@ var _XRInteractionSystem = class _XRInteractionSystem {
11054
11102
  this.raycaster = new THREE51__namespace.Raycaster();
11055
11103
  this.grabs = /* @__PURE__ */ new Map();
11056
11104
  this.hoveredEntityId = null;
11105
+ /** Per-hand hover, with the hit distance so the ray can stop at the surface. */
11106
+ this.hovers = /* @__PURE__ */ new Map();
11057
11107
  /** Monotonic seconds, accumulated from deltaTime for velocity sampling. */
11058
11108
  this.elapsed = 0;
11059
11109
  this.inputSystem = inputSystem;
@@ -11067,6 +11117,7 @@ var _XRInteractionSystem = class _XRInteractionSystem {
11067
11117
  const inputs = this.inputSystem.getInputs();
11068
11118
  const interactables = this.collectInteractables(world);
11069
11119
  this.hoveredEntityId = null;
11120
+ this.hovers.clear();
11070
11121
  for (const input of inputs) {
11071
11122
  if (!input.connected) {
11072
11123
  this.release(world, input);
@@ -11084,6 +11135,10 @@ var _XRInteractionSystem = class _XRInteractionSystem {
11084
11135
  if (this.hoveredEntityId === null) {
11085
11136
  this.hoveredEntityId = candidate.entityId;
11086
11137
  }
11138
+ this.hovers.set(input, {
11139
+ entityId: candidate.entityId,
11140
+ distance: candidate.distance
11141
+ });
11087
11142
  const interactable = world.getComponent(
11088
11143
  candidate.entityId,
11089
11144
  XRInteractableComponent
@@ -11156,7 +11211,7 @@ var _XRInteractionSystem = class _XRInteractionSystem {
11156
11211
  transform.position.x - hand.x,
11157
11212
  transform.position.y - hand.y,
11158
11213
  transform.position.z - hand.z
11159
- );
11214
+ ).applyQuaternion(handQuaternion.clone().invert());
11160
11215
  record.handRotationOffset = handQuaternion.clone().invert().multiply(objectQuaternion);
11161
11216
  record.remote = false;
11162
11217
  record.tracker.clear();
@@ -11267,13 +11322,22 @@ var _XRInteractionSystem = class _XRInteractionSystem {
11267
11322
  }
11268
11323
  }
11269
11324
  if (nearestId !== null) {
11270
- return { entityId: nearestId, viaDistance: false };
11325
+ return {
11326
+ entityId: nearestId,
11327
+ viaDistance: false,
11328
+ distance: nearestDistance
11329
+ };
11271
11330
  }
11272
- const hitId = this.raycast(input, interactables);
11273
- if (hitId === null) return null;
11331
+ const hit = this.raycast(input, interactables);
11332
+ if (hit === null) return null;
11333
+ const hitId = hit.entityId;
11274
11334
  const interactable = world.getComponent(hitId, XRInteractableComponent);
11275
11335
  const viaDistanceAllowed = interactable?.grabReach === "distance" || interactable?.grabReach === "both";
11276
- return { entityId: hitId, viaDistance: viaDistanceAllowed };
11336
+ return {
11337
+ entityId: hitId,
11338
+ viaDistance: viaDistanceAllowed,
11339
+ distance: hit.distance
11340
+ };
11277
11341
  }
11278
11342
  collectInteractables(world) {
11279
11343
  const out = [];
@@ -11304,7 +11368,7 @@ var _XRInteractionSystem = class _XRInteractionSystem {
11304
11368
  nearestId = entry.entityId;
11305
11369
  }
11306
11370
  }
11307
- return nearestId;
11371
+ return nearestId === null ? null : { entityId: nearestId, distance: nearestDistance };
11308
11372
  }
11309
11373
  /**
11310
11374
  * Pick the authored hold this hand should take.
@@ -11379,7 +11443,7 @@ var _XRInteractionSystem = class _XRInteractionSystem {
11379
11443
  transform.position.x - hand.x,
11380
11444
  transform.position.y - hand.y,
11381
11445
  transform.position.z - hand.z
11382
- );
11446
+ ).applyQuaternion(handQuaternion.clone().invert());
11383
11447
  }
11384
11448
  const physics = world.getComponent(entityId, PhysicsComponent);
11385
11449
  let restoreBodyType = null;
@@ -11514,9 +11578,10 @@ var _XRInteractionSystem = class _XRInteractionSystem {
11514
11578
  transform.position.z = hand.z - anchor.z;
11515
11579
  writeEuler(transform.rotation, objectQuaternion);
11516
11580
  } else {
11517
- transform.position.x = hand.x + held.offset.x;
11518
- transform.position.y = hand.y + held.offset.y;
11519
- transform.position.z = hand.z + held.offset.z;
11581
+ const carried = held.offset.clone().applyQuaternion(handQuaternion);
11582
+ transform.position.x = hand.x + carried.x;
11583
+ transform.position.y = hand.y + carried.y;
11584
+ transform.position.z = hand.z + carried.z;
11520
11585
  writeEuler(transform.rotation, objectQuaternion);
11521
11586
  }
11522
11587
  held.tracker.record(
@@ -11569,19 +11634,14 @@ var _XRInteractionSystem = class _XRInteractionSystem {
11569
11634
  const physics = world.getComponent(held.entityId, PhysicsComponent);
11570
11635
  if (held.restoreBodyType !== null && physics) {
11571
11636
  physics.values.rigidbody.type = held.restoreBodyType;
11572
- const body = physics.rigidBody;
11573
11637
  const dynamic = held.restoreBodyType === "dynamic";
11574
- if (body && dynamic && interactable?.throwOnRelease) {
11638
+ if (dynamic && interactable?.throwOnRelease) {
11575
11639
  const velocity = held.tracker.computeVelocity(this.elapsed).multiplyScalar(interactable.throwStrength);
11576
- try {
11577
- body.setLinvel(
11578
- { x: velocity.x, y: velocity.y, z: velocity.z },
11579
- true
11580
- );
11581
- const spin = held.angularVelocity;
11582
- body.setAngvel({ x: spin.x, y: spin.y, z: spin.z }, true);
11583
- } catch {
11584
- }
11640
+ const spin = held.angularVelocity;
11641
+ setPhysicsLiveRef(physics, "pendingVelocity", {
11642
+ linear: { x: velocity.x, y: velocity.y, z: velocity.z },
11643
+ angular: { x: spin.x, y: spin.y, z: spin.z }
11644
+ });
11585
11645
  }
11586
11646
  }
11587
11647
  held.tracker.clear();
@@ -11629,10 +11689,41 @@ var _XRInteractionSystem = class _XRInteractionSystem {
11629
11689
  getHoveredEntityId() {
11630
11690
  return this.hoveredEntityId;
11631
11691
  }
11692
+ /**
11693
+ * What this hand is pointing at, with the distance to it.
11694
+ *
11695
+ * The distance is what lets the pointer ray stop at the surface instead of
11696
+ * passing through the object.
11697
+ */
11698
+ getHoverFor(handedness) {
11699
+ for (const [input, hover] of this.hovers) {
11700
+ if (input.handedness === handedness) return hover;
11701
+ }
11702
+ return null;
11703
+ }
11704
+ /** Every entity under a pointer this frame — drives hover feedback. */
11705
+ getHoveredEntityIds() {
11706
+ const ids = /* @__PURE__ */ new Set();
11707
+ for (const hover of this.hovers.values()) ids.add(hover.entityId);
11708
+ return ids;
11709
+ }
11710
+ /** Entities currently held, so hover feedback can skip them. */
11711
+ getHeldEntityIds() {
11712
+ const ids = /* @__PURE__ */ new Set();
11713
+ for (const record of this.grabs.values()) ids.add(record.entityId);
11714
+ return ids;
11715
+ }
11632
11716
  /** True when any source is holding something. */
11633
11717
  isGrabbing() {
11634
11718
  return this.grabs.size > 0;
11635
11719
  }
11720
+ /** True when THIS hand is holding something. */
11721
+ isGrabbingWith(handedness) {
11722
+ for (const input of this.grabs.keys()) {
11723
+ if (input.handedness === handedness) return true;
11724
+ }
11725
+ return false;
11726
+ }
11636
11727
  /**
11637
11728
  * How an entity is currently held.
11638
11729
  *
@@ -12189,6 +12280,10 @@ function applyHandMaterial(root, material) {
12189
12280
  });
12190
12281
  }
12191
12282
  __name(applyHandMaterial, "applyHandMaterial");
12283
+
12284
+ // core/systems/XRControllerVisualSystem.ts
12285
+ var RAY_LENGTH = 5;
12286
+ var RAY_RADIUS = 3e-3;
12192
12287
  var SLOTS = [
12193
12288
  { index: 0, side: "left" },
12194
12289
  { index: 1, side: "right" }
@@ -12198,6 +12293,8 @@ var _XRControllerVisualSystem = class _XRControllerVisualSystem {
12198
12293
  this.controllerModelFactory = new XRControllerModelFactory_js.XRControllerModelFactory();
12199
12294
  this.handModelFactory = new XRHandModelFactory_js.XRHandModelFactory();
12200
12295
  this.group = new THREE51__namespace.Group();
12296
+ /** Reused so a per-frame colour compare allocates nothing. */
12297
+ this.rayColorScratch = new THREE51__namespace.Color();
12201
12298
  this.slots = /* @__PURE__ */ new Map();
12202
12299
  this.attachedTo = null;
12203
12300
  /** Last applied preset signature, so materials rebuild only on real change. */
@@ -12226,7 +12323,9 @@ var _XRControllerVisualSystem = class _XRControllerVisualSystem {
12226
12323
  const hasHand = liveKind === null || liveKind === "hand";
12227
12324
  visuals.controllerModel.visible = showControllers && hasController;
12228
12325
  visuals.handModel.visible = showHands && hasHand;
12229
- visuals.ray.visible = this.shouldShowRay(interactor);
12326
+ visuals.ray.visible = this.shouldShowRay(interactor, side);
12327
+ this.fitRayToHover(visuals.ray, side);
12328
+ this.applyRayStyle(visuals.ray, interactor);
12230
12329
  if (showHands && this.handMaterial) {
12231
12330
  applyHandMaterial(visuals.handModel, this.handMaterial);
12232
12331
  }
@@ -12302,10 +12401,35 @@ var _XRControllerVisualSystem = class _XRControllerVisualSystem {
12302
12401
  * off entirely; otherwise it appears on hover/grab so it isn't permanent
12303
12402
  * clutter implying the whole scene is clickable.
12304
12403
  */
12305
- shouldShowRay(interactor) {
12404
+ shouldShowRay(interactor, side) {
12306
12405
  if (interactor && !interactor.showRay) return false;
12307
12406
  if (!this.interactionSystem) return true;
12308
- return this.interactionSystem.getHoveredEntityId() !== null || this.interactionSystem.isGrabbing();
12407
+ return this.interactionSystem.getHoverFor(side) !== null || this.interactionSystem.isGrabbingWith(side);
12408
+ }
12409
+ /** Push the author's ray colour and thickness onto the mesh. */
12410
+ applyRayStyle(ray, interactor) {
12411
+ if (!interactor) return;
12412
+ const material = ray.material;
12413
+ if (material?.color && !material.color.equals(this.rayColorScratch.set(interactor.rayColor))) {
12414
+ material.color.copy(this.rayColorScratch);
12415
+ }
12416
+ const radius = Math.max(5e-4, interactor.rayThickness / 2) / RAY_RADIUS;
12417
+ if (ray.scale.x !== radius) {
12418
+ ray.scale.x = radius;
12419
+ ray.scale.y = radius;
12420
+ }
12421
+ }
12422
+ /**
12423
+ * Stop the ray at whatever it is pointing at.
12424
+ *
12425
+ * A fixed-length ray drawn with depth testing off punches straight through the
12426
+ * object, so the user cannot tell where the pointer actually lands. Scaling it
12427
+ * to the hit distance turns it into a pointer that meets the surface.
12428
+ */
12429
+ fitRayToHover(ray, side) {
12430
+ const hover = this.interactionSystem?.getHoverFor(side) ?? null;
12431
+ const length = hover && Number.isFinite(hover.distance) && hover.distance > 0 ? Math.min(hover.distance, RAY_LENGTH) : RAY_LENGTH;
12432
+ ray.scale.z = length;
12309
12433
  }
12310
12434
  /** Lazily build per-slot visuals; the device reports profiles late. */
12311
12435
  ensureSlots(world) {
@@ -12345,22 +12469,32 @@ var _XRControllerVisualSystem = class _XRControllerVisualSystem {
12345
12469
  target.add(this.group);
12346
12470
  this.attachedTo = target;
12347
12471
  }
12472
+ /**
12473
+ * The pointer ray.
12474
+ *
12475
+ * A cylinder rather than a `THREE.Line`, because WebGL ignores `linewidth` —
12476
+ * every line renders one pixel wide regardless, which is nearly invisible on a
12477
+ * headset. A thin mesh is the only way to get a ray with real thickness
12478
+ * without pulling in a line-rendering extension.
12479
+ */
12348
12480
  createRay() {
12349
- const geometry = new THREE51__namespace.BufferGeometry().setFromPoints([
12350
- new THREE51__namespace.Vector3(0, 0, 0),
12351
- new THREE51__namespace.Vector3(0, 0, -5)
12352
- ]);
12353
- const material = new THREE51__namespace.LineBasicMaterial({
12481
+ const geometry = new THREE51__namespace.CylinderGeometry(RAY_RADIUS, RAY_RADIUS, 1, 8, 1);
12482
+ geometry.rotateX(-Math.PI / 2);
12483
+ geometry.translate(0, 0, -0.5);
12484
+ const material = new THREE51__namespace.MeshBasicMaterial({
12354
12485
  color: "#4da3ff",
12355
12486
  transparent: true,
12356
- opacity: 0.6,
12357
- depthTest: false
12487
+ opacity: 0.75,
12488
+ depthTest: false,
12489
+ depthWrite: false
12358
12490
  });
12359
- const line = new THREE51__namespace.Line(geometry, material);
12360
- line.name = "XRPointerRay";
12361
- line.renderOrder = 999;
12362
- line.visible = false;
12363
- return line;
12491
+ const mesh = new THREE51__namespace.Mesh(geometry, material);
12492
+ mesh.name = "XRPointerRay";
12493
+ mesh.renderOrder = 999;
12494
+ mesh.visible = false;
12495
+ mesh.raycast = () => {
12496
+ };
12497
+ return mesh;
12364
12498
  }
12365
12499
  dispose() {
12366
12500
  this.group.removeFromParent();
@@ -13011,6 +13145,152 @@ var _XRHandPoseSystem = class _XRHandPoseSystem {
13011
13145
  };
13012
13146
  __name(_XRHandPoseSystem, "XRHandPoseSystem");
13013
13147
  var XRHandPoseSystem = _XRHandPoseSystem;
13148
+ var XR_HOVER_SHELL_FLAG = "__xrHoverShell";
13149
+ var SHELL_INFLATE = 1.04;
13150
+ var SCALE_SMOOTH_SECONDS = 0.09;
13151
+ var _XRHoverVisualSystem = class _XRHoverVisualSystem {
13152
+ constructor(interactionSystem) {
13153
+ this.records = /* @__PURE__ */ new Map();
13154
+ this.interactionSystem = interactionSystem;
13155
+ }
13156
+ update(world, deltaTime) {
13157
+ if (!world.isXrPresenting()) {
13158
+ this.clearAll(world);
13159
+ return;
13160
+ }
13161
+ const hovered = this.interactionSystem.getHoveredEntityIds();
13162
+ const held = this.interactionSystem.getHeldEntityIds();
13163
+ for (const entityId of hovered) {
13164
+ if (held.has(entityId)) continue;
13165
+ const interactable = world.getComponent(entityId, XRInteractableComponent);
13166
+ if (!interactable?.enabled) continue;
13167
+ if (interactable.hoverBehavior === "none") continue;
13168
+ this.begin(world, entityId, interactable);
13169
+ }
13170
+ for (const [entityId, record] of this.records) {
13171
+ if (!hovered.has(entityId) || held.has(entityId)) {
13172
+ record.active = false;
13173
+ }
13174
+ }
13175
+ this.animate(world, deltaTime);
13176
+ }
13177
+ begin(world, entityId, interactable) {
13178
+ const existing = this.records.get(entityId);
13179
+ if (existing) {
13180
+ existing.active = true;
13181
+ return;
13182
+ }
13183
+ const transform = world.getComponent(entityId, TransformComponent);
13184
+ if (!transform) return;
13185
+ const wantsColor = interactable.hoverBehavior === "color" || interactable.hoverBehavior === "both";
13186
+ this.records.set(entityId, {
13187
+ shell: wantsColor ? this.addShell(world, entityId, interactable) : null,
13188
+ baseScale: new THREE51__namespace.Vector3(
13189
+ transform.scale.x,
13190
+ transform.scale.y,
13191
+ transform.scale.z
13192
+ ),
13193
+ progress: 0,
13194
+ active: true
13195
+ });
13196
+ }
13197
+ /** Build an inflated back-face copy of the object's meshes. */
13198
+ addShell(world, entityId, interactable) {
13199
+ const object = world.getComponent(entityId, Object3DRef)?.object;
13200
+ if (!object) return null;
13201
+ const material = new THREE51__namespace.MeshBasicMaterial({
13202
+ color: new THREE51__namespace.Color(interactable.hoverColor),
13203
+ // Back faces only, so the shell shows just outside the silhouette instead
13204
+ // of covering the object.
13205
+ side: THREE51__namespace.BackSide,
13206
+ transparent: true,
13207
+ opacity: 0.85,
13208
+ depthWrite: false
13209
+ });
13210
+ const group = new THREE51__namespace.Group();
13211
+ group.name = "XRHoverShell";
13212
+ group.userData[XR_HOVER_SHELL_FLAG] = true;
13213
+ group.renderOrder = 998;
13214
+ object.traverse((child) => {
13215
+ if (!(child instanceof THREE51__namespace.Mesh) || !child.geometry) return;
13216
+ if (child.userData[XR_HOVER_SHELL_FLAG]) return;
13217
+ const clone3 = new THREE51__namespace.Mesh(child.geometry, material);
13218
+ child.updateWorldMatrix(true, false);
13219
+ object.updateWorldMatrix(true, false);
13220
+ clone3.matrixAutoUpdate = false;
13221
+ clone3.matrix.copy(object.matrixWorld).invert().multiply(child.matrixWorld).scale(new THREE51__namespace.Vector3(SHELL_INFLATE, SHELL_INFLATE, SHELL_INFLATE));
13222
+ clone3.userData[XR_HOVER_SHELL_FLAG] = true;
13223
+ clone3.raycast = () => {
13224
+ };
13225
+ group.add(clone3);
13226
+ });
13227
+ if (group.children.length === 0) {
13228
+ material.dispose();
13229
+ return null;
13230
+ }
13231
+ object.add(group);
13232
+ return group;
13233
+ }
13234
+ animate(world, deltaTime) {
13235
+ const step = 1 - Math.exp(-Math.max(0, deltaTime) / SCALE_SMOOTH_SECONDS);
13236
+ for (const [entityId, record] of Array.from(this.records)) {
13237
+ const interactable = world.getComponent(entityId, XRInteractableComponent);
13238
+ const target = record.active ? 1 : 0;
13239
+ record.progress += (target - record.progress) * step;
13240
+ const wantsScale = interactable?.hoverBehavior === "scale" || interactable?.hoverBehavior === "both";
13241
+ if (wantsScale) {
13242
+ const transform = world.getComponent(entityId, TransformComponent);
13243
+ if (transform) {
13244
+ const factor = 1 + (interactable.hoverScale - 1) * Math.min(1, record.progress);
13245
+ transform.scale.x = record.baseScale.x * factor;
13246
+ transform.scale.y = record.baseScale.y * factor;
13247
+ transform.scale.z = record.baseScale.z * factor;
13248
+ }
13249
+ }
13250
+ if (!record.active && record.progress < 0.01) {
13251
+ this.finish(world, entityId, record);
13252
+ }
13253
+ }
13254
+ }
13255
+ /** Remove the effect and restore the authored scale exactly. */
13256
+ finish(world, entityId, record) {
13257
+ const transform = world.getComponent(entityId, TransformComponent);
13258
+ if (transform) {
13259
+ transform.scale.x = record.baseScale.x;
13260
+ transform.scale.y = record.baseScale.y;
13261
+ transform.scale.z = record.baseScale.z;
13262
+ }
13263
+ this.disposeShell(record.shell);
13264
+ this.records.delete(entityId);
13265
+ }
13266
+ disposeShell(shell) {
13267
+ if (!shell) return;
13268
+ shell.parent?.remove(shell);
13269
+ shell.traverse((child) => {
13270
+ if (child instanceof THREE51__namespace.Mesh) {
13271
+ const material = child.material;
13272
+ if (Array.isArray(material)) {
13273
+ for (const entry of material) entry.dispose();
13274
+ } else {
13275
+ material?.dispose();
13276
+ }
13277
+ }
13278
+ });
13279
+ }
13280
+ clearAll(world) {
13281
+ for (const [entityId, record] of Array.from(this.records)) {
13282
+ this.finish(world, entityId, record);
13283
+ }
13284
+ }
13285
+ dispose() {
13286
+ for (const record of this.records.values()) {
13287
+ this.disposeShell(record.shell);
13288
+ }
13289
+ this.records.clear();
13290
+ }
13291
+ };
13292
+ __name(_XRHoverVisualSystem, "XRHoverVisualSystem");
13293
+ var XRHoverVisualSystem = _XRHoverVisualSystem;
13014
13294
 
13015
13295
  // core/World.ts
13016
13296
  var _World = class _World {
@@ -14443,12 +14723,14 @@ exports.VideoSystem = VideoSystem;
14443
14723
  exports.World = World;
14444
14724
  exports.XRControllerVisualSystem = XRControllerVisualSystem;
14445
14725
  exports.XRHandPoseSystem = XRHandPoseSystem;
14726
+ exports.XRHoverVisualSystem = XRHoverVisualSystem;
14446
14727
  exports.XRInputSystem = XRInputSystem;
14447
14728
  exports.XRInteractionSystem = XRInteractionSystem;
14448
14729
  exports.XRLocomotionSystem = XRLocomotionSystem;
14449
14730
  exports.XRSessionSystem = XRSessionSystem;
14450
14731
  exports.XRTeleportVisualSystem = XRTeleportVisualSystem;
14451
14732
  exports.XR_FINGERS = XR_FINGERS;
14733
+ exports.XR_HOVER_SHELL_FLAG = XR_HOVER_SHELL_FLAG;
14452
14734
  exports.angularVelocityFrom = angularVelocityFrom;
14453
14735
  exports.applyHandMaterial = applyHandMaterial;
14454
14736
  exports.applyPrefabToSnapshotInstances = applyPrefabToSnapshotInstances;