@relu-interactives/spatial-ecs 0.3.20 → 0.4.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.
Files changed (77) hide show
  1. package/dist/{System-BhnLw0fZ.d.cts → System-QjjSc6LZ.d.cts} +14 -0
  2. package/dist/{System-BhnLw0fZ.d.ts → System-QjjSc6LZ.d.ts} +14 -0
  3. package/dist/{XRHandPose-CLl4MiHx.d.cts → XRHandPose-Capx-RTD.d.cts} +47 -2
  4. package/dist/{XRHandPose-CrzI57JA.d.ts → XRHandPose-Csd3QGkj.d.ts} +47 -2
  5. package/dist/{chunk-CDSE7476.js → chunk-24ZMSBR7.js} +540 -5
  6. package/dist/chunk-24ZMSBR7.js.map +1 -0
  7. package/dist/{chunk-D6KXWNYT.js → chunk-2GTFFLGG.js} +3 -3
  8. package/dist/{chunk-D6KXWNYT.js.map → chunk-2GTFFLGG.js.map} +1 -1
  9. package/dist/{chunk-P42OE3RU.js → chunk-OWQPGEBB.js} +4 -4
  10. package/dist/{chunk-P42OE3RU.js.map → chunk-OWQPGEBB.js.map} +1 -1
  11. package/dist/{chunk-H6DQ7JRE.js → chunk-ZWONFM2F.js} +33 -3
  12. package/dist/chunk-ZWONFM2F.js.map +1 -0
  13. package/dist/components/index.cjs +30 -0
  14. package/dist/components/index.cjs.map +1 -1
  15. package/dist/components/index.d.cts +3 -3
  16. package/dist/components/index.d.ts +3 -3
  17. package/dist/components/index.js +1 -1
  18. package/dist/docs/guide/accessing-components.md +7 -6
  19. package/dist/docs/guide/behaviour-data-binding/component-input.md +10 -6
  20. package/dist/docs/guide/behaviour-data-binding/entity-input.md +13 -5
  21. package/dist/docs/guide/camera-rigs.md +2 -2
  22. package/dist/docs/guide/components/animation.md +12 -5
  23. package/dist/docs/guide/components/audio.md +13 -6
  24. package/dist/docs/guide/components/camera.md +10 -4
  25. package/dist/docs/guide/components/entity-type.md +13 -6
  26. package/dist/docs/guide/components/image-target-anchor.md +6 -2
  27. package/dist/docs/guide/components/image-target.md +10 -5
  28. package/dist/docs/guide/components/image.md +10 -4
  29. package/dist/docs/guide/components/instance-slot.md +7 -3
  30. package/dist/docs/guide/components/instanced-mesh.md +15 -7
  31. package/dist/docs/guide/components/light.md +10 -4
  32. package/dist/docs/guide/components/material.md +14 -8
  33. package/dist/docs/guide/components/mesh-geometry.md +10 -4
  34. package/dist/docs/guide/components/mesh.md +10 -4
  35. package/dist/docs/guide/components/model.md +4 -2
  36. package/dist/docs/guide/components/name.md +11 -6
  37. package/dist/docs/guide/components/object3d-ref.md +7 -3
  38. package/dist/docs/guide/components/parent-id.md +10 -4
  39. package/dist/docs/guide/components/physics.md +12 -6
  40. package/dist/docs/guide/components/script.md +10 -4
  41. package/dist/docs/guide/components/sprite.md +10 -4
  42. package/dist/docs/guide/components/transform.md +6 -2
  43. package/dist/docs/guide/components/video.md +10 -4
  44. package/dist/docs/guide/prefabs.md +2 -0
  45. package/dist/docs/guide/runtime-entity-management.md +1 -1
  46. package/dist/docs/guide/spawning/instanced-mesh.md +11 -6
  47. package/dist/docs/guide/systems/input-system.md +4 -1
  48. package/dist/docs/guide/systems/script-object-system.md +9 -1
  49. package/dist/docs/guide/vr-and-hand-poses.md +7 -3
  50. package/dist/docs/guide/world-api.md +23 -15
  51. package/dist/factories/index.cjs +6 -0
  52. package/dist/factories/index.cjs.map +1 -1
  53. package/dist/factories/index.d.cts +2 -2
  54. package/dist/factories/index.d.ts +2 -2
  55. package/dist/factories/index.js +3 -3
  56. package/dist/{index-6l4hn2Sh.d.ts → index-BAeeVJ6f.d.ts} +94 -2
  57. package/dist/{index-g777AmFv.d.cts → index-BRATaqTT.d.ts} +78 -4
  58. package/dist/{index-JRcoel9F.d.ts → index-C1rprGz_.d.cts} +78 -4
  59. package/dist/{index-C2I5PxCI.d.cts → index-SC9Mgjht.d.cts} +94 -2
  60. package/dist/index.cjs +586 -0
  61. package/dist/index.cjs.map +1 -1
  62. package/dist/index.d.cts +7 -7
  63. package/dist/index.d.ts +7 -7
  64. package/dist/index.js +26 -8
  65. package/dist/index.js.map +1 -1
  66. package/dist/systems/index.cjs +566 -0
  67. package/dist/systems/index.cjs.map +1 -1
  68. package/dist/systems/index.d.cts +2 -2
  69. package/dist/systems/index.d.ts +2 -2
  70. package/dist/systems/index.js +3 -3
  71. package/dist/types.d.cts +1 -1
  72. package/dist/types.d.ts +1 -1
  73. package/dist/worldSnapshotUtils.d.cts +1 -1
  74. package/dist/worldSnapshotUtils.d.ts +1 -1
  75. package/package.json +4 -2
  76. package/dist/chunk-CDSE7476.js.map +0 -1
  77. package/dist/chunk-H6DQ7JRE.js.map +0 -1
