@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
@@ -1,5 +1,5 @@
1
- import { resolveDracoDecoderPath, applyTransform, SHARED_MODEL_RESOURCE_FLAG } from './chunk-NJOO7NO6.js';
2
- import { AnimationComponent, ModelComponent, CameraComponent, Object3DRef, Environment, Postprocessing, TransformComponent, PhysicsComponent, setPhysicsLiveRef, LightComponent, MaterialComponent, DEFAULT_MESH_GEOMETRY_PARAMETERS, MeshGeometryComponent, InstancedMeshComponent, ParentId, ScriptComponent, ScriptObjectComponent, XRRigComponent, VRTeleportAreaComponent, XRInteractableComponent, createAttachPoint, XRHandPoseComponent, createHandPoseEntry, XRInteractorComponent, XRInteractorSideComponent, InstanceSlotComponent, Name, ImageComponent, SpriteComponent, VideoComponent, AudioComponent, ImageTarget, Selectable, PrefabNode, PrefabInstance, handPoseDefinitionFor, getHandPosePreset } from './chunk-ELSZ5U3F.js';
1
+ import { resolveDracoDecoderPath, applyTransform, SHARED_MODEL_RESOURCE_FLAG } from './chunk-7ZIN6TGT.js';
2
+ import { AnimationComponent, ModelComponent, CameraComponent, Object3DRef, Environment, Postprocessing, TransformComponent, PhysicsComponent, setPhysicsLiveRef, LightComponent, MaterialComponent, DEFAULT_MESH_GEOMETRY_PARAMETERS, MeshGeometryComponent, InstancedMeshComponent, ParentId, ScriptComponent, ScriptObjectComponent, XRRigComponent, VRTeleportAreaComponent, XRInteractableComponent, createAttachPoint, XRHandPoseComponent, createHandPoseEntry, XRInteractorComponent, XRInteractorSideComponent, InstanceSlotComponent, Name, ImageComponent, SpriteComponent, VideoComponent, AudioComponent, ImageTarget, Selectable, PrefabNode, PrefabInstance, handPoseDefinitionFor, getHandPosePreset } from './chunk-OMDYLFTN.js';
3
3
  import { __name } from './chunk-OY7ZODZP.js';
4
4
  import * as THREE19 from 'three';
5
5
  import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js';
