@relu-interactives/spatial-ecs 0.3.10 → 0.3.11

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-CjacJ_16.d.ts → XRHandPose-B6I4y1lj.d.ts} +48 -14
  2. package/dist/{XRHandPose-hAtj9wm-.d.cts → XRHandPose-C-KJeYcJ.d.cts} +48 -14
  3. package/dist/{chunk-5UVBEAGW.js → chunk-FSHRPY65.js} +4 -4
  4. package/dist/{chunk-5UVBEAGW.js.map → chunk-FSHRPY65.js.map} +1 -1
  5. package/dist/{chunk-O2HSIZFC.js → chunk-KLU2M43M.js} +270 -33
  6. package/dist/chunk-KLU2M43M.js.map +1 -0
  7. package/dist/{chunk-ELSZ5U3F.js → chunk-TXEGYR76.js} +5 -4
  8. package/dist/chunk-TXEGYR76.js.map +1 -0
  9. package/dist/{chunk-NJOO7NO6.js → chunk-Y3AC3VZ2.js} +3 -3
  10. package/dist/{chunk-NJOO7NO6.js.map → chunk-Y3AC3VZ2.js.map} +1 -1
  11. package/dist/components/index.cjs +3 -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 +3 -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-B-KhN8Ox.d.ts → index-Bi_wIo1o.d.ts} +56 -1
  23. package/dist/{index-B7JDsx4a.d.ts → index-CEZp7qLL.d.ts} +2 -3
  24. package/dist/{index-B9JqYr3Q.d.cts → index-DPRnnSI-.d.cts} +2 -3
  25. package/dist/{index-DU-CF-3m.d.cts → index-jlgDou9W.d.cts} +56 -1
  26. package/dist/index.cjs +270 -30
  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 +270 -30
  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");
@@ -4866,9 +4867,6 @@ function restorePostCreateComponents(world, objectManagementSystem, targetEntity
4866
4867
  if (typeof sourceInteractable.pointable === "boolean") {
4867
4868
  interactable.pointable = sourceInteractable.pointable;
4868
4869
  }
4869
- if (typeof sourceInteractable.grabMode === "string") {
4870
- interactable.grabMode = sourceInteractable.grabMode;
4871
- }
4872
4870
  if (typeof sourceInteractable.grabReach === "string") {
4873
4871
  interactable.grabReach = sourceInteractable.grabReach;
4874
4872
  }
@@ -4927,8 +4925,16 @@ function restorePostCreateComponents(world, objectManagementSystem, targetEntity
4927
4925
  z: typeof grabRot.z === "number" ? grabRot.z : 0
4928
4926
  };
4929
4927
  }
4930
- if (typeof sourceInteractable.highlightOnHover === "boolean") {
4931
- interactable.highlightOnHover = sourceInteractable.highlightOnHover;
4928
+ if (typeof sourceInteractable.hoverBehavior === "string") {
4929
+ interactable.hoverBehavior = sourceInteractable.hoverBehavior;
4930
+ } else if (sourceInteractable.highlightOnHover === false) {
4931
+ interactable.hoverBehavior = "none";
4932
+ }
4933
+ if (typeof sourceInteractable.hoverColor === "string") {
4934
+ interactable.hoverColor = sourceInteractable.hoverColor;
4935
+ }
4936
+ if (typeof sourceInteractable.hoverScale === "number") {
4937
+ interactable.hoverScale = sourceInteractable.hoverScale;
4932
4938
  }
4933
4939
  }
4934
4940
  }
