@relu-interactives/spatial-ecs 0.3.21 → 0.4.1

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 (46) hide show
  1. package/dist/{System-BhnLw0fZ.d.cts → System-ln4Q7Eot.d.cts} +15 -1
  2. package/dist/{System-BhnLw0fZ.d.ts → System-ln4Q7Eot.d.ts} +15 -1
  3. package/dist/{XRHandPose-CrzI57JA.d.ts → XRHandPose-BImIa4ZS.d.ts} +82 -2
  4. package/dist/{XRHandPose-CLl4MiHx.d.cts → XRHandPose-CxPv-LBo.d.cts} +82 -2
  5. package/dist/{chunk-H6DQ7JRE.js → chunk-4AK4I3QF.js} +33 -3
  6. package/dist/chunk-4AK4I3QF.js.map +1 -0
  7. package/dist/{chunk-D6KXWNYT.js → chunk-4DWZK4X4.js} +3 -3
  8. package/dist/{chunk-D6KXWNYT.js.map → chunk-4DWZK4X4.js.map} +1 -1
  9. package/dist/{chunk-CDSE7476.js → chunk-LAOFCFEI.js} +554 -5
  10. package/dist/chunk-LAOFCFEI.js.map +1 -0
  11. package/dist/{chunk-P42OE3RU.js → chunk-SGADE55G.js} +14 -5
  12. package/dist/chunk-SGADE55G.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/mixed-reality.md +120 -0
  19. package/dist/factories/index.cjs +26 -0
  20. package/dist/factories/index.cjs.map +1 -1
  21. package/dist/factories/index.d.cts +5 -3
  22. package/dist/factories/index.d.ts +5 -3
  23. package/dist/factories/index.js +3 -3
  24. package/dist/{index-C2I5PxCI.d.cts → index-CBdnIyI8.d.cts} +94 -2
  25. package/dist/{index-g777AmFv.d.cts → index-CQsHUez3.d.cts} +42 -4
  26. package/dist/{index-JRcoel9F.d.ts → index-CuGqxuk-.d.ts} +42 -4
  27. package/dist/{index-6l4hn2Sh.d.ts → index-n7SeH119.d.ts} +94 -2
  28. package/dist/index.cjs +610 -0
  29. package/dist/index.cjs.map +1 -1
  30. package/dist/index.d.cts +9 -8
  31. package/dist/index.d.ts +9 -8
  32. package/dist/index.js +26 -8
  33. package/dist/index.js.map +1 -1
  34. package/dist/systems/index.cjs +580 -0
  35. package/dist/systems/index.cjs.map +1 -1
  36. package/dist/systems/index.d.cts +2 -2
  37. package/dist/systems/index.d.ts +2 -2
  38. package/dist/systems/index.js +3 -3
  39. package/dist/types.d.cts +1 -1
  40. package/dist/types.d.ts +1 -1
  41. package/dist/worldSnapshotUtils.d.cts +1 -1
  42. package/dist/worldSnapshotUtils.d.ts +1 -1
  43. package/package.json +1 -1
  44. package/dist/chunk-CDSE7476.js.map +0 -1
  45. package/dist/chunk-H6DQ7JRE.js.map +0 -1
  46. package/dist/chunk-P42OE3RU.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");
@@ -2855,6 +2861,24 @@ function createXRHandPoseComponent(world, entityId, ...args) {
2855
2861
  }
2856
2862
  __name(createXRHandPoseComponent, "createXRHandPoseComponent");
2857
2863
 
2864
+ // core/components/XRAnchor.ts
2865
+ var _XRAnchorComponent = class _XRAnchorComponent extends Component {
2866
+ constructor(options) {
2867
+ super();
2868
+ this.enabled = options?.enabled ?? true;
2869
+ }
2870
+ };
2871
+ __name(_XRAnchorComponent, "XRAnchorComponent");
2872
+ var XRAnchorComponent = _XRAnchorComponent;
2873
+
2874
+ // core/factories/components/XRAnchorFactory.ts
2875
+ function createXRAnchorComponent(world, entityId, ...args) {
2876
+ const component = new XRAnchorComponent(...args);
2877
+ world.addComponent(entityId, component);
2878
+ return component;
2879
+ }
2880
+ __name(createXRAnchorComponent, "createXRAnchorComponent");
2881
+
2858
2882
  // core/components/XRInteractable.ts