package/dist/index.cjs CHANGED
@@ -2579,6 +2579,12 @@ var _XRRigComponent = class _XRRigComponent extends Component {
2579
2579
  this.teleportArcColor = options?.teleportArcColor ?? "#4da3ff";
2580
2580
  this.teleportArcInvalidColor = options?.teleportArcInvalidColor ?? "#ff5a5a";
2581
2581
  this.teleportArcThickness = options?.teleportArcThickness ?? 0.012;
2582
+ this.passthrough = options?.passthrough ?? false;
2583
+ this.depthOcclusion = options?.depthOcclusion ?? false;
2584
+ this.environmentPhysics = options?.environmentPhysics ?? "none";
2585
+ this.environmentDebug = options?.environmentDebug ?? false;
2586
+ this.hitTest = options?.hitTest ?? false;
2587
+ this.anchors = options?.anchors ?? false;
2582
2588
  }
2583
2589
  };
2584
2590
  __name(_XRRigComponent, "XRRigComponent");
@@ -4019,6 +4025,8 @@ __export(components_exports, {
4019
4025
  TransformComponent: () => TransformComponent,
4020
4026
  VRTeleportAreaComponent: () => VRTeleportAreaComponent,
4021
4027
  VideoComponent: () => VideoComponent,
4028
+ XRAnchorComponent: () => XRAnchorComponent,
4029
+ XREnvironmentComponent: () => XREnvironmentComponent,
4022
4030
  XRHandPoseComponent: () => XRHandPoseComponent,
4023
4031
  XRInteractableComponent: () => XRInteractableComponent,
4024
4032
  XRInteractorComponent: () => XRInteractorComponent,
@@ -4061,6 +4069,28 @@ var _PrefabNode = class _PrefabNode extends Component {
4061
4069
  __name(_PrefabNode, "PrefabNode");
4062
4070
  var PrefabNode = _PrefabNode;
4063
4071
 
4072
+ // core/components/XREnvironment.ts
4073
+ var _XREnvironmentComponent = class _XREnvironmentComponent extends Component {
4074
+ constructor(kind, semanticLabel = null, orientation = null) {
4075
+ super();
4076
+ this.kind = kind;
4077
+ this.semanticLabel = semanticLabel;
4078
+ this.orientation = orientation;
4079
+ }
4080
+ };
4081
+ __name(_XREnvironmentComponent, "XREnvironmentComponent");
4082
+ var XREnvironmentComponent = _XREnvironmentComponent;
4083
+
4084
+ // core/components/XRAnchor.ts
4085
+ var _XRAnchorComponent = class _XRAnchorComponent extends Component {
4086
+ constructor(options) {
4087
+ super();
4088
+ this.enabled = options?.enabled ?? true;
4089
+ }
4090
+ };
4091
+ __name(_XRAnchorComponent, "XRAnchorComponent");
4092
+ var XRAnchorComponent = _XRAnchorComponent;
4093
+
4064
4094
  // core/factories/objects/ModelFactory.ts
4065
4095
  function getModelFileName(path) {
4066
4096
  const segments = path.split(/[\\/]/).filter(Boolean);
@@ -4876,6 +4906,24 @@ function restorePostCreateComponents(world, objectManagementSystem, targetEntity
4876
4906
  if (typeof sourceXrRig.fallbackEyeHeight === "number") {
4877
4907
  rig.fallbackEyeHeight = sourceXrRig.fallbackEyeHeight;
4878
4908
  }
4909
+ if (typeof sourceXrRig.passthrough === "boolean") {
4910
+ rig.passthrough = sourceXrRig.passthrough;
4911
+ }
4912
+ if (typeof sourceXrRig.depthOcclusion === "boolean") {
4913
+ rig.depthOcclusion = sourceXrRig.depthOcclusion;
4914
+ }
4915
+ if (typeof sourceXrRig.environmentPhysics === "string") {
4916
+ rig.environmentPhysics = sourceXrRig.environmentPhysics;
4917
+ }
4918
+ if (typeof sourceXrRig.environmentDebug === "boolean") {
4919
+ rig.environmentDebug = sourceXrRig.environmentDebug;
4920
+ }
4921
+ if (typeof sourceXrRig.hitTest === "boolean") {
4922
+ rig.hitTest = sourceXrRig.hitTest;
4923
+ }
4924
+ if (typeof sourceXrRig.anchors === "boolean") {
4925
+ rig.anchors = sourceXrRig.anchors;
4926
+ }
4879
4927
  }
4880
4928
  }
4881
4929
  {
@@ -7243,6 +7291,8 @@ var _EnvironmentSyncSystem = class _EnvironmentSyncSystem {
7243
7291
  this.signature = null;
7244
7292
  this.activeTexture = null;
7245
7293
  this.pendingImageUrl = null;
7294
+ /** Whether the skybox is currently withheld because passthrough is on. */
7295
+ this.passthroughSuppressed = false;
7246
7296
  this.scene = scene;
7247
7297
  }
7248
7298
  disposeActiveTexture() {
@@ -7327,6 +7377,18 @@ var _EnvironmentSyncSystem = class _EnvironmentSyncSystem {
7327
7377
  this.scene.fog = new THREE51__namespace.FogExp2(fogColor, density);
7328
7378
  }
7329
7379
  update(world) {
7380
+ const passthrough = world.isXrPassthrough();
7381
+ if (passthrough !== this.passthroughSuppressed) {
7382
+ this.passthroughSuppressed = passthrough;
7383
+ if (passthrough) {
7384
+ this.scene.background = null;
7385
+ } else {
7386
+ this.signature = null;
7387
+ }
7388
+ }
7389
+ if (passthrough) {
7390
+ return;
7391
+ }
7330
7392
  const entityIds = world.query(Environment);
7331
7393
  if (entityIds.length === 0) {
7332
7394
  if (this.signature !== null) {
@@ -12350,6 +12412,509 @@ var _XRSessionSystem = class _XRSessionSystem {
12350
12412
  };
12351
12413
  __name(_XRSessionSystem, "XRSessionSystem");
12352
12414
  var XRSessionSystem = _XRSessionSystem;
12415
+ var PLANE_COLLIDER_THICKNESS = 0.02;
12416
+ var MIN_PLANE_EXTENT = 0.01;
12417
+ function planeColliderFromPolygon(polygon) {
12418
+ if (!polygon || polygon.length < 3) return null;
12419
+ let minX = Number.POSITIVE_INFINITY;
12420
+ let maxX = Number.NEGATIVE_INFINITY;
12421
+ let minZ = Number.POSITIVE_INFINITY;
12422
+ let maxZ = Number.NEGATIVE_INFINITY;
12423
+ for (const point of polygon) {
12424
+ if (!Number.isFinite(point.x) || !Number.isFinite(point.z)) return null;
12425
+ minX = Math.min(minX, point.x);
12426
+ maxX = Math.max(maxX, point.x);
12427
+ minZ = Math.min(minZ, point.z);
12428
+ maxZ = Math.max(maxZ, point.z);
12429
+ }
12430
+ const width = maxX - minX;
12431
+ const depth = maxZ - minZ;
12432
+ if (width < MIN_PLANE_EXTENT || depth < MIN_PLANE_EXTENT) return null;
12433
+ return {
12434
+ // Thin on Y: that is the plane's normal in plane space.
12435
+ size: { x: width, y: PLANE_COLLIDER_THICKNESS, z: depth },
12436
+ // The polygon is not necessarily centred on its own origin, so the box has
12437
+ // to be offset to sit over it. Pushed DOWN half its thickness so the
12438
+ // collider's top face lines up with the real surface — otherwise objects
12439
+ // rest a centimetre above the floor, or sink a centimetre into it.
12440
+ offset: {
12441
+ x: (minX + maxX) / 2,
12442
+ y: -PLANE_COLLIDER_THICKNESS / 2,
12443
+ z: (minZ + maxZ) / 2
12444
+ }
12445
+ };
12446
+ }
12447
+ __name(planeColliderFromPolygon, "planeColliderFromPolygon");
12448
+ function writeXrMeshGeometry(vertices, indices, existing) {
12449
+ const geometry = existing ?? new THREE51__namespace.BufferGeometry();
12450
+ geometry.setAttribute("position", new THREE51__namespace.BufferAttribute(vertices, 3));
12451
+ geometry.setIndex(new THREE51__namespace.BufferAttribute(indices, 1));
12452
+ geometry.computeBoundingSphere();
12453
+ geometry.computeVertexNormals();
12454
+ return geometry;
12455
+ }
12456
+ __name(writeXrMeshGeometry, "writeXrMeshGeometry");
12457
+ function decomposeXrPose(matrixElements, target) {
12458
+ const matrix = new THREE51__namespace.Matrix4().fromArray(
12459
+ matrixElements
12460
+ );
12461
+ const position = new THREE51__namespace.Vector3();
12462
+ const quaternion = new THREE51__namespace.Quaternion();
12463
+ const scale = new THREE51__namespace.Vector3();
12464
+ matrix.decompose(position, quaternion, scale);
12465
+ const euler = new THREE51__namespace.Euler().setFromQuaternion(quaternion, "XYZ");
12466
+ target.position.x = position.x;
12467
+ target.position.y = position.y;
12468
+ target.position.z = position.z;
12469
+ target.rotation.x = euler.x;
12470
+ target.rotation.y = euler.y;
12471
+ target.rotation.z = euler.z;
12472
+ }
12473
+ __name(decomposeXrPose, "decomposeXrPose");
12474
+
12475
+ // core/systems/XREnvironmentSystem.ts
12476
+ var DEBUG_COLOR = 5088255;
12477
+ var _XREnvironmentSystem = class _XREnvironmentSystem {
12478
+ constructor() {
12479
+ /** Keyed by the XRPlane/XRMesh object itself — the runtime's own identity. */
12480
+ this.planes = /* @__PURE__ */ new Map();
12481
+ this.meshes = /* @__PURE__ */ new Map();
12482
+ this.presenting = false;
12483
+ this.debugVisible = false;
12484
+ }
12485
+ update(world) {
12486
+ const presenting = world.isXrPresenting();
12487
+ if (!presenting) {
12488
+ if (this.presenting) {
12489
+ this.presenting = false;
12490
+ this.clearAll(world);
12491
+ }
12492
+ return;
12493
+ }
12494
+ this.presenting = true;
12495
+ const rig = this.findRig(world);
12496
+ const mode = rig?.environmentPhysics ?? "none";
12497
+ if (mode === "none") {
12498
+ if (this.planes.size > 0 || this.meshes.size > 0) this.clearAll(world);
12499
+ return;
12500
+ }
12501
+ const debug = rig?.environmentDebug === true;
12502
+ if (debug !== this.debugVisible) {
12503
+ this.debugVisible = debug;
12504
+ this.applyDebugVisibility(world);
12505
+ }
12506
+ const xr = world.getRenderer().xr;
12507
+ const frame = xr.getFrame?.();
12508
+ const referenceSpace = xr.getReferenceSpace();
12509
+ if (!frame || !referenceSpace) return;
12510
+ const meshesAvailable = Boolean(frame.detectedMeshes);
12511
+ const useMesh = (mode === "mesh" || mode === "auto") && meshesAvailable;
12512
+ const usePlanes = mode === "planes" || mode === "auto" && !useMesh;
12513
+ if (usePlanes) {
12514
+ this.syncPlanes(world, frame, referenceSpace);
12515
+ } else if (this.planes.size > 0) {
12516
+ this.clearMap(world, this.planes);
12517
+ }
12518
+ if (useMesh) {
12519
+ this.syncMeshes(world, frame, referenceSpace);
12520
+ } else if (this.meshes.size > 0) {
12521
+ this.clearMap(world, this.meshes);
12522
+ }
12523
+ }
12524
+ /** How many pieces of real geometry are currently colliding. Script-facing. */
12525
+ getDetectedCount() {
12526
+ return { planes: this.planes.size, meshes: this.meshes.size };
12527
+ }
12528
+ findRig(world) {
12529
+ const ids = world.query(XRRigComponent);
12530
+ if (ids.length === 0) return null;
12531
+ return world.getComponent(ids[0], XRRigComponent) ?? null;
12532
+ }
12533
+ syncPlanes(world, frame, referenceSpace) {
12534
+ const detected = frame.detectedPlanes;
12535
+ if (!detected) return;
12536
+ for (const [plane, entry] of this.planes) {
12537
+ if (!detected.has(plane)) {
12538
+ this.destroyEntry(world, entry);
12539
+ this.planes.delete(plane);
12540
+ }
12541
+ }
12542
+ for (const plane of detected) {
12543
+ const pose = frame.getPose(plane.planeSpace, referenceSpace);
12544
+ if (!pose) continue;
12545
+ let entry = this.planes.get(plane);
12546
+ const changed = plane.lastChangedTime ?? 0;
12547
+ if (!entry) {
12548
+ const created = this.createPlaneEntity(world, plane);
12549
+ if (!created) continue;
12550
+ entry = created;
12551
+ this.planes.set(plane, entry);
12552
+ } else if (changed !== entry.lastChangedTime) {
12553
+ entry.lastChangedTime = changed;
12554
+ this.applyPlaneCollider(world, entry.entityId, plane);
12555
+ }
12556
+ this.applyPose(world, entry.entityId, pose);
12557
+ }
12558
+ }
12559
+ syncMeshes(world, frame, referenceSpace) {
12560
+ const detected = frame.detectedMeshes;
12561
+ if (!detected) return;
12562
+ for (const [mesh, entry] of this.meshes) {
12563
+ if (!detected.has(mesh)) {
12564
+ this.destroyEntry(world, entry);
12565
+ this.meshes.delete(mesh);
12566
+ }
12567
+ }
12568
+ for (const mesh of detected) {
12569
+ const pose = frame.getPose(mesh.meshSpace, referenceSpace);
12570
+ if (!pose) continue;
12571
+ let entry = this.meshes.get(mesh);
12572
+ const changed = mesh.lastChangedTime ?? 0;
12573
+ if (!entry) {
12574
+ const created = this.createMeshEntity(world, mesh);
12575
+ if (!created) continue;
12576
+ entry = created;
12577
+ this.meshes.set(mesh, entry);
12578
+ } else if (changed !== entry.lastChangedTime) {
12579
+ entry.lastChangedTime = changed;
12580
+ writeXrMeshGeometry(mesh.vertices, mesh.indices, entry.geometry);
12581
+ }
12582
+ this.applyPose(world, entry.entityId, pose);
12583
+ }
12584
+ }
12585
+ createPlaneEntity(world, plane) {
12586
+ const collider = planeColliderFromPolygon(plane.polygon);
12587
+ if (!collider) return null;
12588
+ const entityId = world.createEntity();
12589
+ world.addComponents(entityId, [
12590
+ new TransformComponent(),
12591
+ new XREnvironmentComponent(
12592
+ "plane",
12593
+ plane.semanticLabel ?? null,
12594
+ normalizeOrientation(plane.orientation)
12595
+ )
12596
+ ]);
12597
+ this.applyPlaneCollider(world, entityId, plane);
12598
+ const entry = {
12599
+ entityId,
12600
+ lastChangedTime: plane.lastChangedTime ?? 0,
12601
+ geometry: null,
12602
+ material: null
12603
+ };
12604
+ if (this.debugVisible) this.attachPlaneDebug(world, entry, collider);
12605
+ return entry;
12606
+ }
12607
+ applyPlaneCollider(world, entityId, plane) {
12608
+ const collider = planeColliderFromPolygon(plane.polygon);
12609
+ if (!collider) return;
12610
+ const existing = world.getComponent(entityId, PhysicsComponent);
12611
+ const values = {
12612
+ // Fixed: the real room does not move because something hit it.
12613
+ rigidbody: { type: "fixed" },
12614
+ collider: {
12615
+ shape: "cuboid",
12616
+ size: collider.size,
12617
+ offsetPosition: collider.offset
12618
+ }
12619
+ };
12620
+ if (existing) {
12621
+ existing.setValues(values);
12622
+ } else {
12623
+ world.addComponent(entityId, new PhysicsComponent(values));
12624
+ }
12625
+ }
12626
+ createMeshEntity(world, mesh) {
12627
+ if (!mesh.vertices?.length || !mesh.indices?.length) return null;
12628
+ const geometry = writeXrMeshGeometry(mesh.vertices, mesh.indices);
12629
+ const material = new THREE51__namespace.MeshBasicMaterial({
12630
+ color: DEBUG_COLOR,
12631
+ wireframe: true
12632
+ });
12633
+ const root = new THREE51__namespace.Group();
12634
+ const child = new THREE51__namespace.Mesh(geometry, material);
12635
+ child.visible = this.debugVisible;
12636
+ root.add(child);
12637
+ const entityId = world.createEntity();
12638
+ world.addComponents(entityId, [
12639
+ new TransformComponent(),
12640
+ new Object3DRef(root),
12641
+ new XREnvironmentComponent("mesh", mesh.semanticLabel ?? null, null),
12642
+ new PhysicsComponent({
12643
+ rigidbody: { type: "fixed" },
12644
+ collider: { shape: "trimesh" }
12645
+ })
12646
+ ]);
12647
+ return {
12648
+ entityId,
12649
+ lastChangedTime: mesh.lastChangedTime ?? 0,
12650
+ geometry,
12651
+ material
12652
+ };
12653
+ }
12654
+ /** A thin box so a detected plane can be SEEN, not just collided with. */
12655
+ attachPlaneDebug(world, entry, collider) {
12656
+ const geometry = new THREE51__namespace.BoxGeometry(
12657
+ collider.size.x,
12658
+ collider.size.y,
12659
+ collider.size.z
12660
+ );
12661
+ const material = new THREE51__namespace.MeshBasicMaterial({
12662
+ color: DEBUG_COLOR,
12663
+ wireframe: true
12664
+ });
12665
+ const root = new THREE51__namespace.Group();
12666
+ root.add(new THREE51__namespace.Mesh(geometry, material));
12667
+ entry.geometry = geometry;
12668
+ entry.material = material;
12669
+ world.addComponent(entry.entityId, new Object3DRef(root));
12670
+ }
12671
+ applyPose(world, entityId, pose) {
12672
+ const transform = world.getComponent(entityId, TransformComponent);
12673
+ if (!transform) return;
12674
+ decomposeXrPose(pose.transform.matrix, transform);
12675
+ }
12676
+ applyDebugVisibility(world) {
12677
+ for (const map of [this.planes, this.meshes]) {
12678
+ for (const entry of map.values()) {
12679
+ const ref = world.getComponent(entry.entityId, Object3DRef);
12680
+ ref?.object.traverse((child) => {
12681
+ if (child.isMesh) child.visible = this.debugVisible;
12682
+ });
12683
+ }
12684
+ }
12685
+ }
12686
+ destroyEntry(world, entry) {
12687
+ entry.geometry?.dispose();
12688
+ entry.material?.dispose();
12689
+ world.destroyEntity(entry.entityId);
12690
+ }
12691
+ clearMap(world, map) {
12692
+ for (const entry of map.values()) this.destroyEntry(world, entry);
12693
+ map.clear();
12694
+ }
12695
+ clearAll(world) {
12696
+ this.clearMap(world, this.planes);
12697
+ this.clearMap(world, this.meshes);
12698
+ }
12699
+ dispose(world) {
12700
+ this.clearAll(world);
12701
+ }
12702
+ };
12703
+ __name(_XREnvironmentSystem, "XREnvironmentSystem");
12704
+ var XREnvironmentSystem = _XREnvironmentSystem;
12705
+ function normalizeOrientation(value) {
12706
+ return value === "horizontal" || value === "vertical" ? value : null;
12707
+ }
12708
+ __name(normalizeOrientation, "normalizeOrientation");
12709
+ var _XRHitTestSystem = class _XRHitTestSystem {
12710
+ constructor() {
12711
+ this.sources = /* @__PURE__ */ new Map();
12712
+ /** Targets whose async request is in flight, so it is not fired twice. */
12713
+ this.pending = /* @__PURE__ */ new Set();
12714
+ this.results = /* @__PURE__ */ new Map();
12715
+ this.presenting = false;
12716
+ this.enabled = false;
12717
+ }
12718
+ update(world) {
12719
+ const presenting = world.isXrPresenting();
12720
+ if (!presenting) {
12721
+ if (this.presenting) {
12722
+ this.presenting = false;
12723
+ this.teardown();
12724
+ }
12725
+ return;
12726
+ }
12727
+ if (!this.presenting) {
12728
+ this.presenting = true;
12729
+ this.enabled = this.rigWantsHitTest(world);
12730
+ }
12731
+ if (!this.enabled) return;
12732
+ const xr = world.getRenderer().xr;
12733
+ const session = xr.getSession();
12734
+ const referenceSpace = xr.getReferenceSpace();
12735
+ const frame = xr.getFrame?.();
12736
+ if (!session || !referenceSpace || !frame) return;
12737
+ this.ensureViewerSource(session);
12738
+ this.ensureInputSources(session);
12739
+ this.results.clear();
12740
+ for (const [key, source] of this.sources) {
12741
+ const hits = frame.getHitTestResults(source);
12742
+ if (!hits || hits.length === 0) continue;
12743
+ const pose = hits[0].getPose(referenceSpace);
12744
+ if (!pose) continue;
12745
+ const matrix = new THREE51__namespace.Matrix4().fromArray(
12746
+ pose.transform.matrix
12747
+ );
12748
+ const position = new THREE51__namespace.Vector3();
12749
+ const quaternion = new THREE51__namespace.Quaternion();
12750
+ matrix.decompose(position, quaternion, new THREE51__namespace.Vector3());
12751
+ this.results.set(key, { position, quaternion });
12752
+ }
12753
+ }
12754
+ /**
12755
+ * Where the given ray meets the real world this frame, or null.
12756
+ *
12757
+ * Null is the normal case, not an error: the source may still be being
12758
+ * requested, the ray may point at nothing recognised, or the runtime may not
12759
+ * have granted `hit-test` at all.
12760
+ */
12761
+ getHitResult(target = "viewer") {
12762
+ return this.results.get(target) ?? null;
12763
+ }
12764
+ rigWantsHitTest(world) {
12765
+ const ids = world.query(XRRigComponent);
12766
+ if (ids.length === 0) return false;
12767
+ return world.getComponent(ids[0], XRRigComponent)?.hitTest === true;
12768
+ }
12769
+ ensureViewerSource(session) {
12770
+ if (this.sources.has("viewer") || this.pending.has("viewer")) return;
12771
+ if (typeof session.requestHitTestSource !== "function") return;
12772
+ this.pending.add("viewer");
12773
+ void session.requestReferenceSpace("viewer").then((space) => session.requestHitTestSource?.({ space })).then((source) => {
12774
+ if (source && this.presenting) this.sources.set("viewer", source);
12775
+ else source?.cancel?.();
12776
+ }).catch(() => {
12777
+ }).finally(() => this.pending.delete("viewer"));
12778
+ }
12779
+ ensureInputSources(session) {
12780
+ if (typeof session.requestHitTestSource !== "function") return;
12781
+ if (!session.inputSources) return;
12782
+ for (const input of session.inputSources) {
12783
+ const handedness = input.handedness;
12784
+ if (!handedness || handedness === "none") continue;
12785
+ if (!input.targetRaySpace) continue;
12786
+ if (this.sources.has(handedness) || this.pending.has(handedness)) continue;
12787
+ this.pending.add(handedness);
12788
+ const request = session.requestHitTestSource({
12789
+ space: input.targetRaySpace
12790
+ });
12791
+ if (!request) {
12792
+ this.pending.delete(handedness);
12793
+ continue;
12794
+ }
12795
+ void request.then((source) => {
12796
+ if (source && this.presenting) this.sources.set(handedness, source);
12797
+ else source?.cancel?.();
12798
+ }).catch(() => {
12799
+ }).finally(() => this.pending.delete(handedness));
12800
+ }
12801
+ }
12802
+ teardown() {
12803
+ for (const source of this.sources.values()) source.cancel?.();
12804
+ this.sources.clear();
12805
+ this.results.clear();
12806
+ this.pending.clear();
12807
+ this.enabled = false;
12808
+ }
12809
+ dispose() {
12810
+ this.presenting = false;
12811
+ this.teardown();
12812
+ }
12813
+ };
12814
+ __name(_XRHitTestSystem, "XRHitTestSystem");
12815
+ var XRHitTestSystem = _XRHitTestSystem;
12816
+ var _XRAnchorSystem = class _XRAnchorSystem {
12817
+ constructor() {
12818
+ this.anchors = /* @__PURE__ */ new Map();
12819
+ /** In-flight creations, so one entity is not anchored twice. */
12820
+ this.pending = /* @__PURE__ */ new Set();
12821
+ this.presenting = false;
12822
+ }
12823
+ update(world) {
12824
+ const presenting = world.isXrPresenting();
12825
+ if (!presenting) {
12826
+ if (this.presenting) {
12827
+ this.presenting = false;
12828
+ this.releaseAll();
12829
+ }
12830
+ return;
12831
+ }
12832
+ this.presenting = true;
12833
+ const xr = world.getRenderer().xr;
12834
+ const referenceSpace = xr.getReferenceSpace();
12835
+ const frame = xr.getFrame?.();
12836
+ if (!frame || !referenceSpace) return;
12837
+ const entityIds = world.query(XRAnchorComponent, TransformComponent);
12838
+ const live = new Set(entityIds);
12839
+ for (const [entityId, anchor] of this.anchors) {
12840
+ if (!live.has(entityId)) {
12841
+ anchor.delete?.();
12842
+ this.anchors.delete(entityId);
12843
+ }
12844
+ }
12845
+ for (const entityId of entityIds) {
12846
+ const component = world.getComponent(entityId, XRAnchorComponent);
12847
+ const transform = world.getComponent(entityId, TransformComponent);
12848
+ if (!component || !transform) continue;
12849
+ if (!component.enabled) {
12850
+ const existing = this.anchors.get(entityId);
12851
+ if (existing) {
12852
+ existing.delete?.();
12853
+ this.anchors.delete(entityId);
12854
+ }
12855
+ continue;
12856
+ }
12857
+ const anchor = this.anchors.get(entityId);
12858
+ if (!anchor) {
12859
+ this.requestAnchor(entityId, transform, frame, referenceSpace);
12860
+ continue;
12861
+ }
12862
+ const pose = frame.getPose(anchor.anchorSpace, referenceSpace);
12863
+ if (pose) decomposeXrPose(pose.transform.matrix, transform);
12864
+ }
12865
+ }
12866
+ /** Whether this entity's pose is currently being driven by an anchor. */
12867
+ isAnchored(entityId) {
12868
+ return this.anchors.has(entityId);
12869
+ }
12870
+ requestAnchor(entityId, transform, frame, referenceSpace) {
12871
+ if (this.pending.has(entityId)) return;
12872
+ if (typeof frame.createAnchor !== "function") return;
12873
+ const RigidTransform = globalThis.XRRigidTransform;
12874
+ if (!RigidTransform) return;
12875
+ const quaternion = new THREE51__namespace.Quaternion().setFromEuler(
12876
+ new THREE51__namespace.Euler(
12877
+ transform.rotation.x,
12878
+ transform.rotation.y,
12879
+ transform.rotation.z,
12880
+ "XYZ"
12881
+ )
12882
+ );
12883
+ const pose = new RigidTransform(
12884
+ {
12885
+ x: transform.position.x,
12886
+ y: transform.position.y,
12887
+ z: transform.position.z,
12888
+ w: 1
12889
+ },
12890
+ {
12891
+ x: quaternion.x,
12892
+ y: quaternion.y,
12893
+ z: quaternion.z,
12894
+ w: quaternion.w
12895
+ }
12896
+ );
12897
+ const request = frame.createAnchor(pose, referenceSpace);
12898
+ if (!request) return;
12899
+ this.pending.add(entityId);
12900
+ void request.then((anchor) => {
12901
+ if (anchor && this.presenting) this.anchors.set(entityId, anchor);
12902
+ else anchor?.delete?.();
12903
+ }).catch(() => {
12904
+ }).finally(() => this.pending.delete(entityId));
12905
+ }
12906
+ releaseAll() {
12907
+ for (const anchor of this.anchors.values()) anchor.delete?.();
12908
+ this.anchors.clear();
12909
+ this.pending.clear();
12910
+ }
12911
+ dispose() {
12912
+ this.presenting = false;
12913
+ this.releaseAll();
12914
+ }
12915
+ };
12916
+ __name(_XRAnchorSystem, "XRAnchorSystem");
12917
+ var XRAnchorSystem = _XRAnchorSystem;
12353
12918
  function buildGeometry2(area) {
12354
12919
  if (area.shape === "circle") {
12355
12920
  const radius = Math.max(5e-3, area.width / 2);
@@ -14006,6 +14571,24 @@ var _World = class _World {
14006
14571
  isXrPresenting() {
14007
14572
  return this.renderer.xr?.isPresenting === true;
14008
14573
  }
14574
+ /**
14575
+ * `true` while the session is compositing over the real world (passthrough).
14576
+ *
14577
+ * Read from the session's `environmentBlendMode` rather than from what the app
14578
+ * ASKED for: a request for `immersive-ar` can be granted as a plain opaque
14579
+ * session, and the blend mode is the only thing that says what the user is
14580
+ * actually looking at. `additive` (optical see-through) and `alpha-blend`
14581
+ * (video passthrough) both mean the real world is visible behind the render.
14582
+ *
14583
+ * The load-bearing consequence is that nothing may paint an opaque background:
14584
+ * a skybox drawn over the whole framebuffer hides the room completely, which
14585
+ * looks exactly like passthrough having failed.
14586
+ */
14587
+ isXrPassthrough() {
14588
+ if (!this.isXrPresenting()) return false;
14589
+ const blendMode = this.renderer.xr?.getEnvironmentBlendMode?.();
14590
+ return blendMode === "additive" || blendMode === "alpha-blend";
14591
+ }
14009
14592
  /**
14010
14593
  * Pauses world simulation. While paused, `update()` skips all system ticks so
14011
14594
  * physics, animation, scripts, and rendering stop advancing. The last rendered
@@ -15015,8 +15598,11 @@ exports.VELOCITY_SAMPLES = VELOCITY_SAMPLES;
15015
15598
  exports.VRTeleportAreaSystem = VRTeleportAreaSystem;
15016
15599
  exports.VideoSystem = VideoSystem;
15017
15600
  exports.World = World;
15601
+ exports.XRAnchorSystem = XRAnchorSystem;
15018
15602
  exports.XRControllerVisualSystem = XRControllerVisualSystem;
15603
+ exports.XREnvironmentSystem = XREnvironmentSystem;
15019
15604
  exports.XRHandPoseSystem = XRHandPoseSystem;
15605
+ exports.XRHitTestSystem = XRHitTestSystem;
15020
15606
  exports.XRHoverVisualSystem = XRHoverVisualSystem;
15021
15607
  exports.XRInputSystem = XRInputSystem;
15022
15608
  exports.XRInteractionSystem = XRInteractionSystem;