@@ -7445,6 +7451,20 @@ var _PostprocessingSyncSystem = class _PostprocessingSyncSystem {
7445
7451
  __name(_PostprocessingSyncSystem, "PostprocessingSyncSystem");
7446
7452
  var PostprocessingSyncSystem = _PostprocessingSyncSystem;
7447
7453
  var MIN_SIZE = 1e-4;
7454
+ function toBodyType(RAPIER, bodyType) {
7455
+ switch (bodyType) {
7456
+ case "fixed":
7457
+ return RAPIER.RigidBodyType.Fixed;
7458
+ case "kinematicPosition":
7459
+ return RAPIER.RigidBodyType.KinematicPositionBased;
7460
+ case "kinematicVelocity":
7461
+ return RAPIER.RigidBodyType.KinematicVelocityBased;
7462
+ case "dynamic":
7463
+ default:
7464
+ return RAPIER.RigidBodyType.Dynamic;
7465
+ }
7466
+ }
7467
+ __name(toBodyType, "toBodyType");
7448
7468
  function createRigidBodyDesc(RAPIER, bodyType) {
7449
7469
  if (bodyType === "fixed") {
7450
7470
  return RAPIER.RigidBodyDesc.fixed();
@@ -7743,7 +7763,8 @@ var _PhysicsSyncSystem = class _PhysicsSyncSystem {
7743
7763
  setPhysicsLiveRef(physics, "collider", null);
7744
7764
  continue;
7745
7765
  }
7746
- const rigidBodySignature = JSON.stringify(physics.values.rigidbody);
7766
+ const { type: _desiredBodyType, ...rigidBodyRebuildValues } = physics.values.rigidbody;
7767
+ const rigidBodySignature = JSON.stringify(rigidBodyRebuildValues);
7747
7768
  const previousRigidBodySignature = this.rigidBodySignatures.get(entityId);
7748
7769
  const hasRigidBody = this.rigidBodyHandles.has(entityId);
7749
7770
  const shouldRecreateBody = !hasRigidBody || previousRigidBodySignature !== rigidBodySignature;
@@ -7798,6 +7819,25 @@ var _PhysicsSyncSystem = class _PhysicsSyncSystem {
7798
7819
  continue;
7799
7820
  }
7800
7821
  setPhysicsLiveRef(physics, "rigidBody", body);
7822
+ const desiredBodyType = this.isEditorMode ? "fixed" : physics.values.rigidbody.type;
7823
+ const desiredRapierType = toBodyType(this.rapier, desiredBodyType);
7824
+ if (body.bodyType() !== desiredRapierType) {
7825
+ body.setBodyType(desiredRapierType, true);
7826
+ if (!this.isEditorMode && desiredBodyType === "dynamic") {
7827
+ body.setAdditionalMass(physics.values.rigidbody.mass, true);
7828
+ }
7829
+ }
7830
+ const pending = physics.pendingVelocity;
7831
+ if (pending) {
7832
+ setPhysicsLiveRef(physics, "pendingVelocity", null);
7833
+ if (!this.isEditorMode && desiredBodyType === "dynamic") {
7834
+ try {
7835
+ body.setLinvel(pending.linear, true);
7836
+ body.setAngvel(pending.angular, true);
7837
+ } catch {
7838
+ }
7839
+ }
7840
+ }
7801
7841
  if (this.isEditorMode || physics.values.rigidbody.type !== "dynamic") {
7802
7842
  syncBodyFromTransform(body, transform);
7803
7843
  }
@@ -11054,6 +11094,8 @@ var _XRInteractionSystem = class _XRInteractionSystem {
11054
11094
  this.raycaster = new THREE51__namespace.Raycaster();
11055
11095
  this.grabs = /* @__PURE__ */ new Map();
11056
11096
  this.hoveredEntityId = null;
11097
+ /** Per-hand hover, with the hit distance so the ray can stop at the surface. */
11098
+ this.hovers = /* @__PURE__ */ new Map();
11057
11099
  /** Monotonic seconds, accumulated from deltaTime for velocity sampling. */
11058
11100
  this.elapsed = 0;
11059
11101
  this.inputSystem = inputSystem;
@@ -11067,6 +11109,7 @@ var _XRInteractionSystem = class _XRInteractionSystem {
11067
11109
  const inputs = this.inputSystem.getInputs();
11068
11110
  const interactables = this.collectInteractables(world);
11069
11111
  this.hoveredEntityId = null;
11112
+ this.hovers.clear();
11070
11113
  for (const input of inputs) {
11071
11114
  if (!input.connected) {
11072
11115
  this.release(world, input);
@@ -11084,6 +11127,10 @@ var _XRInteractionSystem = class _XRInteractionSystem {
11084
11127
  if (this.hoveredEntityId === null) {
11085
11128
  this.hoveredEntityId = candidate.entityId;
11086
11129
  }
11130
+ this.hovers.set(input, {
11131
+ entityId: candidate.entityId,
11132
+ distance: candidate.distance
11133
+ });
11087
11134
  const interactable = world.getComponent(
11088
11135
  candidate.entityId,
11089
11136
  XRInteractableComponent
@@ -11156,7 +11203,7 @@ var _XRInteractionSystem = class _XRInteractionSystem {
11156
11203
  transform.position.x - hand.x,
11157
11204
  transform.position.y - hand.y,
11158
11205
  transform.position.z - hand.z
11159
- );
11206
+ ).applyQuaternion(handQuaternion.clone().invert());
11160
11207
  record.handRotationOffset = handQuaternion.clone().invert().multiply(objectQuaternion);
11161
11208
  record.remote = false;
11162
11209
  record.tracker.clear();
@@ -11267,13 +11314,22 @@ var _XRInteractionSystem = class _XRInteractionSystem {
11267
11314
  }
11268
11315
  }
11269
11316
  if (nearestId !== null) {
11270
- return { entityId: nearestId, viaDistance: false };
11317
+ return {
11318
+ entityId: nearestId,
11319
+ viaDistance: false,
11320
+ distance: nearestDistance
11321
+ };
11271
11322
  }
11272
- const hitId = this.raycast(input, interactables);
11273
- if (hitId === null) return null;
11323
+ const hit = this.raycast(input, interactables);
11324
+ if (hit === null) return null;
11325
+ const hitId = hit.entityId;
11274
11326
  const interactable = world.getComponent(hitId, XRInteractableComponent);
11275
11327
  const viaDistanceAllowed = interactable?.grabReach === "distance" || interactable?.grabReach === "both";
11276
- return { entityId: hitId, viaDistance: viaDistanceAllowed };
11328
+ return {
11329
+ entityId: hitId,
11330
+ viaDistance: viaDistanceAllowed,
11331
+ distance: hit.distance
11332
+ };
11277
11333
  }
11278
11334
  collectInteractables(world) {
11279
11335
  const out = [];
@@ -11304,7 +11360,7 @@ var _XRInteractionSystem = class _XRInteractionSystem {
11304
11360
  nearestId = entry.entityId;
11305
11361
  }
11306
11362
  }
11307
- return nearestId;
11363
+ return nearestId === null ? null : { entityId: nearestId, distance: nearestDistance };
11308
11364
  }
11309
11365
  /**
11310
11366
  * Pick the authored hold this hand should take.
@@ -11379,7 +11435,7 @@ var _XRInteractionSystem = class _XRInteractionSystem {
11379
11435
  transform.position.x - hand.x,
11380
11436
  transform.position.y - hand.y,
11381
11437
  transform.position.z - hand.z
11382
- );
11438
+ ).applyQuaternion(handQuaternion.clone().invert());
11383
11439
  }
11384
11440
  const physics = world.getComponent(entityId, PhysicsComponent);
11385
11441
  let restoreBodyType = null;
@@ -11514,9 +11570,10 @@ var _XRInteractionSystem = class _XRInteractionSystem {
11514
11570
  transform.position.z = hand.z - anchor.z;
11515
11571
  writeEuler(transform.rotation, objectQuaternion);
11516
11572
  } 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;
11573
+ const carried = held.offset.clone().applyQuaternion(handQuaternion);
11574
+ transform.position.x = hand.x + carried.x;
11575
+ transform.position.y = hand.y + carried.y;
11576
+ transform.position.z = hand.z + carried.z;
11520
11577
  writeEuler(transform.rotation, objectQuaternion);
11521
11578
  }
11522
11579
  held.tracker.record(
@@ -11569,19 +11626,14 @@ var _XRInteractionSystem = class _XRInteractionSystem {
11569
11626
  const physics = world.getComponent(held.entityId, PhysicsComponent);
11570
11627
  if (held.restoreBodyType !== null && physics) {
11571
11628
  physics.values.rigidbody.type = held.restoreBodyType;
11572
- const body = physics.rigidBody;
11573
11629
  const dynamic = held.restoreBodyType === "dynamic";
11574
- if (body && dynamic && interactable?.throwOnRelease) {
11630
+ if (dynamic && interactable?.throwOnRelease) {
11575
11631
  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
- }
11632
+ const spin = held.angularVelocity;
11633
+ setPhysicsLiveRef(physics, "pendingVelocity", {
11634
+ linear: { x: velocity.x, y: velocity.y, z: velocity.z },
11635
+ angular: { x: spin.x, y: spin.y, z: spin.z }
11636
+ });
11585
11637
  }
11586
11638
  }
11587
11639
  held.tracker.clear();
@@ -11629,6 +11681,30 @@ var _XRInteractionSystem = class _XRInteractionSystem {
11629
11681
  getHoveredEntityId() {
11630
11682
  return this.hoveredEntityId;
11631
11683
  }
11684
+ /**
11685
+ * What this hand is pointing at, with the distance to it.
11686
+ *
11687
+ * The distance is what lets the pointer ray stop at the surface instead of
11688
+ * passing through the object.
11689
+ */
11690
+ getHoverFor(handedness) {
11691
+ for (const [input, hover] of this.hovers) {
11692
+ if (input.handedness === handedness) return hover;
11693
+ }
11694
+ return null;
11695
+ }
11696
+ /** Every entity under a pointer this frame — drives hover feedback. */
11697
+ getHoveredEntityIds() {
11698
+ const ids = /* @__PURE__ */ new Set();
11699
+ for (const hover of this.hovers.values()) ids.add(hover.entityId);
11700
+ return ids;
11701
+ }
11702
+ /** Entities currently held, so hover feedback can skip them. */
11703
+ getHeldEntityIds() {
11704
+ const ids = /* @__PURE__ */ new Set();
11705
+ for (const record of this.grabs.values()) ids.add(record.entityId);
11706
+ return ids;
11707
+ }
11632
11708
  /** True when any source is holding something. */
11633
11709
  isGrabbing() {
11634
11710
  return this.grabs.size > 0;
@@ -12189,6 +12265,9 @@ function applyHandMaterial(root, material) {
12189
12265
  });
12190
12266
  }
12191
12267
  __name(applyHandMaterial, "applyHandMaterial");
12268
+
12269
+ // core/systems/XRControllerVisualSystem.ts
12270
+ var RAY_LENGTH = 5;
12192
12271
  var SLOTS = [
12193
12272
  { index: 0, side: "left" },
12194
12273
  { index: 1, side: "right" }
@@ -12227,6 +12306,7 @@ var _XRControllerVisualSystem = class _XRControllerVisualSystem {
12227
12306
  visuals.controllerModel.visible = showControllers && hasController;
12228
12307
  visuals.handModel.visible = showHands && hasHand;
12229
12308
  visuals.ray.visible = this.shouldShowRay(interactor);
12309
+ this.fitRayToHover(visuals.ray, side);
12230
12310
  if (showHands && this.handMaterial) {
12231
12311
  applyHandMaterial(visuals.handModel, this.handMaterial);
12232
12312
  }
@@ -12307,6 +12387,18 @@ var _XRControllerVisualSystem = class _XRControllerVisualSystem {
12307
12387
  if (!this.interactionSystem) return true;
12308
12388
  return this.interactionSystem.getHoveredEntityId() !== null || this.interactionSystem.isGrabbing();
12309
12389
  }
12390
+ /**
12391
+ * Stop the ray at whatever it is pointing at.
12392
+ *
12393
+ * A fixed-length ray drawn with depth testing off punches straight through the
12394
+ * object, so the user cannot tell where the pointer actually lands. Scaling it
12395
+ * to the hit distance turns it into a pointer that meets the surface.
12396
+ */
12397
+ fitRayToHover(ray, side) {
12398
+ const hover = this.interactionSystem?.getHoverFor(side) ?? null;
12399
+ const length = hover && Number.isFinite(hover.distance) && hover.distance > 0 ? Math.min(hover.distance, RAY_LENGTH) : RAY_LENGTH;
12400
+ ray.scale.z = length / RAY_LENGTH;
12401
+ }
12310
12402
  /** Lazily build per-slot visuals; the device reports profiles late. */
12311
12403
  ensureSlots(world) {
12312
12404
  const renderer = world.getRenderer();
@@ -12348,7 +12440,7 @@ var _XRControllerVisualSystem = class _XRControllerVisualSystem {
12348
12440
  createRay() {
12349
12441
  const geometry = new THREE51__namespace.BufferGeometry().setFromPoints([
12350
12442
  new THREE51__namespace.Vector3(0, 0, 0),
12351
- new THREE51__namespace.Vector3(0, 0, -5)
12443
+ new THREE51__namespace.Vector3(0, 0, -RAY_LENGTH)
12352
12444
  ]);
12353
12445
  const material = new THREE51__namespace.LineBasicMaterial({
12354
12446
  color: "#4da3ff",
@@ -13011,6 +13103,152 @@ var _XRHandPoseSystem = class _XRHandPoseSystem {
13011
13103
  };
13012
13104
  __name(_XRHandPoseSystem, "XRHandPoseSystem");
13013
13105
  var XRHandPoseSystem = _XRHandPoseSystem;
13106
+ var XR_HOVER_SHELL_FLAG = "__xrHoverShell";
13107
+ var SHELL_INFLATE = 1.04;
13108
+ var SCALE_SMOOTH_SECONDS = 0.09;
13109
+ var _XRHoverVisualSystem = class _XRHoverVisualSystem {
13110
+ constructor(interactionSystem) {
13111
+ this.records = /* @__PURE__ */ new Map();
13112
+ this.interactionSystem = interactionSystem;
13113
+ }
13114
+ update(world, deltaTime) {
13115
+ if (!world.isXrPresenting()) {
13116
+ this.clearAll(world);
13117
+ return;
13118
+ }
13119
+ const hovered = this.interactionSystem.getHoveredEntityIds();
13120
+ const held = this.interactionSystem.getHeldEntityIds();
13121
+ for (const entityId of hovered) {
13122
+ if (held.has(entityId)) continue;
13123
+ const interactable = world.getComponent(entityId, XRInteractableComponent);
13124
+ if (!interactable?.enabled) continue;
13125
+ if (interactable.hoverBehavior === "none") continue;
13126
+ this.begin(world, entityId, interactable);
13127
+ }
13128
+ for (const [entityId, record] of this.records) {
13129
+ if (!hovered.has(entityId) || held.has(entityId)) {
13130
+ record.active = false;
13131
+ }
13132
+ }
13133
+ this.animate(world, deltaTime);
13134
+ }
13135
+ begin(world, entityId, interactable) {
13136
+ const existing = this.records.get(entityId);
13137
+ if (existing) {
13138
+ existing.active = true;
13139
+ return;
13140
+ }
13141
+ const transform = world.getComponent(entityId, TransformComponent);
13142
+ if (!transform) return;
13143
+ const wantsColor = interactable.hoverBehavior === "color" || interactable.hoverBehavior === "both";
13144
+ this.records.set(entityId, {
13145
+ shell: wantsColor ? this.addShell(world, entityId, interactable) : null,
13146
+ baseScale: new THREE51__namespace.Vector3(
13147
+ transform.scale.x,
13148
+ transform.scale.y,
13149
+ transform.scale.z
13150
+ ),
13151
+ progress: 0,
13152
+ active: true
13153
+ });
13154
+ }
13155
+ /** Build an inflated back-face copy of the object's meshes. */
13156
+ addShell(world, entityId, interactable) {
13157
+ const object = world.getComponent(entityId, Object3DRef)?.object;
13158
+ if (!object) return null;
13159
+ const material = new THREE51__namespace.MeshBasicMaterial({
13160
+ color: new THREE51__namespace.Color(interactable.hoverColor),
13161
+ // Back faces only, so the shell shows just outside the silhouette instead
13162
+ // of covering the object.
13163
+ side: THREE51__namespace.BackSide,
13164
+ transparent: true,
13165
+ opacity: 0.85,
13166
+ depthWrite: false
13167
+ });
13168
+ const group = new THREE51__namespace.Group();
13169
+ group.name = "XRHoverShell";
13170
+ group.userData[XR_HOVER_SHELL_FLAG] = true;
13171
+ group.renderOrder = 998;
13172
+ object.traverse((child) => {
13173
+ if (!(child instanceof THREE51__namespace.Mesh) || !child.geometry) return;
13174
+ if (child.userData[XR_HOVER_SHELL_FLAG]) return;
13175
+ const clone3 = new THREE51__namespace.Mesh(child.geometry, material);
13176
+ child.updateWorldMatrix(true, false);
13177
+ object.updateWorldMatrix(true, false);
13178
+ clone3.matrixAutoUpdate = false;
13179
+ clone3.matrix.copy(object.matrixWorld).invert().multiply(child.matrixWorld).scale(new THREE51__namespace.Vector3(SHELL_INFLATE, SHELL_INFLATE, SHELL_INFLATE));
13180
+ clone3.userData[XR_HOVER_SHELL_FLAG] = true;
13181
+ clone3.raycast = () => {
13182
+ };
13183
+ group.add(clone3);
13184
+ });
13185
+ if (group.children.length === 0) {
13186
+ material.dispose();
13187
+ return null;
13188
+ }
13189
+ object.add(group);
13190
+ return group;
13191
+ }
13192
+ animate(world, deltaTime) {
13193
+ const step = 1 - Math.exp(-Math.max(0, deltaTime) / SCALE_SMOOTH_SECONDS);
13194
+ for (const [entityId, record] of Array.from(this.records)) {
13195
+ const interactable = world.getComponent(entityId, XRInteractableComponent);
13196
+ const target = record.active ? 1 : 0;
13197
+ record.progress += (target - record.progress) * step;
13198
+ const wantsScale = interactable?.hoverBehavior === "scale" || interactable?.hoverBehavior === "both";
13199
+ if (wantsScale) {
13200
+ const transform = world.getComponent(entityId, TransformComponent);
13201
+ if (transform) {
13202
+ const factor = 1 + (interactable.hoverScale - 1) * Math.min(1, record.progress);
13203
+ transform.scale.x = record.baseScale.x * factor;
13204
+ transform.scale.y = record.baseScale.y * factor;
13205
+ transform.scale.z = record.baseScale.z * factor;
13206
+ }
13207
+ }
13208
+ if (!record.active && record.progress < 0.01) {
13209
+ this.finish(world, entityId, record);
13210
+ }
13211
+ }
13212
+ }
13213
+ /** Remove the effect and restore the authored scale exactly. */
13214
+ finish(world, entityId, record) {
13215
+ const transform = world.getComponent(entityId, TransformComponent);
13216
+ if (transform) {
13217
+ transform.scale.x = record.baseScale.x;
13218
+ transform.scale.y = record.baseScale.y;
13219
+ transform.scale.z = record.baseScale.z;
13220
+ }
13221
+ this.disposeShell(record.shell);
13222
+ this.records.delete(entityId);
13223
+ }
13224
+ disposeShell(shell) {
13225
+ if (!shell) return;
13226
+ shell.parent?.remove(shell);
13227
+ shell.traverse((child) => {
13228
+ if (child instanceof THREE51__namespace.Mesh) {
13229
+ const material = child.material;
13230
+ if (Array.isArray(material)) {
13231
+ for (const entry of material) entry.dispose();
13232
+ } else {
13233
+ material?.dispose();
13234
+ }
13235
+ }
13236
+ });
13237
+ }
13238
+ clearAll(world) {
13239
+ for (const [entityId, record] of Array.from(this.records)) {
13240
+ this.finish(world, entityId, record);
13241
+ }
13242
+ }
13243
+ dispose() {
13244
+ for (const record of this.records.values()) {
13245
+ this.disposeShell(record.shell);
13246
+ }
13247
+ this.records.clear();
13248
+ }
13249
+ };
13250
+ __name(_XRHoverVisualSystem, "XRHoverVisualSystem");
13251
+ var XRHoverVisualSystem = _XRHoverVisualSystem;
13014
13252
 
13015
13253
  // core/World.ts
13016
13254
  var _World = class _World {
@@ -14443,12 +14681,14 @@ exports.VideoSystem = VideoSystem;
14443
14681
  exports.World = World;
14444
14682
  exports.XRControllerVisualSystem = XRControllerVisualSystem;
14445
14683
  exports.XRHandPoseSystem = XRHandPoseSystem;
14684
+ exports.XRHoverVisualSystem = XRHoverVisualSystem;
14446
14685
  exports.XRInputSystem = XRInputSystem;
14447
14686
  exports.XRInteractionSystem = XRInteractionSystem;
14448
14687
  exports.XRLocomotionSystem = XRLocomotionSystem;
14449
14688
  exports.XRSessionSystem = XRSessionSystem;
14450
14689
  exports.XRTeleportVisualSystem = XRTeleportVisualSystem;
14451
14690
  exports.XR_FINGERS = XR_FINGERS;
14691
+ exports.XR_HOVER_SHELL_FLAG = XR_HOVER_SHELL_FLAG;
14452
14692
  exports.angularVelocityFrom = angularVelocityFrom;
14453
14693
  exports.applyHandMaterial = applyHandMaterial;
14454
14694
  exports.applyPrefabToSnapshotInstances = applyPrefabToSnapshotInstances;