2859
2883
  var _XRInteractableComponent = class _XRInteractableComponent extends Component {
2860
2884
  constructor(options) {
@@ -2966,6 +2990,7 @@ function registerDefaultComponentFactories(objectManagementSystem) {
2966
2990
  objectManagementSystem.registerComponentFactory("xrRig", createXRRigComponent);
2967
2991
  objectManagementSystem.registerComponentFactory("vrTeleportArea", createVRTeleportAreaComponent);
2968
2992
  objectManagementSystem.registerComponentFactory("xrHandPose", createXRHandPoseComponent);
2993
+ objectManagementSystem.registerComponentFactory("xrAnchor", createXRAnchorComponent);
2969
2994
  objectManagementSystem.registerComponentFactory("xrInteractable", createXRInteractableComponent);
2970
2995
  objectManagementSystem.registerComponentFactory("xrInteractor", createXRInteractorComponent);
2971
2996
  objectManagementSystem.registerComponentFactory("xrInteractorSide", createXRInteractorSideComponent);
@@ -4019,6 +4044,8 @@ __export(components_exports, {
4019
4044
  TransformComponent: () => TransformComponent,
4020
4045
  VRTeleportAreaComponent: () => VRTeleportAreaComponent,
4021
4046
  VideoComponent: () => VideoComponent,
4047
+ XRAnchorComponent: () => XRAnchorComponent,
4048
+ XREnvironmentComponent: () => XREnvironmentComponent,
4022
4049
  XRHandPoseComponent: () => XRHandPoseComponent,
4023
4050
  XRInteractableComponent: () => XRInteractableComponent,
4024
4051
  XRInteractorComponent: () => XRInteractorComponent,
@@ -4061,6 +4088,18 @@ var _PrefabNode = class _PrefabNode extends Component {
4061
4088
  __name(_PrefabNode, "PrefabNode");
4062
4089
  var PrefabNode = _PrefabNode;
4063
4090
 
4091
+ // core/components/XREnvironment.ts
4092
+ var _XREnvironmentComponent = class _XREnvironmentComponent extends Component {
4093
+ constructor(kind, semanticLabel = null, orientation = null) {
4094
+ super();
4095
+ this.kind = kind;
4096
+ this.semanticLabel = semanticLabel;
4097
+ this.orientation = orientation;
4098
+ }
4099
+ };
4100
+ __name(_XREnvironmentComponent, "XREnvironmentComponent");
4101
+ var XREnvironmentComponent = _XREnvironmentComponent;
4102
+
4064
4103
  // core/factories/objects/ModelFactory.ts
4065
4104
  function getModelFileName(path) {
4066
4105
  const segments = path.split(/[\\/]/).filter(Boolean);
@@ -4723,6 +4762,7 @@ function buildComponentValueMaps(snapshot) {
4723
4762
  snapshot.components,
4724
4763
  "XRHandPoseComponent"
4725
4764
  ),
4765
+ xrAnchorValues: readByComponent(snapshot.components, "XRAnchorComponent"),
4726
4766
  xrInteractorSideValues: readByComponent(
4727
4767
  snapshot.components,
4728
4768
  "XRInteractorSideComponent"
@@ -4876,6 +4916,24 @@ function restorePostCreateComponents(world, objectManagementSystem, targetEntity
4876
4916
  if (typeof sourceXrRig.fallbackEyeHeight === "number") {
4877
4917
  rig.fallbackEyeHeight = sourceXrRig.fallbackEyeHeight;
4878
4918
  }
4919
+ if (typeof sourceXrRig.passthrough === "boolean") {
4920
+ rig.passthrough = sourceXrRig.passthrough;
4921
+ }
4922
+ if (typeof sourceXrRig.depthOcclusion === "boolean") {
4923
+ rig.depthOcclusion = sourceXrRig.depthOcclusion;
4924
+ }
4925
+ if (typeof sourceXrRig.environmentPhysics === "string") {
4926
+ rig.environmentPhysics = sourceXrRig.environmentPhysics;
4927
+ }
4928
+ if (typeof sourceXrRig.environmentDebug === "boolean") {
4929
+ rig.environmentDebug = sourceXrRig.environmentDebug;
4930
+ }
4931
+ if (typeof sourceXrRig.hitTest === "boolean") {
4932
+ rig.hitTest = sourceXrRig.hitTest;
4933
+ }
4934
+ if (typeof sourceXrRig.anchors === "boolean") {
4935
+ rig.anchors = sourceXrRig.anchors;
4936
+ }
4879
4937
  }
4880
4938
  }
4881
4939
  {
@@ -5013,6 +5071,19 @@ function restorePostCreateComponents(world, objectManagementSystem, targetEntity
5013
5071
  }
5014
5072
  }
5015
5073
  }
5074
+ {
5075
+ const sourceAnchor = asRecord3(values.xrAnchorValues.get(sourceEntityId));
5076
+ let anchor = world.getComponent(targetEntityId, XRAnchorComponent);
5077
+ if (sourceAnchor && !anchor) {
5078
+ anchor = new XRAnchorComponent();
5079
+ world.addComponent(targetEntityId, anchor);
5080
+ }
5081
+ if (sourceAnchor && anchor) {
5082
+ if (typeof sourceAnchor.enabled === "boolean") {
5083
+ anchor.enabled = sourceAnchor.enabled;
5084
+ }
5085
+ }
5086
+ }
5016
5087
  {
5017
5088
  const sourceInteractor = asRecord3(
5018
5089
  values.xrInteractorValues.get(sourceEntityId)
@@ -7243,6 +7314,8 @@ var _EnvironmentSyncSystem = class _EnvironmentSyncSystem {
7243
7314
  this.signature = null;
7244
7315
  this.activeTexture = null;
7245
7316
  this.pendingImageUrl = null;
7317
+ /** Whether the skybox is currently withheld because passthrough is on. */
7318
+ this.passthroughSuppressed = false;
7246
7319
  this.scene = scene;
7247
7320
  }
7248
7321
  disposeActiveTexture() {
@@ -7327,6 +7400,18 @@ var _EnvironmentSyncSystem = class _EnvironmentSyncSystem {
7327
7400
  this.scene.fog = new THREE51__namespace.FogExp2(fogColor, density);
7328
7401
  }
7329
7402
  update(world) {
7403
+ const passthrough = world.isXrPassthrough();
7404
+ if (passthrough !== this.passthroughSuppressed) {
7405
+ this.passthroughSuppressed = passthrough;
7406
+ if (passthrough) {
7407
+ this.scene.background = null;
7408
+ } else {
7409
+ this.signature = null;
7410
+ }
7411
+ }
7412
+ if (passthrough) {
7413
+ return;
7414
+ }
7330
7415
  const entityIds = world.query(Environment);
7331
7416
  if (entityIds.length === 0) {
7332
7417
  if (this.signature !== null) {
@@ -12350,6 +12435,509 @@ var _XRSessionSystem = class _XRSessionSystem {
12350
12435
  };
12351
12436
  __name(_XRSessionSystem, "XRSessionSystem");
12352
12437
  var XRSessionSystem = _XRSessionSystem;
12438
+ var PLANE_COLLIDER_THICKNESS = 0.02;
12439
+ var MIN_PLANE_EXTENT = 0.01;
12440
+ function planeColliderFromPolygon(polygon) {
12441
+ if (!polygon || polygon.length < 3) return null;
12442
+ let minX = Number.POSITIVE_INFINITY;
12443
+ let maxX = Number.NEGATIVE_INFINITY;
12444
+ let minZ = Number.POSITIVE_INFINITY;
12445
+ let maxZ = Number.NEGATIVE_INFINITY;
12446
+ for (const point of polygon) {
12447
+ if (!Number.isFinite(point.x) || !Number.isFinite(point.z)) return null;
12448
+ minX = Math.min(minX, point.x);
12449
+ maxX = Math.max(maxX, point.x);
12450
+ minZ = Math.min(minZ, point.z);
12451
+ maxZ = Math.max(maxZ, point.z);
12452
+ }
12453
+ const width = maxX - minX;
12454
+ const depth = maxZ - minZ;
12455
+ if (width < MIN_PLANE_EXTENT || depth < MIN_PLANE_EXTENT) return null;
12456
+ return {
12457
+ // Thin on Y: that is the plane's normal in plane space.
12458
+ size: { x: width, y: PLANE_COLLIDER_THICKNESS, z: depth },
12459
+ // The polygon is not necessarily centred on its own origin, so the box has
12460
+ // to be offset to sit over it. Pushed DOWN half its thickness so the
12461
+ // collider's top face lines up with the real surface — otherwise objects
12462
+ // rest a centimetre above the floor, or sink a centimetre into it.
12463
+ offset: {
12464
+ x: (minX + maxX) / 2,
12465
+ y: -PLANE_COLLIDER_THICKNESS / 2,
12466
+ z: (minZ + maxZ) / 2
12467
+ }
12468
+ };
12469
+ }
12470
+ __name(planeColliderFromPolygon, "planeColliderFromPolygon");
12471
+ function writeXrMeshGeometry(vertices, indices, existing) {
12472
+ const geometry = existing ?? new THREE51__namespace.BufferGeometry();
12473
+ geometry.setAttribute("position", new THREE51__namespace.BufferAttribute(vertices, 3));
12474
+ geometry.setIndex(new THREE51__namespace.BufferAttribute(indices, 1));
12475
+ geometry.computeBoundingSphere();
12476
+ geometry.computeVertexNormals();
12477
+ return geometry;
12478
+ }
12479
+ __name(writeXrMeshGeometry, "writeXrMeshGeometry");
12480
+ function decomposeXrPose(matrixElements, target) {
12481
+ const matrix = new THREE51__namespace.Matrix4().fromArray(
12482
+ matrixElements
12483
+ );
12484
+ const position = new THREE51__namespace.Vector3();
12485
+ const quaternion = new THREE51__namespace.Quaternion();
12486
+ const scale = new THREE51__namespace.Vector3();
12487
+ matrix.decompose(position, quaternion, scale);
12488
+ const euler = new THREE51__namespace.Euler().setFromQuaternion(quaternion, "XYZ");
12489
+ target.position.x = position.x;
12490
+ target.position.y = position.y;
12491
+ target.position.z = position.z;
12492
+ target.rotation.x = euler.x;
12493
+ target.rotation.y = euler.y;
12494
+ target.rotation.z = euler.z;
12495
+ }
12496
+ __name(decomposeXrPose, "decomposeXrPose");
12497
+
12498
+ // core/systems/XREnvironmentSystem.ts
12499
+ var DEBUG_COLOR = 5088255;
12500
+ var _XREnvironmentSystem = class _XREnvironmentSystem {
12501
+ constructor() {
12502
+ /** Keyed by the XRPlane/XRMesh object itself — the runtime's own identity. */
12503
+ this.planes = /* @__PURE__ */ new Map();
12504
+ this.meshes = /* @__PURE__ */ new Map();
12505
+ this.presenting = false;
12506
+ this.debugVisible = false;
12507
+ }
12508
+ update(world) {
12509
+ const presenting = world.isXrPresenting();
12510
+ if (!presenting) {
12511
+ if (this.presenting) {
12512
+ this.presenting = false;
12513
+ this.clearAll(world);
12514
+ }
12515
+ return;
12516
+ }
12517
+ this.presenting = true;
12518
+ const rig = this.findRig(world);
12519
+ const mode = rig?.environmentPhysics ?? "none";
12520
+ if (mode === "none") {
12521
+ if (this.planes.size > 0 || this.meshes.size > 0) this.clearAll(world);
12522
+ return;
12523
+ }
12524
+ const debug = rig?.environmentDebug === true;
12525
+ if (debug !== this.debugVisible) {
12526
+ this.debugVisible = debug;
12527
+ this.applyDebugVisibility(world);
12528
+ }
12529
+ const xr = world.getRenderer().xr;
12530
+ const frame = xr.getFrame?.();
12531
+ const referenceSpace = xr.getReferenceSpace();
12532
+ if (!frame || !referenceSpace) return;
12533
+ const meshesAvailable = Boolean(frame.detectedMeshes);
12534
+ const useMesh = (mode === "mesh" || mode === "auto") && meshesAvailable;
12535
+ const usePlanes = mode === "planes" || mode === "auto" && !useMesh;
12536
+ if (usePlanes) {
12537
+ this.syncPlanes(world, frame, referenceSpace);
12538
+ } else if (this.planes.size > 0) {
12539
+ this.clearMap(world, this.planes);
12540
+ }
12541
+ if (useMesh) {
12542
+ this.syncMeshes(world, frame, referenceSpace);
12543
+ } else if (this.meshes.size > 0) {
12544
+ this.clearMap(world, this.meshes);
12545
+ }
12546
+ }
12547
+ /** How many pieces of real geometry are currently colliding. Script-facing. */
12548
+ getDetectedCount() {
12549
+ return { planes: this.planes.size, meshes: this.meshes.size };
12550
+ }
12551
+ findRig(world) {
12552
+ const ids = world.query(XRRigComponent);
12553
+ if (ids.length === 0) return null;
12554
+ return world.getComponent(ids[0], XRRigComponent) ?? null;
12555
+ }
12556
+ syncPlanes(world, frame, referenceSpace) {
12557
+ const detected = frame.detectedPlanes;
12558
+ if (!detected) return;
12559
+ for (const [plane, entry] of this.planes) {
12560
+ if (!detected.has(plane)) {
12561
+ this.destroyEntry(world, entry);
12562
+ this.planes.delete(plane);
12563
+ }
12564
+ }
12565
+ for (const plane of detected) {
12566
+ const pose = frame.getPose(plane.planeSpace, referenceSpace);
12567
+ if (!pose) continue;
12568
+ let entry = this.planes.get(plane);
12569
+ const changed = plane.lastChangedTime ?? 0;
12570
+ if (!entry) {
12571
+ const created = this.createPlaneEntity(world, plane);
12572
+ if (!created) continue;
12573
+ entry = created;
12574
+ this.planes.set(plane, entry);
12575
+ } else if (changed !== entry.lastChangedTime) {
12576
+ entry.lastChangedTime = changed;
12577
+ this.applyPlaneCollider(world, entry.entityId, plane);
12578
+ }
12579
+ this.applyPose(world, entry.entityId, pose);
12580
+ }
12581
+ }
12582
+ syncMeshes(world, frame, referenceSpace) {
12583
+ const detected = frame.detectedMeshes;
12584
+ if (!detected) return;
12585
+ for (const [mesh, entry] of this.meshes) {
12586
+ if (!detected.has(mesh)) {
12587
+ this.destroyEntry(world, entry);
12588
+ this.meshes.delete(mesh);
12589
+ }
12590
+ }
12591
+ for (const mesh of detected) {
12592
+ const pose = frame.getPose(mesh.meshSpace, referenceSpace);
12593
+ if (!pose) continue;
12594
+ let entry = this.meshes.get(mesh);
12595
+ const changed = mesh.lastChangedTime ?? 0;
12596
+ if (!entry) {
12597
+ const created = this.createMeshEntity(world, mesh);
12598
+ if (!created) continue;
12599
+ entry = created;
12600
+ this.meshes.set(mesh, entry);
12601
+ } else if (changed !== entry.lastChangedTime) {
12602
+ entry.lastChangedTime = changed;
12603
+ writeXrMeshGeometry(mesh.vertices, mesh.indices, entry.geometry);
12604
+ }
12605
+ this.applyPose(world, entry.entityId, pose);
12606
+ }
12607
+ }
12608
+ createPlaneEntity(world, plane) {
12609
+ const collider = planeColliderFromPolygon(plane.polygon);
12610
+ if (!collider) return null;
12611
+ const entityId = world.createEntity();
12612
+ world.addComponents(entityId, [
12613
+ new TransformComponent(),
12614
+ new XREnvironmentComponent(
12615
+ "plane",
12616
+ plane.semanticLabel ?? null,
12617
+ normalizeOrientation(plane.orientation)
12618
+ )
12619
+ ]);
12620
+ this.applyPlaneCollider(world, entityId, plane);
12621
+ const entry = {
12622
+ entityId,
12623
+ lastChangedTime: plane.lastChangedTime ?? 0,
12624
+ geometry: null,
12625
+ material: null
12626
+ };
12627
+ if (this.debugVisible) this.attachPlaneDebug(world, entry, collider);
12628
+ return entry;
12629
+ }
12630
+ applyPlaneCollider(world, entityId, plane) {
12631
+ const collider = planeColliderFromPolygon(plane.polygon);
12632
+ if (!collider) return;
12633
+ const existing = world.getComponent(entityId, PhysicsComponent);
12634
+ const values = {
12635
+ // Fixed: the real room does not move because something hit it.
12636
+ rigidbody: { type: "fixed" },
12637
+ collider: {
12638
+ shape: "cuboid",
12639
+ size: collider.size,
12640
+ offsetPosition: collider.offset
12641
+ }
12642
+ };
12643
+ if (existing) {
12644
+ existing.setValues(values);
12645
+ } else {
12646
+ world.addComponent(entityId, new PhysicsComponent(values));
12647
+ }
12648
+ }
12649
+ createMeshEntity(world, mesh) {
12650
+ if (!mesh.vertices?.length || !mesh.indices?.length) return null;
12651
+ const geometry = writeXrMeshGeometry(mesh.vertices, mesh.indices);
12652
+ const material = new THREE51__namespace.MeshBasicMaterial({
12653
+ color: DEBUG_COLOR,
12654
+ wireframe: true
12655
+ });
12656
+ const root = new THREE51__namespace.Group();
12657
+ const child = new THREE51__namespace.Mesh(geometry, material);
12658
+ child.visible = this.debugVisible;
12659
+ root.add(child);
12660
+ const entityId = world.createEntity();
12661
+ world.addComponents(entityId, [
12662
+ new TransformComponent(),
12663
+ new Object3DRef(root),
12664
+ new XREnvironmentComponent("mesh", mesh.semanticLabel ?? null, null),
12665
+ new PhysicsComponent({
12666
+ rigidbody: { type: "fixed" },
12667
+ collider: { shape: "trimesh" }
12668
+ })
12669
+ ]);
12670
+ return {
12671
+ entityId,
12672
+ lastChangedTime: mesh.lastChangedTime ?? 0,
12673
+ geometry,
12674
+ material
12675
+ };
12676
+ }
12677
+ /** A thin box so a detected plane can be SEEN, not just collided with. */
12678
+ attachPlaneDebug(world, entry, collider) {
12679
+ const geometry = new THREE51__namespace.BoxGeometry(
12680
+ collider.size.x,
12681
+ collider.size.y,
12682
+ collider.size.z
12683
+ );
12684
+ const material = new THREE51__namespace.MeshBasicMaterial({
12685
+ color: DEBUG_COLOR,
12686
+ wireframe: true
12687
+ });
12688
+ const root = new THREE51__namespace.Group();
12689
+ root.add(new THREE51__namespace.Mesh(geometry, material));
12690
+ entry.geometry = geometry;
12691
+ entry.material = material;
12692
+ world.addComponent(entry.entityId, new Object3DRef(root));
12693
+ }
12694
+ applyPose(world, entityId, pose) {
12695
+ const transform = world.getComponent(entityId, TransformComponent);
12696
+ if (!transform) return;
12697
+ decomposeXrPose(pose.transform.matrix, transform);
12698
+ }
12699
+ applyDebugVisibility(world) {
12700
+ for (const map of [this.planes, this.meshes]) {
12701
+ for (const entry of map.values()) {
12702
+ const ref = world.getComponent(entry.entityId, Object3DRef);
12703
+ ref?.object.traverse((child) => {
12704
+ if (child.isMesh) child.visible = this.debugVisible;
12705
+ });
12706
+ }
12707
+ }
12708
+ }
12709
+ destroyEntry(world, entry) {
12710
+ entry.geometry?.dispose();
12711
+ entry.material?.dispose();
12712
+ world.destroyEntity(entry.entityId);
12713
+ }
12714
+ clearMap(world, map) {
12715
+ for (const entry of map.values()) this.destroyEntry(world, entry);
12716
+ map.clear();
12717
+ }
12718
+ clearAll(world) {
12719
+ this.clearMap(world, this.planes);
12720
+ this.clearMap(world, this.meshes);
12721
+ }
12722
+ dispose(world) {
12723
+ this.clearAll(world);
12724
+ }
12725
+ };
12726
+ __name(_XREnvironmentSystem, "XREnvironmentSystem");
12727
+ var XREnvironmentSystem = _XREnvironmentSystem;
12728
+ function normalizeOrientation(value) {
12729
+ return value === "horizontal" || value === "vertical" ? value : null;
12730
+ }
12731
+ __name(normalizeOrientation, "normalizeOrientation");
12732
+ var _XRHitTestSystem = class _XRHitTestSystem {
12733
+ constructor() {
12734
+ this.sources = /* @__PURE__ */ new Map();
12735
+ /** Targets whose async request is in flight, so it is not fired twice. */
12736
+ this.pending = /* @__PURE__ */ new Set();
12737
+ this.results = /* @__PURE__ */ new Map();
12738
+ this.presenting = false;
12739
+ this.enabled = false;
12740
+ }
12741
+ update(world) {
12742
+ const presenting = world.isXrPresenting();
12743
+ if (!presenting) {
12744
+ if (this.presenting) {
12745
+ this.presenting = false;
12746
+ this.teardown();
12747
+ }
12748
+ return;
12749
+ }
12750
+ if (!this.presenting) {
12751
+ this.presenting = true;
12752
+ this.enabled = this.rigWantsHitTest(world);
12753
+ }
12754
+ if (!this.enabled) return;
12755
+ const xr = world.getRenderer().xr;
12756
+ const session = xr.getSession();
12757
+ const referenceSpace = xr.getReferenceSpace();
12758
+ const frame = xr.getFrame?.();
12759
+ if (!session || !referenceSpace || !frame) return;
12760
+ this.ensureViewerSource(session);
12761
+ this.ensureInputSources(session);
12762
+ this.results.clear();
12763
+ for (const [key, source] of this.sources) {
12764
+ const hits = frame.getHitTestResults(source);
12765
+ if (!hits || hits.length === 0) continue;
12766
+ const pose = hits[0].getPose(referenceSpace);
12767
+ if (!pose) continue;
12768
+ const matrix = new THREE51__namespace.Matrix4().fromArray(
12769
+ pose.transform.matrix
12770
+ );
12771
+ const position = new THREE51__namespace.Vector3();
12772
+ const quaternion = new THREE51__namespace.Quaternion();
12773
+ matrix.decompose(position, quaternion, new THREE51__namespace.Vector3());
12774
+ this.results.set(key, { position, quaternion });
12775
+ }
12776
+ }
12777
+ /**
12778
+ * Where the given ray meets the real world this frame, or null.
12779
+ *
12780
+ * Null is the normal case, not an error: the source may still be being
12781
+ * requested, the ray may point at nothing recognised, or the runtime may not
12782
+ * have granted `hit-test` at all.
12783
+ */
12784
+ getHitResult(target = "viewer") {
12785
+ return this.results.get(target) ?? null;
12786
+ }
12787
+ rigWantsHitTest(world) {
12788
+ const ids = world.query(XRRigComponent);
12789
+ if (ids.length === 0) return false;
12790
+ return world.getComponent(ids[0], XRRigComponent)?.hitTest === true;
12791
+ }
12792
+ ensureViewerSource(session) {
12793
+ if (this.sources.has("viewer") || this.pending.has("viewer")) return;
12794
+ if (typeof session.requestHitTestSource !== "function") return;
12795
+ this.pending.add("viewer");
12796
+ void session.requestReferenceSpace("viewer").then((space) => session.requestHitTestSource?.({ space })).then((source) => {
12797
+ if (source && this.presenting) this.sources.set("viewer", source);
12798
+ else source?.cancel?.();
12799
+ }).catch(() => {
12800
+ }).finally(() => this.pending.delete("viewer"));
12801
+ }
12802
+ ensureInputSources(session) {
12803
+ if (typeof session.requestHitTestSource !== "function") return;
12804
+ if (!session.inputSources) return;
12805
+ for (const input of session.inputSources) {
12806
+ const handedness = input.handedness;
12807
+ if (!handedness || handedness === "none") continue;
12808
+ if (!input.targetRaySpace) continue;
12809
+ if (this.sources.has(handedness) || this.pending.has(handedness)) continue;
12810
+ this.pending.add(handedness);
12811
+ const request = session.requestHitTestSource({
12812
+ space: input.targetRaySpace
12813
+ });
12814
+ if (!request) {
12815
+ this.pending.delete(handedness);
12816
+ continue;
12817
+ }
12818
+ void request.then((source) => {
12819
+ if (source && this.presenting) this.sources.set(handedness, source);
12820
+ else source?.cancel?.();
12821
+ }).catch(() => {
12822
+ }).finally(() => this.pending.delete(handedness));
12823
+ }
12824
+ }
12825
+ teardown() {
12826
+ for (const source of this.sources.values()) source.cancel?.();
12827
+ this.sources.clear();
12828
+ this.results.clear();
12829
+ this.pending.clear();
12830
+ this.enabled = false;
12831
+ }
12832
+ dispose() {
12833
+ this.presenting = false;
12834
+ this.teardown();
12835
+ }
12836
+ };
12837
+ __name(_XRHitTestSystem, "XRHitTestSystem");
12838
+ var XRHitTestSystem = _XRHitTestSystem;
12839
+ var _XRAnchorSystem = class _XRAnchorSystem {
12840
+ constructor() {
12841
+ this.anchors = /* @__PURE__ */ new Map();
12842
+ /** In-flight creations, so one entity is not anchored twice. */
12843
+ this.pending = /* @__PURE__ */ new Set();
12844
+ this.presenting = false;
12845
+ }
12846
+ update(world) {
12847
+ const presenting = world.isXrPresenting();
12848
+ if (!presenting) {
12849
+ if (this.presenting) {
12850
+ this.presenting = false;
12851
+ this.releaseAll();
12852
+ }
12853
+ return;
12854
+ }
12855
+ this.presenting = true;
12856
+ const xr = world.getRenderer().xr;
12857
+ const referenceSpace = xr.getReferenceSpace();
12858
+ const frame = xr.getFrame?.();
12859
+ if (!frame || !referenceSpace) return;
12860
+ const entityIds = world.query(XRAnchorComponent, TransformComponent);
12861
+ const live = new Set(entityIds);
12862
+ for (const [entityId, anchor] of this.anchors) {
12863
+ if (!live.has(entityId)) {
12864
+ anchor.delete?.();
12865
+ this.anchors.delete(entityId);
12866
+ }
12867
+ }
12868
+ for (const entityId of entityIds) {
12869
+ const component = world.getComponent(entityId, XRAnchorComponent);
12870
+ const transform = world.getComponent(entityId, TransformComponent);
12871
+ if (!component || !transform) continue;
12872
+ if (!component.enabled) {
12873
+ const existing = this.anchors.get(entityId);
12874
+ if (existing) {
12875
+ existing.delete?.();
12876
+ this.anchors.delete(entityId);
12877
+ }
12878
+ continue;
12879
+ }
12880
+ const anchor = this.anchors.get(entityId);
12881
+ if (!anchor) {
12882
+ this.requestAnchor(entityId, transform, frame, referenceSpace);
12883
+ continue;
12884
+ }
12885
+ const pose = frame.getPose(anchor.anchorSpace, referenceSpace);
12886
+ if (pose) decomposeXrPose(pose.transform.matrix, transform);
12887
+ }
12888
+ }
12889
+ /** Whether this entity's pose is currently being driven by an anchor. */
12890
+ isAnchored(entityId) {
12891
+ return this.anchors.has(entityId);
12892
+ }
12893
+ requestAnchor(entityId, transform, frame, referenceSpace) {
12894
+ if (this.pending.has(entityId)) return;
12895
+ if (typeof frame.createAnchor !== "function") return;
12896
+ const RigidTransform = globalThis.XRRigidTransform;
12897
+ if (!RigidTransform) return;
12898
+ const quaternion = new THREE51__namespace.Quaternion().setFromEuler(
12899
+ new THREE51__namespace.Euler(
12900
+ transform.rotation.x,
12901
+ transform.rotation.y,
12902
+ transform.rotation.z,
12903
+ "XYZ"
12904
+ )
12905
+ );
12906
+ const pose = new RigidTransform(
12907
+ {
12908
+ x: transform.position.x,
12909
+ y: transform.position.y,
12910
+ z: transform.position.z,
12911
+ w: 1
12912
+ },
12913
+ {
12914
+ x: quaternion.x,
12915
+ y: quaternion.y,
12916
+ z: quaternion.z,
12917
+ w: quaternion.w
12918
+ }
12919
+ );
12920
+ const request = frame.createAnchor(pose, referenceSpace);
12921
+ if (!request) return;
12922
+ this.pending.add(entityId);
12923
+ void request.then((anchor) => {
12924
+ if (anchor && this.presenting) this.anchors.set(entityId, anchor);
12925
+ else anchor?.delete?.();
12926
+ }).catch(() => {
12927
+ }).finally(() => this.pending.delete(entityId));
12928
+ }
12929
+ releaseAll() {
12930
+ for (const anchor of this.anchors.values()) anchor.delete?.();
12931
+ this.anchors.clear();
12932
+ this.pending.clear();
12933
+ }
12934
+ dispose() {
12935
+ this.presenting = false;
12936
+ this.releaseAll();
12937
+ }
12938
+ };
12939
+ __name(_XRAnchorSystem, "XRAnchorSystem");
12940
+ var XRAnchorSystem = _XRAnchorSystem;
12353
12941
  function buildGeometry2(area) {
12354
12942
  if (area.shape === "circle") {
12355
12943
  const radius = Math.max(5e-3, area.width / 2);
@@ -14006,6 +14594,24 @@ var _World = class _World {
14006
14594
  isXrPresenting() {
14007
14595
  return this.renderer.xr?.isPresenting === true;
14008
14596
  }
14597
+ /**
14598
+ * `true` while the session is compositing over the real world (passthrough).
14599
+ *
14600
+ * Read from the session's `environmentBlendMode` rather than from what the app
14601
+ * ASKED for: a request for `immersive-ar` can be granted as a plain opaque
14602
+ * session, and the blend mode is the only thing that says what the user is
14603
+ * actually looking at. `additive` (optical see-through) and `alpha-blend`
14604
+ * (video passthrough) both mean the real world is visible behind the render.
14605
+ *
14606
+ * The load-bearing consequence is that nothing may paint an opaque background:
14607
+ * a skybox drawn over the whole framebuffer hides the room completely, which
14608
+ * looks exactly like passthrough having failed.
14609
+ */
14610
+ isXrPassthrough() {
14611
+ if (!this.isXrPresenting()) return false;
14612
+ const blendMode = this.renderer.xr?.getEnvironmentBlendMode?.();
14613
+ return blendMode === "additive" || blendMode === "alpha-blend";
14614
+ }
14009
14615
  /**
14010
14616
  * Pauses world simulation. While paused, `update()` skips all system ticks so
14011
14617
  * physics, animation, scripts, and rendering stop advancing. The last rendered
@@ -15015,8 +15621,11 @@ exports.VELOCITY_SAMPLES = VELOCITY_SAMPLES;
15015
15621
  exports.VRTeleportAreaSystem = VRTeleportAreaSystem;
15016
15622
  exports.VideoSystem = VideoSystem;
15017
15623
  exports.World = World;
15624
+ exports.XRAnchorSystem = XRAnchorSystem;
15018
15625
  exports.XRControllerVisualSystem = XRControllerVisualSystem;
15626
+ exports.XREnvironmentSystem = XREnvironmentSystem;
15019
15627
  exports.XRHandPoseSystem = XRHandPoseSystem;
15628
+ exports.XRHitTestSystem = XRHitTestSystem;
15020
15629
  exports.XRHoverVisualSystem = XRHoverVisualSystem;
15021
15630
  exports.XRInputSystem = XRInputSystem;
15022
15631
  exports.XRInteractionSystem = XRInteractionSystem;
@@ -15076,6 +15685,7 @@ exports.createSpriteComponent = createSpriteComponent;
15076
15685
  exports.createTransformComponent = createTransformComponent;
15077
15686
  exports.createVRTeleportAreaComponent = createVRTeleportAreaComponent;
15078
15687
  exports.createVideoComponent = createVideoComponent;
15688
+ exports.createXRAnchorComponent = createXRAnchorComponent;
15079
15689
  exports.createXRHandPoseComponent = createXRHandPoseComponent;
15080
15690
  exports.createXRInteractableComponent = createXRInteractableComponent;
15081
15691
  exports.createXRInteractorComponent = createXRInteractorComponent;