@@ -901,6 +901,20 @@ var _PostprocessingSyncSystem = class _PostprocessingSyncSystem {
901
901
  __name(_PostprocessingSyncSystem, "PostprocessingSyncSystem");
902
902
  var PostprocessingSyncSystem = _PostprocessingSyncSystem;
903
903
  var MIN_SIZE = 1e-4;
904
+ function toBodyType(RAPIER, bodyType) {
905
+ switch (bodyType) {
906
+ case "fixed":
907
+ return RAPIER.RigidBodyType.Fixed;
908
+ case "kinematicPosition":
909
+ return RAPIER.RigidBodyType.KinematicPositionBased;
910
+ case "kinematicVelocity":
911
+ return RAPIER.RigidBodyType.KinematicVelocityBased;
912
+ case "dynamic":
913
+ default:
914
+ return RAPIER.RigidBodyType.Dynamic;
915
+ }
916
+ }
917
+ __name(toBodyType, "toBodyType");
904
918
  function createRigidBodyDesc(RAPIER, bodyType) {
905
919
  if (bodyType === "fixed") {
906
920
  return RAPIER.RigidBodyDesc.fixed();
@@ -1199,7 +1213,8 @@ var _PhysicsSyncSystem = class _PhysicsSyncSystem {
1199
1213
  setPhysicsLiveRef(physics, "collider", null);
1200
1214
  continue;
1201
1215
  }
1202
- const rigidBodySignature = JSON.stringify(physics.values.rigidbody);
1216
+ const { type: _desiredBodyType, ...rigidBodyRebuildValues } = physics.values.rigidbody;
1217
+ const rigidBodySignature = JSON.stringify(rigidBodyRebuildValues);
1203
1218
  const previousRigidBodySignature = this.rigidBodySignatures.get(entityId);
1204
1219
  const hasRigidBody = this.rigidBodyHandles.has(entityId);
1205
1220
  const shouldRecreateBody = !hasRigidBody || previousRigidBodySignature !== rigidBodySignature;
@@ -1254,6 +1269,25 @@ var _PhysicsSyncSystem = class _PhysicsSyncSystem {
1254
1269
  continue;
1255
1270
  }
1256
1271
  setPhysicsLiveRef(physics, "rigidBody", body);
1272
+ const desiredBodyType = this.isEditorMode ? "fixed" : physics.values.rigidbody.type;
1273
+ const desiredRapierType = toBodyType(this.rapier, desiredBodyType);
1274
+ if (body.bodyType() !== desiredRapierType) {
1275
+ body.setBodyType(desiredRapierType, true);
1276
+ if (!this.isEditorMode && desiredBodyType === "dynamic") {
1277
+ body.setAdditionalMass(physics.values.rigidbody.mass, true);
1278
+ }
1279
+ }
1280
+ const pending = physics.pendingVelocity;
1281
+ if (pending) {
1282
+ setPhysicsLiveRef(physics, "pendingVelocity", null);
1283
+ if (!this.isEditorMode && desiredBodyType === "dynamic") {
1284
+ try {
1285
+ body.setLinvel(pending.linear, true);
1286
+ body.setAngvel(pending.angular, true);
1287
+ } catch {
1288
+ }
1289
+ }
1290
+ }
1257
1291
  if (this.isEditorMode || physics.values.rigidbody.type !== "dynamic") {
1258
1292
  syncBodyFromTransform(body, transform);
1259
1293
  }
@@ -2981,9 +3015,6 @@ function restorePostCreateComponents(world, objectManagementSystem, targetEntity
2981
3015
  if (typeof sourceInteractable.pointable === "boolean") {
2982
3016
  interactable.pointable = sourceInteractable.pointable;
2983
3017
  }
2984
- if (typeof sourceInteractable.grabMode === "string") {
2985
- interactable.grabMode = sourceInteractable.grabMode;
2986
- }
2987
3018
  if (typeof sourceInteractable.grabReach === "string") {
2988
3019
  interactable.grabReach = sourceInteractable.grabReach;
2989
3020
  }
@@ -3042,8 +3073,16 @@ function restorePostCreateComponents(world, objectManagementSystem, targetEntity
3042
3073
  z: typeof grabRot.z === "number" ? grabRot.z : 0
3043
3074
  };
3044
3075
  }
3045
- if (typeof sourceInteractable.highlightOnHover === "boolean") {
3046
- interactable.highlightOnHover = sourceInteractable.highlightOnHover;
3076
+ if (typeof sourceInteractable.hoverBehavior === "string") {
3077
+ interactable.hoverBehavior = sourceInteractable.hoverBehavior;
3078
+ } else if (sourceInteractable.highlightOnHover === false) {
3079
+ interactable.hoverBehavior = "none";
3080
+ }
3081
+ if (typeof sourceInteractable.hoverColor === "string") {
3082
+ interactable.hoverColor = sourceInteractable.hoverColor;
3083
+ }
3084
+ if (typeof sourceInteractable.hoverScale === "number") {
3085
+ interactable.hoverScale = sourceInteractable.hoverScale;
3047
3086
  }
3048
3087
  }
3049
3088
  }
@@ -3095,6 +3134,12 @@ function restorePostCreateComponents(world, objectManagementSystem, targetEntity
3095
3134
  if (typeof sourceInteractor.showRay === "boolean") {
3096
3135
  interactor.showRay = sourceInteractor.showRay;
3097
3136
  }
3137
+ if (typeof sourceInteractor.rayColor === "string") {
3138
+ interactor.rayColor = sourceInteractor.rayColor;
3139
+ }
3140
+ if (typeof sourceInteractor.rayThickness === "number") {
3141
+ interactor.rayThickness = sourceInteractor.rayThickness;
3142
+ }
3098
3143
  }
3099
3144
  }
3100
3145
  {
@@ -6537,6 +6582,8 @@ var _XRInteractionSystem = class _XRInteractionSystem {
6537
6582
  this.raycaster = new THREE19.Raycaster();
6538
6583
  this.grabs = /* @__PURE__ */ new Map();
6539
6584
  this.hoveredEntityId = null;
6585
+ /** Per-hand hover, with the hit distance so the ray can stop at the surface. */
6586
+ this.hovers = /* @__PURE__ */ new Map();
6540
6587
  /** Monotonic seconds, accumulated from deltaTime for velocity sampling. */
6541
6588
  this.elapsed = 0;
6542
6589
  this.inputSystem = inputSystem;
@@ -6550,6 +6597,7 @@ var _XRInteractionSystem = class _XRInteractionSystem {
6550
6597
  const inputs = this.inputSystem.getInputs();
6551
6598
  const interactables = this.collectInteractables(world);
6552
6599
  this.hoveredEntityId = null;
6600
+ this.hovers.clear();
6553
6601
  for (const input of inputs) {
6554
6602
  if (!input.connected) {
6555
6603
  this.release(world, input);
@@ -6567,6 +6615,10 @@ var _XRInteractionSystem = class _XRInteractionSystem {
6567
6615
  if (this.hoveredEntityId === null) {
6568
6616
  this.hoveredEntityId = candidate.entityId;
6569
6617
  }
6618
+ this.hovers.set(input, {
6619
+ entityId: candidate.entityId,
6620
+ distance: candidate.distance
6621
+ });
6570
6622
  const interactable = world.getComponent(
6571
6623
  candidate.entityId,
6572
6624
  XRInteractableComponent
@@ -6639,7 +6691,7 @@ var _XRInteractionSystem = class _XRInteractionSystem {
6639
6691
  transform.position.x - hand.x,
6640
6692
  transform.position.y - hand.y,
6641
6693
  transform.position.z - hand.z
6642
- );
6694
+ ).applyQuaternion(handQuaternion.clone().invert());
6643
6695
  record.handRotationOffset = handQuaternion.clone().invert().multiply(objectQuaternion);
6644
6696
  record.remote = false;
6645
6697
  record.tracker.clear();
@@ -6750,13 +6802,22 @@ var _XRInteractionSystem = class _XRInteractionSystem {
6750
6802
  }
6751
6803
  }
6752
6804
  if (nearestId !== null) {
6753
- return { entityId: nearestId, viaDistance: false };
6805
+ return {
6806
+ entityId: nearestId,
6807
+ viaDistance: false,
6808
+ distance: nearestDistance
6809
+ };
6754
6810
  }
6755
- const hitId = this.raycast(input, interactables);
6756
- if (hitId === null) return null;
6811
+ const hit = this.raycast(input, interactables);
6812
+ if (hit === null) return null;
6813
+ const hitId = hit.entityId;
6757
6814
  const interactable = world.getComponent(hitId, XRInteractableComponent);
6758
6815
  const viaDistanceAllowed = interactable?.grabReach === "distance" || interactable?.grabReach === "both";
6759
- return { entityId: hitId, viaDistance: viaDistanceAllowed };
6816
+ return {
6817
+ entityId: hitId,
6818
+ viaDistance: viaDistanceAllowed,
6819
+ distance: hit.distance
6820
+ };
6760
6821
  }
6761
6822
  collectInteractables(world) {
6762
6823
  const out = [];
@@ -6787,7 +6848,7 @@ var _XRInteractionSystem = class _XRInteractionSystem {
6787
6848
  nearestId = entry.entityId;
6788
6849
  }
6789
6850
  }
6790
- return nearestId;
6851
+ return nearestId === null ? null : { entityId: nearestId, distance: nearestDistance };
6791
6852
  }
6792
6853
  /**
6793
6854
  * Pick the authored hold this hand should take.
@@ -6862,7 +6923,7 @@ var _XRInteractionSystem = class _XRInteractionSystem {
6862
6923
  transform.position.x - hand.x,
6863
6924
  transform.position.y - hand.y,
6864
6925
  transform.position.z - hand.z
6865
- );
6926
+ ).applyQuaternion(handQuaternion.clone().invert());
6866
6927
  }
6867
6928
  const physics = world.getComponent(entityId, PhysicsComponent);
6868
6929
  let restoreBodyType = null;
@@ -6997,9 +7058,10 @@ var _XRInteractionSystem = class _XRInteractionSystem {
6997
7058
  transform.position.z = hand.z - anchor.z;
6998
7059
  writeEuler(transform.rotation, objectQuaternion);
6999
7060
  } else {
7000
- transform.position.x = hand.x + held.offset.x;
7001
- transform.position.y = hand.y + held.offset.y;
7002
- transform.position.z = hand.z + held.offset.z;
7061
+ const carried = held.offset.clone().applyQuaternion(handQuaternion);
7062
+ transform.position.x = hand.x + carried.x;
7063
+ transform.position.y = hand.y + carried.y;
7064
+ transform.position.z = hand.z + carried.z;
7003
7065
  writeEuler(transform.rotation, objectQuaternion);
7004
7066
  }
7005
7067
  held.tracker.record(
@@ -7052,19 +7114,14 @@ var _XRInteractionSystem = class _XRInteractionSystem {
7052
7114
  const physics = world.getComponent(held.entityId, PhysicsComponent);
7053
7115
  if (held.restoreBodyType !== null && physics) {
7054
7116
  physics.values.rigidbody.type = held.restoreBodyType;
7055
- const body = physics.rigidBody;
7056
7117
  const dynamic = held.restoreBodyType === "dynamic";
7057
- if (body && dynamic && interactable?.throwOnRelease) {
7118
+ if (dynamic && interactable?.throwOnRelease) {
7058
7119
  const velocity = held.tracker.computeVelocity(this.elapsed).multiplyScalar(interactable.throwStrength);
7059
- try {
7060
- body.setLinvel(
7061
- { x: velocity.x, y: velocity.y, z: velocity.z },
7062
- true
7063
- );
7064
- const spin = held.angularVelocity;
7065
- body.setAngvel({ x: spin.x, y: spin.y, z: spin.z }, true);
7066
- } catch {
7067
- }
7120
+ const spin = held.angularVelocity;
7121
+ setPhysicsLiveRef(physics, "pendingVelocity", {
7122
+ linear: { x: velocity.x, y: velocity.y, z: velocity.z },
7123
+ angular: { x: spin.x, y: spin.y, z: spin.z }
7124
+ });
7068
7125
  }
7069
7126
  }
7070
7127
  held.tracker.clear();
@@ -7112,10 +7169,41 @@ var _XRInteractionSystem = class _XRInteractionSystem {
7112
7169
  getHoveredEntityId() {
7113
7170
  return this.hoveredEntityId;
7114
7171
  }
7172
+ /**
7173
+ * What this hand is pointing at, with the distance to it.
7174
+ *
7175
+ * The distance is what lets the pointer ray stop at the surface instead of
7176
+ * passing through the object.
7177
+ */
7178
+ getHoverFor(handedness) {
7179
+ for (const [input, hover] of this.hovers) {
7180
+ if (input.handedness === handedness) return hover;
7181
+ }
7182
+ return null;
7183
+ }
7184
+ /** Every entity under a pointer this frame — drives hover feedback. */
7185
+ getHoveredEntityIds() {
7186
+ const ids = /* @__PURE__ */ new Set();
7187
+ for (const hover of this.hovers.values()) ids.add(hover.entityId);
7188
+ return ids;
7189
+ }
7190
+ /** Entities currently held, so hover feedback can skip them. */
7191
+ getHeldEntityIds() {
7192
+ const ids = /* @__PURE__ */ new Set();
7193
+ for (const record of this.grabs.values()) ids.add(record.entityId);
7194
+ return ids;
7195
+ }
7115
7196
  /** True when any source is holding something. */
7116
7197
  isGrabbing() {
7117
7198
  return this.grabs.size > 0;
7118
7199
  }
7200
+ /** True when THIS hand is holding something. */
7201
+ isGrabbingWith(handedness) {
7202
+ for (const input of this.grabs.keys()) {
7203
+ if (input.handedness === handedness) return true;
7204
+ }
7205
+ return false;
7206
+ }
7119
7207
  /**
7120
7208
  * How an entity is currently held.
7121
7209
  *
@@ -7672,6 +7760,10 @@ function applyHandMaterial(root, material) {
7672
7760
  });
7673
7761
  }
7674
7762
  __name(applyHandMaterial, "applyHandMaterial");
7763
+
7764
+ // core/systems/XRControllerVisualSystem.ts
7765
+ var RAY_LENGTH = 5;
7766
+ var RAY_RADIUS = 3e-3;
7675
7767
  var SLOTS = [
7676
7768
  { index: 0, side: "left" },
7677
7769
  { index: 1, side: "right" }
@@ -7681,6 +7773,8 @@ var _XRControllerVisualSystem = class _XRControllerVisualSystem {
7681
7773
  this.controllerModelFactory = new XRControllerModelFactory();
7682
7774
  this.handModelFactory = new XRHandModelFactory();
7683
7775
  this.group = new THREE19.Group();
7776
+ /** Reused so a per-frame colour compare allocates nothing. */
7777
+ this.rayColorScratch = new THREE19.Color();
7684
7778
  this.slots = /* @__PURE__ */ new Map();
7685
7779
  this.attachedTo = null;
7686
7780
  /** Last applied preset signature, so materials rebuild only on real change. */
@@ -7709,7 +7803,9 @@ var _XRControllerVisualSystem = class _XRControllerVisualSystem {
7709
7803
  const hasHand = liveKind === null || liveKind === "hand";
7710
7804
  visuals.controllerModel.visible = showControllers && hasController;
7711
7805
  visuals.handModel.visible = showHands && hasHand;
7712
- visuals.ray.visible = this.shouldShowRay(interactor);
7806
+ visuals.ray.visible = this.shouldShowRay(interactor, side);
7807
+ this.fitRayToHover(visuals.ray, side);
7808
+ this.applyRayStyle(visuals.ray, interactor);
7713
7809
  if (showHands && this.handMaterial) {
7714
7810
  applyHandMaterial(visuals.handModel, this.handMaterial);
7715
7811
  }
@@ -7785,10 +7881,35 @@ var _XRControllerVisualSystem = class _XRControllerVisualSystem {
7785
7881
  * off entirely; otherwise it appears on hover/grab so it isn't permanent
7786
7882
  * clutter implying the whole scene is clickable.
7787
7883
  */
7788
- shouldShowRay(interactor) {
7884
+ shouldShowRay(interactor, side) {
7789
7885
  if (interactor && !interactor.showRay) return false;
7790
7886
  if (!this.interactionSystem) return true;
7791
- return this.interactionSystem.getHoveredEntityId() !== null || this.interactionSystem.isGrabbing();
7887
+ return this.interactionSystem.getHoverFor(side) !== null || this.interactionSystem.isGrabbingWith(side);
7888
+ }
7889
+ /** Push the author's ray colour and thickness onto the mesh. */
7890
+ applyRayStyle(ray, interactor) {
7891
+ if (!interactor) return;
7892
+ const material = ray.material;
7893
+ if (material?.color && !material.color.equals(this.rayColorScratch.set(interactor.rayColor))) {
7894
+ material.color.copy(this.rayColorScratch);
7895
+ }
7896
+ const radius = Math.max(5e-4, interactor.rayThickness / 2) / RAY_RADIUS;
7897
+ if (ray.scale.x !== radius) {
7898
+ ray.scale.x = radius;
7899
+ ray.scale.y = radius;
7900
+ }
7901
+ }
7902
+ /**
7903
+ * Stop the ray at whatever it is pointing at.
7904
+ *
7905
+ * A fixed-length ray drawn with depth testing off punches straight through the
7906
+ * object, so the user cannot tell where the pointer actually lands. Scaling it
7907
+ * to the hit distance turns it into a pointer that meets the surface.
7908
+ */
7909
+ fitRayToHover(ray, side) {
7910
+ const hover = this.interactionSystem?.getHoverFor(side) ?? null;
7911
+ const length = hover && Number.isFinite(hover.distance) && hover.distance > 0 ? Math.min(hover.distance, RAY_LENGTH) : RAY_LENGTH;
7912
+ ray.scale.z = length;
7792
7913
  }
7793
7914
  /** Lazily build per-slot visuals; the device reports profiles late. */
7794
7915
  ensureSlots(world) {
@@ -7828,22 +7949,32 @@ var _XRControllerVisualSystem = class _XRControllerVisualSystem {
7828
7949
  target.add(this.group);
7829
7950
  this.attachedTo = target;
7830
7951
  }
7952
+ /**
7953
+ * The pointer ray.
7954
+ *
7955
+ * A cylinder rather than a `THREE.Line`, because WebGL ignores `linewidth` —
7956
+ * every line renders one pixel wide regardless, which is nearly invisible on a
7957
+ * headset. A thin mesh is the only way to get a ray with real thickness
7958
+ * without pulling in a line-rendering extension.
7959
+ */
7831
7960
  createRay() {
7832
- const geometry = new THREE19.BufferGeometry().setFromPoints([
7833
- new THREE19.Vector3(0, 0, 0),
7834
- new THREE19.Vector3(0, 0, -5)
7835
- ]);
7836
- const material = new THREE19.LineBasicMaterial({
7961
+ const geometry = new THREE19.CylinderGeometry(RAY_RADIUS, RAY_RADIUS, 1, 8, 1);
7962
+ geometry.rotateX(-Math.PI / 2);
7963
+ geometry.translate(0, 0, -0.5);
7964
+ const material = new THREE19.MeshBasicMaterial({
7837
7965
  color: "#4da3ff",
7838
7966
  transparent: true,
7839
- opacity: 0.6,
7840
- depthTest: false
7967
+ opacity: 0.75,
7968
+ depthTest: false,
7969
+ depthWrite: false
7841
7970
  });
7842
- const line = new THREE19.Line(geometry, material);
7843
- line.name = "XRPointerRay";
7844
- line.renderOrder = 999;
7845
- line.visible = false;
7846
- return line;
7971
+ const mesh = new THREE19.Mesh(geometry, material);
7972
+ mesh.name = "XRPointerRay";
7973
+ mesh.renderOrder = 999;
7974
+ mesh.visible = false;
7975
+ mesh.raycast = () => {
7976
+ };
7977
+ return mesh;
7847
7978
  }
7848
7979
  dispose() {
7849
7980
  this.group.removeFromParent();
@@ -8494,7 +8625,153 @@ var _XRHandPoseSystem = class _XRHandPoseSystem {
8494
8625
  };
8495
8626
  __name(_XRHandPoseSystem, "XRHandPoseSystem");
8496
8627
  var XRHandPoseSystem = _XRHandPoseSystem;
8628
+ var XR_HOVER_SHELL_FLAG = "__xrHoverShell";
8629
+ var SHELL_INFLATE = 1.04;
8630
+ var SCALE_SMOOTH_SECONDS = 0.09;
8631
+ var _XRHoverVisualSystem = class _XRHoverVisualSystem {
8632
+ constructor(interactionSystem) {
8633
+ this.records = /* @__PURE__ */ new Map();
8634
+ this.interactionSystem = interactionSystem;
8635
+ }
8636
+ update(world, deltaTime) {
8637
+ if (!world.isXrPresenting()) {
8638
+ this.clearAll(world);
8639
+ return;
8640
+ }
8641
+ const hovered = this.interactionSystem.getHoveredEntityIds();
8642
+ const held = this.interactionSystem.getHeldEntityIds();
8643
+ for (const entityId of hovered) {
8644
+ if (held.has(entityId)) continue;
8645
+ const interactable = world.getComponent(entityId, XRInteractableComponent);
8646
+ if (!interactable?.enabled) continue;
8647
+ if (interactable.hoverBehavior === "none") continue;
8648
+ this.begin(world, entityId, interactable);
8649
+ }
8650
+ for (const [entityId, record] of this.records) {
8651
+ if (!hovered.has(entityId) || held.has(entityId)) {
8652
+ record.active = false;
8653
+ }
8654
+ }
8655
+ this.animate(world, deltaTime);
8656
+ }
8657
+ begin(world, entityId, interactable) {
8658
+ const existing = this.records.get(entityId);
8659
+ if (existing) {
8660
+ existing.active = true;
8661
+ return;
8662
+ }
8663
+ const transform = world.getComponent(entityId, TransformComponent);
8664
+ if (!transform) return;
8665
+ const wantsColor = interactable.hoverBehavior === "color" || interactable.hoverBehavior === "both";
8666
+ this.records.set(entityId, {
8667
+ shell: wantsColor ? this.addShell(world, entityId, interactable) : null,
8668
+ baseScale: new THREE19.Vector3(
8669
+ transform.scale.x,
8670
+ transform.scale.y,
8671
+ transform.scale.z
8672
+ ),
8673
+ progress: 0,
8674
+ active: true
8675
+ });
8676
+ }
8677
+ /** Build an inflated back-face copy of the object's meshes. */
8678
+ addShell(world, entityId, interactable) {
8679
+ const object = world.getComponent(entityId, Object3DRef)?.object;
8680
+ if (!object) return null;
8681
+ const material = new THREE19.MeshBasicMaterial({
8682
+ color: new THREE19.Color(interactable.hoverColor),
8683
+ // Back faces only, so the shell shows just outside the silhouette instead
8684
+ // of covering the object.
8685
+ side: THREE19.BackSide,
8686
+ transparent: true,
8687
+ opacity: 0.85,
8688
+ depthWrite: false
8689
+ });
8690
+ const group = new THREE19.Group();
8691
+ group.name = "XRHoverShell";
8692
+ group.userData[XR_HOVER_SHELL_FLAG] = true;
8693
+ group.renderOrder = 998;
8694
+ object.traverse((child) => {
8695
+ if (!(child instanceof THREE19.Mesh) || !child.geometry) return;
8696
+ if (child.userData[XR_HOVER_SHELL_FLAG]) return;
8697
+ const clone2 = new THREE19.Mesh(child.geometry, material);
8698
+ child.updateWorldMatrix(true, false);
8699
+ object.updateWorldMatrix(true, false);
8700
+ clone2.matrixAutoUpdate = false;
8701
+ clone2.matrix.copy(object.matrixWorld).invert().multiply(child.matrixWorld).scale(new THREE19.Vector3(SHELL_INFLATE, SHELL_INFLATE, SHELL_INFLATE));
8702
+ clone2.userData[XR_HOVER_SHELL_FLAG] = true;
8703
+ clone2.raycast = () => {
8704
+ };
8705
+ group.add(clone2);
8706
+ });
8707
+ if (group.children.length === 0) {
8708
+ material.dispose();
8709
+ return null;
8710
+ }
8711
+ object.add(group);
8712
+ return group;
8713
+ }
8714
+ animate(world, deltaTime) {
8715
+ const step = 1 - Math.exp(-Math.max(0, deltaTime) / SCALE_SMOOTH_SECONDS);
8716
+ for (const [entityId, record] of Array.from(this.records)) {
8717
+ const interactable = world.getComponent(entityId, XRInteractableComponent);
8718
+ const target = record.active ? 1 : 0;
8719
+ record.progress += (target - record.progress) * step;
8720
+ const wantsScale = interactable?.hoverBehavior === "scale" || interactable?.hoverBehavior === "both";
8721
+ if (wantsScale) {
8722
+ const transform = world.getComponent(entityId, TransformComponent);
8723
+ if (transform) {
8724
+ const factor = 1 + (interactable.hoverScale - 1) * Math.min(1, record.progress);
8725
+ transform.scale.x = record.baseScale.x * factor;
8726
+ transform.scale.y = record.baseScale.y * factor;
8727
+ transform.scale.z = record.baseScale.z * factor;
8728
+ }
8729
+ }
8730
+ if (!record.active && record.progress < 0.01) {
8731
+ this.finish(world, entityId, record);
8732
+ }
8733
+ }
8734
+ }
8735
+ /** Remove the effect and restore the authored scale exactly. */
8736
+ finish(world, entityId, record) {
8737
+ const transform = world.getComponent(entityId, TransformComponent);
8738
+ if (transform) {
8739
+ transform.scale.x = record.baseScale.x;
8740
+ transform.scale.y = record.baseScale.y;
8741
+ transform.scale.z = record.baseScale.z;
8742
+ }
8743
+ this.disposeShell(record.shell);
8744
+ this.records.delete(entityId);
8745
+ }
8746
+ disposeShell(shell) {
8747
+ if (!shell) return;
8748
+ shell.parent?.remove(shell);
8749
+ shell.traverse((child) => {
8750
+ if (child instanceof THREE19.Mesh) {
8751
+ const material = child.material;
8752
+ if (Array.isArray(material)) {
8753
+ for (const entry of material) entry.dispose();
8754
+ } else {
8755
+ material?.dispose();
8756
+ }
8757
+ }
8758
+ });
8759
+ }
8760
+ clearAll(world) {
8761
+ for (const [entityId, record] of Array.from(this.records)) {
8762
+ this.finish(world, entityId, record);
8763
+ }
8764
+ }
8765
+ dispose() {
8766
+ for (const record of this.records.values()) {
8767
+ this.disposeShell(record.shell);
8768
+ }
8769
+ this.records.clear();
8770
+ }
8771
+ };
8772
+ __name(_XRHoverVisualSystem, "XRHoverVisualSystem");
8773
+ var XRHoverVisualSystem = _XRHoverVisualSystem;
8497
8774
 
8498
- export { ADDED_NODE_MARKER_COMPONENT, ARC_GRAVITY, ARC_MAX_TIME, ARC_SPEED, ARC_STEPS, AnimationSystem, AudioSystem, Behaviour, CURLED_COS, CameraSyncSystem, DEFAULT_ENTER_CONFIDENCE, DEFAULT_EXIT_CONFIDENCE, DEFAULT_LOST_GRACE_SECONDS, DEFAULT_MIN_HOLD_SECONDS, EnvironmentSyncSystem, FALLBACK_HAND_SCALE, HYDRATABLE_ENTITY_KINDS, HandPoseTracker, InputSystem, InstancedMeshSystem, KeyboardKeys, LightSyncSystem, MATCH_FALLOFF, MAX_PLAUSIBLE_SPEED, MAX_SAMPLE_AGE_SECONDS, MaterialSyncSystem, MeshGeometrySystem, NODE_KEY_SEPARATOR, NO_XR_CAPABILITIES, OVERRIDE_DIFF_EXCLUDED_COMPONENTS, OVERRIDE_ROOT_EXCLUDED_COMPONENTS, ObjectVisibilitySystem, PINCH_FINGERS, PREFAB_FILE_VERSION, PREFAB_FILE_VERSION_NESTED, PhysicsSyncSystem, PostprocessingSyncSystem, RenderSystem, SCRIPT_OBJECT_ROOT_FLAG, STRAIGHT_COS, ScriptBehaviourSystem, ScriptObject, ScriptObjectSystem, ThrowVelocityTracker, TransformSyncSystem, VELOCITY_SAMPLES, VRTeleportAreaSystem, VideoSystem, XRControllerVisualSystem, XRHandPoseSystem, XRInputSystem, XRInteractionSystem, XRLocomotionSystem, XRSessionSystem, XRTeleportVisualSystem, XR_FINGERS, angularVelocityFrom, applyHandMaterial, arcGroundCrossing, buildComponentValueMaps, buildNodeKeyMapsFromSnapshot, buildNodeMapsFromSnapshot, canonicalizeComponentValue, clearPrefabCache, collectDiffPaths, collectEntitySubtree, collectSnapshotSubtreeIds, computeFingerCurl, computeHandShape, computeInstanceOverrides, computePalmPose, computePalmWidth, computePinch, computeSpread, createHandMaterial, decanonicalizeComponentValue, despawnPrefab, encodeOverride, estimateHandScale, findSnapshotRootEntityIds, findTopLevelNestedRegions, groupOverridePaths, hydrateWorldFromSnapshot, hydrateWorldFromSnapshotDetailed, indexSnapshotByEntity, instantiateSubtree, loadPrefab, matchHandPose, mergeComponentValue, parseOverride, parsePrefabAsset, prefabFileVersionFor, readXrCapabilities, restoreComponentState, restorePostCreateComponents, sampleTeleportArc, setPrefabAssetUrlResolver, spawnPrefab };
8499
- //# sourceMappingURL=chunk-O2HSIZFC.js.map
8500
- //# sourceMappingURL=chunk-O2HSIZFC.js.map
8775
+ export { ADDED_NODE_MARKER_COMPONENT, ARC_GRAVITY, ARC_MAX_TIME, ARC_SPEED, ARC_STEPS, AnimationSystem, AudioSystem, Behaviour, CURLED_COS, CameraSyncSystem, DEFAULT_ENTER_CONFIDENCE, DEFAULT_EXIT_CONFIDENCE, DEFAULT_LOST_GRACE_SECONDS, DEFAULT_MIN_HOLD_SECONDS, EnvironmentSyncSystem, FALLBACK_HAND_SCALE, HYDRATABLE_ENTITY_KINDS, HandPoseTracker, InputSystem, InstancedMeshSystem, KeyboardKeys, LightSyncSystem, MATCH_FALLOFF, MAX_PLAUSIBLE_SPEED, MAX_SAMPLE_AGE_SECONDS, MaterialSyncSystem, MeshGeometrySystem, NODE_KEY_SEPARATOR, NO_XR_CAPABILITIES, OVERRIDE_DIFF_EXCLUDED_COMPONENTS, OVERRIDE_ROOT_EXCLUDED_COMPONENTS, ObjectVisibilitySystem, PINCH_FINGERS, PREFAB_FILE_VERSION, PREFAB_FILE_VERSION_NESTED, PhysicsSyncSystem, PostprocessingSyncSystem, RenderSystem, SCRIPT_OBJECT_ROOT_FLAG, STRAIGHT_COS, ScriptBehaviourSystem, ScriptObject, ScriptObjectSystem, ThrowVelocityTracker, TransformSyncSystem, VELOCITY_SAMPLES, VRTeleportAreaSystem, VideoSystem, XRControllerVisualSystem, XRHandPoseSystem, XRHoverVisualSystem, XRInputSystem, XRInteractionSystem, XRLocomotionSystem, XRSessionSystem, XRTeleportVisualSystem, XR_FINGERS, XR_HOVER_SHELL_FLAG, angularVelocityFrom, applyHandMaterial, arcGroundCrossing, buildComponentValueMaps, buildNodeKeyMapsFromSnapshot, buildNodeMapsFromSnapshot, canonicalizeComponentValue, clearPrefabCache, collectDiffPaths, collectEntitySubtree, collectSnapshotSubtreeIds, computeFingerCurl, computeHandShape, computeInstanceOverrides, computePalmPose, computePalmWidth, computePinch, computeSpread, createHandMaterial, decanonicalizeComponentValue, despawnPrefab, encodeOverride, estimateHandScale, findSnapshotRootEntityIds, findTopLevelNestedRegions, groupOverridePaths, hydrateWorldFromSnapshot, hydrateWorldFromSnapshotDetailed, indexSnapshotByEntity, instantiateSubtree, loadPrefab, matchHandPose, mergeComponentValue, parseOverride, parsePrefabAsset, prefabFileVersionFor, readXrCapabilities, restoreComponentState, restorePostCreateComponents, sampleTeleportArc, setPrefabAssetUrlResolver, spawnPrefab };
8776
+ //# sourceMappingURL=chunk-LCJ6IH2O.js.map
8777
+ //# sourceMappingURL=chunk-LCJ6IH2O.js.map