@terraware/web-components 5.0.6 → 5.0.7-rc.2

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 (36) hide show
  1. package/components/VirtualWalkthrough/Annotation.d.ts.map +1 -1
  2. package/components/VirtualWalkthrough/Annotation.js +6 -5
  3. package/components/VirtualWalkthrough/AutoRotator.d.ts +10 -0
  4. package/components/VirtualWalkthrough/AutoRotator.d.ts.map +1 -1
  5. package/components/VirtualWalkthrough/AutoRotator.js +21 -5
  6. package/components/VirtualWalkthrough/TfXrNavigation.d.ts +14 -0
  7. package/components/VirtualWalkthrough/TfXrNavigation.d.ts.map +1 -1
  8. package/components/VirtualWalkthrough/TfXrNavigation.js +46 -0
  9. package/components/VirtualWalkthrough/VirtualWalkthroughViewer.d.ts +12 -0
  10. package/components/VirtualWalkthrough/VirtualWalkthroughViewer.d.ts.map +1 -1
  11. package/components/VirtualWalkthrough/VirtualWalkthroughViewer.js +34 -19
  12. package/components/VirtualWalkthrough/boundary-wall.d.ts.map +1 -1
  13. package/components/VirtualWalkthrough/boundary-wall.js +1 -1
  14. package/components/VirtualWalkthrough/camera-adoption.d.ts +39 -0
  15. package/components/VirtualWalkthrough/camera-adoption.d.ts.map +1 -0
  16. package/components/VirtualWalkthrough/camera-adoption.js +66 -0
  17. package/components/VirtualWalkthrough/useAdoptedCamera.d.ts +14 -0
  18. package/components/VirtualWalkthrough/useAdoptedCamera.d.ts.map +1 -0
  19. package/components/VirtualWalkthrough/useAdoptedCamera.js +45 -0
  20. package/components/VirtualWalkthrough/useXrRenderTuning.d.ts +2 -1
  21. package/components/VirtualWalkthrough/useXrRenderTuning.d.ts.map +1 -1
  22. package/components/VirtualWalkthrough/useXrRenderTuning.js +10 -1
  23. package/components/VirtualWalkthrough/walkthrough-camera.d.ts +8 -1
  24. package/components/VirtualWalkthrough/walkthrough-camera.d.ts.map +1 -1
  25. package/components/VirtualWalkthrough/walkthrough-camera.js +38 -10
  26. package/components/VirtualWalkthrough/xr-annotation-interaction.d.ts.map +1 -1
  27. package/components/VirtualWalkthrough/xr-annotation-interaction.js +2 -2
  28. package/components/VirtualWalkthrough/xr-gaze-dwell.d.ts.map +1 -1
  29. package/components/VirtualWalkthrough/xr-gaze-dwell.js +2 -2
  30. package/hooks/cameraEntityContext.d.ts +12 -0
  31. package/hooks/cameraEntityContext.d.ts.map +1 -0
  32. package/hooks/cameraEntityContext.js +14 -0
  33. package/hooks/useCameraPosition.d.ts +7 -1
  34. package/hooks/useCameraPosition.d.ts.map +1 -1
  35. package/hooks/useCameraPosition.js +32 -10
  36. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"Annotation.d.ts","sourceRoot":"","sources":["../../../src/components/VirtualWalkthrough/Annotation.tsx"],"names":[],"mappings":"AAUA,OAAO,yBAAyB,CAAC;AAEjC,MAAM,MAAM,kBAAkB,GAAG,MAAM,GAAG,OAAO,GAAG,OAAO,CAAC;AAS5D,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,MAAM,CAAC,EAAE,IAAI,EAAE,CAAC;IAChB,IAAI,CAAC,EAAE,kBAAkB,CAAC;IAC1B,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IAC1C,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,KAAK,IAAI,CAAC;IAChE,MAAM,CAAC,EAAE,CAAC,UAAU,EAAE,eAAe,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACjF,sBAAsB,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;CACnG;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,QAAA,MAAM,UAAU,GAAI,OAAO,eAAe,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,4CA2L7D,CAAC;AAEF,eAAe,UAAU,CAAC"}
1
+ {"version":3,"file":"Annotation.d.ts","sourceRoot":"","sources":["../../../src/components/VirtualWalkthrough/Annotation.tsx"],"names":[],"mappings":"AAWA,OAAO,yBAAyB,CAAC;AAEjC,MAAM,MAAM,kBAAkB,GAAG,MAAM,GAAG,OAAO,GAAG,OAAO,CAAC;AAS5D,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,MAAM,CAAC,EAAE,IAAI,EAAE,CAAC;IAChB,IAAI,CAAC,EAAE,kBAAkB,CAAC;IAC1B,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IAC1C,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,KAAK,IAAI,CAAC;IAChE,MAAM,CAAC,EAAE,CAAC,UAAU,EAAE,eAAe,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACjF,sBAAsB,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;CACnG;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,QAAA,MAAM,UAAU,GAAI,OAAO,eAAe,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,4CA2L7D,CAAC;AAEF,eAAe,UAAU,CAAC"}
@@ -1,10 +1,11 @@
1
- import { useRef, useEffect, useCallback, useMemo } from 'react';
1
+ import { useContext, useRef, useEffect, useCallback, useMemo } from 'react';
2
2
  import { Entity } from '@playcanvas/react';
3
3
  import { Script } from '@playcanvas/react/components';
4
4
  import { useApp } from '@playcanvas/react/hooks';
5
5
  import { Gizmo, TranslateGizmo } from 'playcanvas';
6
6
  import { Annotation as Annotation$1 } from 'playcanvas/scripts/esm/annotations.mjs';
7
7
  import { CameraControls } from 'playcanvas/scripts/esm/camera-controls.mjs';
8
+ import { CameraEntityContext } from '../../hooks/cameraEntityContext.js';
8
9
  import { useCameraPosition } from '../../hooks/useCameraPosition.js';
9
10
  import './annotation-styles.css';
10
11
  import { jsx } from 'react/jsx-runtime';
@@ -52,6 +53,7 @@ const Annotation = props => {
52
53
  index
53
54
  } = props;
54
55
  const app = useApp();
56
+ const walkthroughCamera = useContext(CameraEntityContext);
55
57
  const {
56
58
  setCamera
57
59
  } = useCameraPosition();
@@ -146,9 +148,8 @@ const Annotation = props => {
146
148
  }
147
149
  const setupGizmo = () => {
148
150
  const annotationEntity = app.root.findByName(entityName);
149
- const cameraEntity = app.root.findByName('camera');
150
- const cameraComponent = cameraEntity?.camera;
151
- const cameraControls = cameraEntity?.script?.get(CameraControls.scriptName);
151
+ const cameraComponent = walkthroughCamera?.camera;
152
+ const cameraControls = walkthroughCamera?.script?.get(CameraControls.scriptName);
152
153
  if (!annotationEntity || !cameraComponent) {
153
154
  return false;
154
155
  }
@@ -188,7 +189,7 @@ const Annotation = props => {
188
189
  clearInterval(intervalId);
189
190
  cleanupGizmo();
190
191
  };
191
- }, [app, isEdit, isSelected, visible, onPositionChange, entityName]);
192
+ }, [app, isEdit, isSelected, visible, onPositionChange, entityName, walkthroughCamera]);
192
193
  return /*#__PURE__*/jsx(Entity, {
193
194
  name: entityName,
194
195
  position: position,
@@ -88,6 +88,16 @@ export declare class AutoRotator extends Script {
88
88
  * Initialize the script.
89
89
  */
90
90
  initialize(): void;
91
+ /**
92
+ * Finds the camera and the script posing it. WalkthroughCamera is mounted on this entity, the rig,
93
+ * so that it poses a camera the walkthrough adopted from a host scene as readily as one of its own;
94
+ * CameraControls sits on the camera entity itself.
95
+ *
96
+ * Called again while nothing has been found, because an adopted camera is reparented into the rig
97
+ * by an effect that runs after the scripts are mounted.
98
+ * @private
99
+ */
100
+ private resolveCamera;
91
101
  /**
92
102
  * Update loop that handles auto-rotation logic.
93
103
  * Runs after CameraControls updates.
@@ -1 +1 @@
1
- {"version":3,"file":"AutoRotator.d.ts","sourceRoot":"","sources":["../../../src/components/VirtualWalkthrough/AutoRotator.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,MAAM,EAAQ,MAAM,YAAY,CAAC;AAEhD;;;GAGG;AACH,qBAAa,WAAY,SAAQ,MAAM;IACrC,MAAM,CAAC,UAAU,SAAiB;IAElC;;;;;OAKG;IACH,KAAK,SAAK;IAEV;;;;;OAKG;IACH,UAAU,SAAK;IAEf;;;;;OAKG;IACH,YAAY,SAAK;IAEjB;;;;;OAKG;IACH,eAAe,SAAK;IAEpB;;;OAGG;IACH,OAAO,CAAC,KAAK,CAAK;IAElB;;;OAGG;IACH,OAAO,CAAC,kBAAkB,CAAS;IAEnC;;;OAGG;IACH,OAAO,CAAC,mBAAmB,CAAS;IAEpC;;;OAGG;IACH,OAAO,CAAC,KAAK,CAAuB;IAEpC;;;OAGG;IACH,OAAO,CAAC,GAAG,CAAuB;IAElC;;;OAGG;IACH,OAAO,CAAC,YAAY,CAAa;IAEjC;;;OAGG;IACH,OAAO,CAAC,cAAc,CAAa;IAEnC;;;OAGG;IACH,OAAO,CAAC,iBAAiB,CAAa;IAEtC;;;OAGG;IACH,OAAO,CAAC,aAAa,CAAC,CAAa;IACnC,OAAO,CAAC,SAAS,CAAC,CAAa;IAE/B;;;OAGG;IACH,OAAO,CAAC,eAAe;IAMvB;;OAEG;IACH,UAAU;IAgBV;;;OAGG;IACH,UAAU,CAAC,EAAE,EAAE,MAAM;IAsErB;;;;;;OAMG;IACH,OAAO,CAAC,UAAU;IAWlB;;OAEG;IACH,OAAO;CAQR"}
1
+ {"version":3,"file":"AutoRotator.d.ts","sourceRoot":"","sources":["../../../src/components/VirtualWalkthrough/AutoRotator.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,MAAM,EAAQ,MAAM,YAAY,CAAC;AAEhD;;;GAGG;AACH,qBAAa,WAAY,SAAQ,MAAM;IACrC,MAAM,CAAC,UAAU,SAAiB;IAElC;;;;;OAKG;IACH,KAAK,SAAK;IAEV;;;;;OAKG;IACH,UAAU,SAAK;IAEf;;;;;OAKG;IACH,YAAY,SAAK;IAEjB;;;;;OAKG;IACH,eAAe,SAAK;IAEpB;;;OAGG;IACH,OAAO,CAAC,KAAK,CAAK;IAElB;;;OAGG;IACH,OAAO,CAAC,kBAAkB,CAAS;IAEnC;;;OAGG;IACH,OAAO,CAAC,mBAAmB,CAAS;IAEpC;;;OAGG;IACH,OAAO,CAAC,KAAK,CAAuB;IAEpC;;;OAGG;IACH,OAAO,CAAC,GAAG,CAAuB;IAElC;;;OAGG;IACH,OAAO,CAAC,YAAY,CAAa;IAEjC;;;OAGG;IACH,OAAO,CAAC,cAAc,CAAa;IAEnC;;;OAGG;IACH,OAAO,CAAC,iBAAiB,CAAa;IAEtC;;;OAGG;IACH,OAAO,CAAC,aAAa,CAAC,CAAa;IACnC,OAAO,CAAC,SAAS,CAAC,CAAa;IAE/B;;;OAGG;IACH,OAAO,CAAC,eAAe;IAMvB;;OAEG;IACH,UAAU;IAYV;;;;;;;;OAQG;IACH,OAAO,CAAC,aAAa;IAQrB;;;OAGG;IACH,UAAU,CAAC,EAAE,EAAE,MAAM;IA0ErB;;;;;;OAMG;IACH,OAAO,CAAC,UAAU;IAWlB;;OAEG;IACH,OAAO;CAQR"}
@@ -112,11 +112,24 @@ class AutoRotator extends Script {
112
112
  this.app.graphicsDevice.canvas.addEventListener('pointerdown', this.onPointerDown);
113
113
  }
114
114
  window.addEventListener('keydown', this.onKeyDown);
115
- this.cameraEntity = this.entity.findByName('camera');
116
- if (this.cameraEntity) {
117
- this.cameraControls = this.cameraEntity.script?.cameraControls ?? null;
118
- this.walkthroughCamera = this.cameraEntity.script?.walkthroughCamera ?? null;
119
- }
115
+ this.resolveCamera();
116
+ }
117
+
118
+ /**
119
+ * Finds the camera and the script posing it. WalkthroughCamera is mounted on this entity, the rig,
120
+ * so that it poses a camera the walkthrough adopted from a host scene as readily as one of its own;
121
+ * CameraControls sits on the camera entity itself.
122
+ *
123
+ * Called again while nothing has been found, because an adopted camera is reparented into the rig
124
+ * by an effect that runs after the scripts are mounted.
125
+ * @private
126
+ */
127
+ resolveCamera() {
128
+ this.cameraEntity = this.entity.findComponent('camera')?.entity ?? null;
129
+ this.cameraControls = this.cameraEntity?.script?.cameraControls ?? null;
130
+ // @ts-expect-error - scripts are added dynamically to the entity
131
+ const rigWalkthroughCamera = this.entity.script?.walkthroughCamera;
132
+ this.walkthroughCamera = rigWalkthroughCamera ?? this.cameraEntity?.script?.walkthroughCamera ?? null;
120
133
  }
121
134
 
122
135
  /**
@@ -124,6 +137,9 @@ class AutoRotator extends Script {
124
137
  * Runs after CameraControls updates.
125
138
  */
126
139
  postUpdate(dt) {
140
+ if (!this.cameraControls && !this.walkthroughCamera) {
141
+ this.resolveCamera();
142
+ }
127
143
  const hasControls = this.cameraControls && this.cameraControls._pose;
128
144
  const hasWalkthrough = !!this.walkthroughCamera;
129
145
  if (!hasControls && !hasWalkthrough) {
@@ -21,6 +21,20 @@ export declare class TfXrNavigation extends PcXrNavigation {
21
21
  clampDistance: number;
22
22
  private _gestures;
23
23
  private _isBlocked;
24
+ /**
25
+ * Tracks controllers the base script never heard about.
26
+ *
27
+ * Movement, turning and teleport all iterate `_inputSources`, which the base fills from the XR
28
+ * input `add` event it subscribes to in initialize. A walkthrough that adopts the host's camera is
29
+ * mounted into a session that is already running, so the controllers present at session start
30
+ * fired `add` before this script existed and every control is inert for the whole session.
31
+ *
32
+ * The sources are picked up by hand rather than by re-firing `add` on the input manager, which
33
+ * would also reach the host's own listeners and have them handle controllers they already know
34
+ * about. The handlers mirror the base's and are stored in its map, so its teardown detaches them.
35
+ */
36
+ private _syncInputSources;
37
+ private _resolveCamera;
24
38
  tryTeleport(inputSource: XrInputSource): void;
25
39
  update(dt: number): void;
26
40
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"TfXrNavigation.d.ts","sourceRoot":"","sources":["../../../src/components/VirtualWalkthrough/TfXrNavigation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,IAAI,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AACzD,OAAO,EAAE,YAAY,IAAI,cAAc,EAAE,MAAM,6CAA6C,CAAC;AAkB7F;;;;GAIG;AACH,qBAAa,cAAe,SAAQ,cAAc;IAChD,MAAM,CAAC,UAAU,SAAoB;IAErC,yFAAyF;IACzF,iBAAiB,CAAC,EAAE,CAAC,WAAW,EAAE,aAAa,KAAK,OAAO,CAAC;IAE5D;;;;;OAKG;IACH,YAAY,OAAc;IAC1B,YAAY,SAAK;IAEjB,iGAAiG;IACjG,aAAa,SAAK;IAElB,OAAO,CAAC,SAAS,CAA8B;IAE/C,OAAO,CAAC,UAAU;IAIlB,WAAW,CAAC,WAAW,EAAE,aAAa;IAYtC,MAAM,CAAC,EAAE,EAAE,MAAM;IAmCjB;;;;;;;OAOG;IACH,UAAU;IAoBV;;;;;;;;;;OAUG;IACH,cAAc,CAAC,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,GAAG,EAAE;QAAE,KAAK,EAAE,IAAI,CAAC;QAAC,KAAK,EAAE,OAAO,CAAA;KAAE;CAkBnF"}
1
+ {"version":3,"file":"TfXrNavigation.d.ts","sourceRoot":"","sources":["../../../src/components/VirtualWalkthrough/TfXrNavigation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,IAAI,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AACzD,OAAO,EAAE,YAAY,IAAI,cAAc,EAAE,MAAM,6CAA6C,CAAC;AAoB7F;;;;GAIG;AACH,qBAAa,cAAe,SAAQ,cAAc;IAChD,MAAM,CAAC,UAAU,SAAoB;IAErC,yFAAyF;IACzF,iBAAiB,CAAC,EAAE,CAAC,WAAW,EAAE,aAAa,KAAK,OAAO,CAAC;IAE5D;;;;;OAKG;IACH,YAAY,OAAc;IAC1B,YAAY,SAAK;IAEjB,iGAAiG;IACjG,aAAa,SAAK;IAElB,OAAO,CAAC,SAAS,CAA8B;IAE/C,OAAO,CAAC,UAAU;IAIlB;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,iBAAiB;IA2BzB,OAAO,CAAC,cAAc;IAOtB,WAAW,CAAC,WAAW,EAAE,aAAa;IAYtC,MAAM,CAAC,EAAE,EAAE,MAAM;IAqCjB;;;;;;;OAOG;IACH,UAAU;IAoBV;;;;;;;;;;OAUG;IACH,cAAc,CAAC,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,GAAG,EAAE;QAAE,KAAK,EAAE,IAAI,CAAC;QAAC,KAAK,EAAE,OAAO,CAAA;KAAE;CAkBnF"}
@@ -30,6 +30,50 @@ class TfXrNavigation extends XrNavigation {
30
30
  _isBlocked(inputSource) {
31
31
  return !this.enableTeleport || this.isTeleportBlocked?.(inputSource) === true;
32
32
  }
33
+
34
+ /**
35
+ * Tracks controllers the base script never heard about.
36
+ *
37
+ * Movement, turning and teleport all iterate `_inputSources`, which the base fills from the XR
38
+ * input `add` event it subscribes to in initialize. A walkthrough that adopts the host's camera is
39
+ * mounted into a session that is already running, so the controllers present at session start
40
+ * fired `add` before this script existed and every control is inert for the whole session.
41
+ *
42
+ * The sources are picked up by hand rather than by re-firing `add` on the input manager, which
43
+ * would also reach the host's own listeners and have them handle controllers they already know
44
+ * about. The handlers mirror the base's and are stored in its map, so its teardown detaches them.
45
+ */
46
+ _syncInputSources() {
47
+ const internals = this;
48
+ for (const inputSource of this.app.xr?.input?.inputSources ?? []) {
49
+ if (internals._inputSources.has(inputSource)) {
50
+ continue;
51
+ }
52
+ const handleSelectStart = () => {
53
+ internals._activePointers.set(inputSource, true);
54
+ internals._arcHits.delete(inputSource);
55
+ };
56
+ const handleSelectEnd = () => {
57
+ internals._activePointers.set(inputSource, false);
58
+ if (this.enableTeleport) {
59
+ this.tryTeleport(inputSource);
60
+ }
61
+ };
62
+ inputSource.on('selectstart', handleSelectStart);
63
+ inputSource.on('selectend', handleSelectEnd);
64
+ internals._inputHandlers.set(inputSource, {
65
+ handleSelectStart,
66
+ handleSelectEnd
67
+ });
68
+ internals._inputSources.add(inputSource);
69
+ }
70
+ }
71
+ _resolveCamera() {
72
+ const internals = this;
73
+ if (!internals._cameraEntity) {
74
+ internals._cameraEntity = this.entity.findComponent('camera')?.entity ?? null;
75
+ }
76
+ }
33
77
  tryTeleport(inputSource) {
34
78
  // The latch decides on the frames the press actually aimed through, because super.tryTeleport
35
79
  // commits the arc hit cached back then rather than re-tracing the release-time ray. The live
@@ -41,6 +85,8 @@ class TfXrNavigation extends XrNavigation {
41
85
  super.tryTeleport(inputSource);
42
86
  }
43
87
  update(dt) {
88
+ this._resolveCamera();
89
+ this._syncInputSources();
44
90
  super.update(dt);
45
91
  const internals = this;
46
92
  for (const inputSource of internals._inputSources) {
@@ -13,6 +13,18 @@ export interface VirtualWalkthroughViewerProps {
13
13
  /**
14
14
  * Camera component mounted on the camera entity. Defaults to a 60 degree camera clearing to the
15
15
  * horizon color.
16
+ *
17
+ * Pass null to mount no camera of the walkthrough's own, which is what a caller sharing a scene
18
+ * it does not own wants: an XR session is welded to the camera it was started on, so a
19
+ * walkthrough that brought its own could only take over by ending the session and dropping the
20
+ * user out of the headset. The walkthrough adopts the scene's existing camera into its rig
21
+ * instead, for as long as it is mounted.
22
+ *
23
+ * A scene shared this way is expected to hold exactly one camera. The adoption takes the first
24
+ * camera in graph traversal order, which is only certain to be the one the headset renders
25
+ * through while it is the only one there: a scene that also carries a spectator or overlay camera
26
+ * can hand the rig the wrong entity, and locomotion then goes inert with nothing reported. Prefer
27
+ * the session's own camera, `app.xr.camera`, over the traversal if that ever needs to hold.
16
28
  */
17
29
  camera?: ReactNode;
18
30
  /** World-space point the camera looks at. */
@@ -1 +1 @@
1
- {"version":3,"file":"VirtualWalkthroughViewer.d.ts","sourceRoot":"","sources":["../../../src/components/VirtualWalkthrough/VirtualWalkthroughViewer.tsx"],"names":[],"mappings":"AAAA,OAAc,EAAE,SAAS,EAAqD,MAAM,OAAO,CAAC;AAY5F,OAAmB,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAO3D,OAAmB,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAS3D,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAmB5C;;;;GAIG;AACH,MAAM,WAAW,6BAA6B;IAC5C,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B;;;OAGG;IACH,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,6CAA6C;IAC7C,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IAClC,8CAA8C;IAC9C,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IAC1C,2FAA2F;IAC3F,WAAW,CAAC,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC7D,uEAAuE;IACvE,WAAW,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC;IACzC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6CAA6C;IAC7C,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC,eAAe,CAAC,CAAC;IAC3C;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;;OAGG;IACH,QAAQ,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,KAAK,KAAK,IAAI,CAAC;IACnC;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,WAAW,EAAE,eAAe,EAAE,CAAC;IAC/B,iBAAiB,EAAE,CAAC,WAAW,EAAE,eAAe,EAAE,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5E,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,kBAAkB,CAAC,EAAE,MAAM,IAAI,CAAC;IAChC,sBAAsB,CAAC,EAAE,MAAM,CAAC;CACjC;AAED,QAAA,MAAM,wBAAwB,GAAI,oUAuB/B,6BAA6B,4CAqa/B,CAAC;AAEF,eAAe,wBAAwB,CAAC"}
1
+ {"version":3,"file":"VirtualWalkthroughViewer.d.ts","sourceRoot":"","sources":["../../../src/components/VirtualWalkthrough/VirtualWalkthroughViewer.tsx"],"names":[],"mappings":"AAAA,OAAc,EAAE,SAAS,EAAqD,MAAM,OAAO,CAAC;AAa5F,OAAmB,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAO3D,OAAmB,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAS3D,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAoB5C;;;;GAIG;AACH,MAAM,WAAW,6BAA6B;IAC5C,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B;;;;;;;;;;;;;;;OAeG;IACH,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,6CAA6C;IAC7C,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IAClC,8CAA8C;IAC9C,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IAC1C,2FAA2F;IAC3F,WAAW,CAAC,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC7D,uEAAuE;IACvE,WAAW,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC;IACzC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6CAA6C;IAC7C,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC,eAAe,CAAC,CAAC;IAC3C;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;;OAGG;IACH,QAAQ,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,KAAK,KAAK,IAAI,CAAC;IACnC;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,WAAW,EAAE,eAAe,EAAE,CAAC;IAC/B,iBAAiB,EAAE,CAAC,WAAW,EAAE,eAAe,EAAE,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5E,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,kBAAkB,CAAC,EAAE,MAAM,IAAI,CAAC;IAChC,sBAAsB,CAAC,EAAE,MAAM,CAAC;CACjC;AAED,QAAA,MAAM,wBAAwB,GAAI,oUAuB/B,6BAA6B,4CAkb/B,CAAC;AAEF,eAAe,wBAAwB,CAAC"}
@@ -5,6 +5,7 @@ import { Script, Camera } from '@playcanvas/react/components';
5
5
  import { useApp } from '@playcanvas/react/hooks';
6
6
  import { Vec3, Color } from 'playcanvas';
7
7
  import { XrControllers } from 'playcanvas/scripts/esm/xr/xr-controllers.mjs';
8
+ import { CameraEntityContext } from '../../hooks/cameraEntityContext.js';
8
9
  import { useCameraPosition } from '../../hooks/useCameraPosition.js';
9
10
  import { useDevicePerformance } from '../../hooks/useDevicePerformance.js';
10
11
  import useXr from '../../hooks/useXr.js';
@@ -24,10 +25,11 @@ import XrExitButton from './XrExitButton.js';
24
25
  import XrGazeDwell from './XrGazeDwell.js';
25
26
  import XrPointerRay from './XrPointerRay.js';
26
27
  import { XrStartPose } from './XrStartPose.js';
28
+ import useAdoptedCamera from './useAdoptedCamera.js';
27
29
  import useXrRenderTuning from './useXrRenderTuning.js';
28
30
  import { WalkthroughCamera } from './walkthrough-camera.js';
29
31
  import { rayHitsInteractiveUi } from './xr-interactive-ui.js';
30
- import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
32
+ import { jsx, jsxs } from 'react/jsx-runtime';
31
33
 
32
34
  const DEFAULT_FOCUS_POINT = [0, 0.1, 0];
33
35
  const DEFAULT_POSITION = [1, 0.1, 0];
@@ -77,9 +79,19 @@ const VirtualWalkthroughViewer = ({
77
79
  maxImagesPerAnnotation
78
80
  }) => {
79
81
  const theme = useTheme();
82
+ // Held as state so the effects and hooks that address these entities re-run once they
83
+ // exist. Addressed directly: a viewer mounted into a host's scene shares the graph
84
+ // with the host's entities, so the camera can be found by component type.
85
+ const [cameraRoot, setCameraRoot] = useState(null);
86
+ const [cameraEntity, setCameraEntity] = useState(null);
87
+ const adoptedCamera = useAdoptedCamera(cameraRoot, cameraEntity);
88
+ // The entity carrying the live camera component: the viewer's own, or the host's while adopted.
89
+ // Everything that poses the camera or projects through it addresses this rather than the entity
90
+ // the viewer mounts a camera of its own on, which holds nothing while a camera is adopted.
91
+ const activeCamera = adoptedCamera ?? cameraEntity;
80
92
  const {
81
93
  setCamera
82
- } = useCameraPosition();
94
+ } = useCameraPosition(activeCamera);
83
95
  const {
84
96
  isHighPerformance
85
97
  } = useDevicePerformance();
@@ -163,27 +175,27 @@ const VirtualWalkthroughViewer = ({
163
175
  if (!groundPlane.length || isCurrentlyInXr) {
164
176
  return;
165
177
  }
166
- // @ts-expect-error - scripts are added dynamically to the camera entity
167
- const walkthroughCam = app.root.findByName('camera')?.script?.walkthroughCamera;
178
+ // @ts-expect-error - scripts are added dynamically to the entity
179
+ const walkthroughCam = cameraRoot?.script?.walkthroughCamera;
168
180
  if (walkthroughCam) {
169
181
  // Should be changed to a react prop if shallowEquals in playcanvas/react is fixed (see https://github.com/playcanvas/react/pull/298)
170
182
  walkthroughCam.groundPlane = groundPlane;
171
183
  }
172
- }, [groundPlane, app, isCurrentlyInXr]);
184
+ }, [groundPlane, cameraRoot, isCurrentlyInXr]);
173
185
  useEffect(() => {
174
186
  // Set imperatively for the same reason as BoundaryRing: boundsCenter is a Vec3, and the
175
187
  // @playcanvas/react memo() comparator stops at the first prop with an .equals() method.
176
188
  // @ts-expect-error - scripts are added dynamically to the entity
177
- const navigation = app.root.findByName('camera-root')?.script?.tfXrNavigation;
189
+ const navigation = cameraRoot?.script?.tfXrNavigation;
178
190
  if (navigation) {
179
191
  navigation.boundsCenter = sceneBoundsCenter;
180
192
  navigation.boundsRadius = sceneBoundsRadius;
181
193
  }
182
- }, [app, sceneBoundsCenter, sceneBoundsRadius]);
194
+ }, [cameraRoot, sceneBoundsCenter, sceneBoundsRadius]);
183
195
  const handleToggleFreeFly = useCallback(() => {
184
196
  const newFreeFly = !isFreeFly;
185
- // @ts-expect-error - scripts are added dynamically to the camera entity
186
- const walkthroughCam = app.root.findByName('camera')?.script?.walkthroughCamera;
197
+ // @ts-expect-error - scripts are added dynamically to the entity
198
+ const walkthroughCam = cameraRoot?.script?.walkthroughCamera;
187
199
  if (walkthroughCam) {
188
200
  walkthroughCam.freeFly = newFreeFly;
189
201
  }
@@ -191,7 +203,7 @@ const VirtualWalkthroughViewer = ({
191
203
  setCamera(origin, cameraPosition);
192
204
  }
193
205
  setIsFreeFly(newFreeFly);
194
- }, [isFreeFly, app, setCamera, origin, cameraPosition]);
206
+ }, [isFreeFly, cameraRoot, setCamera, origin, cameraPosition]);
195
207
  useEffect(() => {
196
208
  setLocalAnnotations(annotations);
197
209
  }, [annotations]);
@@ -301,22 +313,25 @@ const VirtualWalkthroughViewer = ({
301
313
  scaleFactor: scaleFactor,
302
314
  revealRain: isHighPerformance
303
315
  }, 'splat'), [isHighPerformance, splatSrc, splatFormat, scaleFactor, splatModelProps]);
304
- return /*#__PURE__*/jsxs(Fragment, {
316
+ return /*#__PURE__*/jsxs(CameraEntityContext.Provider, {
317
+ value: activeCamera,
305
318
  children: [/*#__PURE__*/jsx(GradientSky, {
306
319
  topColor: skyColor || '#FFFFFF',
307
320
  horizonColor: skyColor || '#EAF8FF',
308
321
  groundColor: groundColor || '#C3BDB7'
309
322
  }), /*#__PURE__*/jsxs(Entity, {
310
323
  name: "camera-root",
311
- children: [/*#__PURE__*/jsxs(Entity, {
324
+ ref: setCameraRoot,
325
+ children: [/*#__PURE__*/jsx(Entity, {
312
326
  name: "camera",
313
- children: [camera, !isCurrentlyInXr && /*#__PURE__*/jsx(Script, {
314
- script: WalkthroughCamera,
315
- boundsCenter: sceneBoundsCenter,
316
- boundsRadius: sceneBoundsRadius,
317
- enableFly: !isTextFieldFocused,
318
- averageCameraHeight: averageCameraHeight
319
- })]
327
+ ref: setCameraEntity,
328
+ children: camera
329
+ }), !isCurrentlyInXr && /*#__PURE__*/jsx(Script, {
330
+ script: WalkthroughCamera,
331
+ boundsCenter: sceneBoundsCenter,
332
+ boundsRadius: sceneBoundsRadius,
333
+ enableFly: !isTextFieldFocused,
334
+ averageCameraHeight: averageCameraHeight
320
335
  }), /*#__PURE__*/jsx(XrExitButton, {
321
336
  onClose: onXrClose
322
337
  }), /*#__PURE__*/jsx(Script, {
@@ -1 +1 @@
1
- {"version":3,"file":"boundary-wall.d.ts","sourceRoot":"","sources":["../../../src/components/VirtualWalkthrough/boundary-wall.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,EAOL,MAAM,EAEN,IAAI,EACL,MAAM,YAAY,CAAC;AAKpB,MAAM,MAAM,0BAA0B,GAAG;IACvC,MAAM,EAAE,IAAI,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,gHAAgH;IAChH,WAAW,EAAE,IAAI,EAAE,CAAC;IACpB,qEAAqE;IACrE,KAAK,EAAE,MAAM,CAAC;IACd,sGAAsG;IACtG,IAAI,EAAE,MAAM,CAAC;IACb,yGAAyG;IACzG,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,oBAAoB,GAAG;IACjC,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,GAAG,EAAE,MAAM,EAAE,CAAC;IACd,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,gBAAgB,GAAI,sEAQ9B,0BAA0B,KAAG,oBAqD/B,CAAC;AA2EF;;;;;;;GAOG;AACH,qBAAa,kBAAmB,SAAQ,MAAM;IAC5C,MAAM,CAAC,UAAU,SAAkB;IAEnC,MAAM,OAAc;IACpB,MAAM,SAAK;IACX,WAAW,EAAE,IAAI,EAAE,CAAM;IACzB,uEAAuE;IACvE,KAAK,SAAK;IACV;;;OAGG;IACH,IAAI,SAAO;IACX,QAAQ,SAAM;IACd,WAAW,SAAO;IAClB,2CAA2C;IAC3C,SAAS,SAAS;IAClB,0EAA0E;IAC1E,YAAY,SAAO;IACnB,0EAA0E;IAC1E,gBAAgB,SAAO;IACvB,SAAS,QAA2B;IACpC,SAAS,QAA2B;IAEpC,OAAO,CAAC,SAAS,CAAC,CAAiB;IACnC,OAAO,CAAC,KAAK,CAAC,CAAO;IACrB,OAAO,CAAC,OAAO,CAAuB;IACtC,OAAO,CAAC,WAAW,CAAuC;IAC1D,OAAO,CAAC,UAAU,CAAuB;IAEzC,UAAU;IAwBV;;;;OAIG;IACH,OAAO,CAAC,oBAAoB;IAW5B,OAAO;IA6CP,MAAM;IAgCN,OAAO,CAAC,UAAU;CASnB"}
1
+ {"version":3,"file":"boundary-wall.d.ts","sourceRoot":"","sources":["../../../src/components/VirtualWalkthrough/boundary-wall.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,EAOL,MAAM,EAEN,IAAI,EACL,MAAM,YAAY,CAAC;AAKpB,MAAM,MAAM,0BAA0B,GAAG;IACvC,MAAM,EAAE,IAAI,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,gHAAgH;IAChH,WAAW,EAAE,IAAI,EAAE,CAAC;IACpB,qEAAqE;IACrE,KAAK,EAAE,MAAM,CAAC;IACd,sGAAsG;IACtG,IAAI,EAAE,MAAM,CAAC;IACb,yGAAyG;IACzG,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,oBAAoB,GAAG;IACjC,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,GAAG,EAAE,MAAM,EAAE,CAAC;IACd,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,gBAAgB,GAAI,sEAQ9B,0BAA0B,KAAG,oBAqD/B,CAAC;AA2EF;;;;;;;GAOG;AACH,qBAAa,kBAAmB,SAAQ,MAAM;IAC5C,MAAM,CAAC,UAAU,SAAkB;IAEnC,MAAM,OAAc;IACpB,MAAM,SAAK;IACX,WAAW,EAAE,IAAI,EAAE,CAAM;IACzB,uEAAuE;IACvE,KAAK,SAAK;IACV;;;OAGG;IACH,IAAI,SAAO;IACX,QAAQ,SAAM;IACd,WAAW,SAAO;IAClB,2CAA2C;IAC3C,SAAS,SAAS;IAClB,0EAA0E;IAC1E,YAAY,SAAO;IACnB,0EAA0E;IAC1E,gBAAgB,SAAO;IACvB,SAAS,QAA2B;IACpC,SAAS,QAA2B;IAEpC,OAAO,CAAC,SAAS,CAAC,CAAiB;IACnC,OAAO,CAAC,KAAK,CAAC,CAAO;IACrB,OAAO,CAAC,OAAO,CAAuB;IACtC,OAAO,CAAC,WAAW,CAAuC;IAC1D,OAAO,CAAC,UAAU,CAAuB;IAEzC,UAAU;IAwBV;;;;OAIG;IACH,OAAO,CAAC,oBAAoB;IAW5B,OAAO;IA6CP,MAAM;IAgCN,OAAO,CAAC,UAAU;CASnB"}
@@ -237,7 +237,7 @@ class BoundaryWallScript extends Script {
237
237
  */
238
238
  _resolveDependencies() {
239
239
  if (!this._camera) {
240
- this._camera = this.app.root.findByName('camera');
240
+ this._camera = this.app.root.findComponent('camera')?.entity ?? null;
241
241
  }
242
242
  if (!this._navigation) {
243
243
  const rig = this.app.root.findByName('camera-root');
@@ -0,0 +1,39 @@
1
+ import { Entity } from 'playcanvas';
2
+ /**
3
+ * A camera the walkthrough took over, and the call that hands it back.
4
+ */
5
+ export interface AdoptedCamera {
6
+ /** The entity now carrying the live camera component, which the rig poses and projects from. */
7
+ camera: Entity;
8
+ release: () => void;
9
+ }
10
+ /**
11
+ * Hands the scene's existing camera to a walkthrough rig that hasn't been given one.
12
+ *
13
+ * A caller mounting the viewer into a scene it doesn't own passes `camera={null}`, because an XR
14
+ * session is welded to the camera component it was started on — `XrManager.camera` is read-only and
15
+ * `start` refuses while a session is running — so a walkthrough that brought a camera of its own
16
+ * could only become the session's camera by ending it, which drops the user out of the headset.
17
+ *
18
+ * That leaves the rig with no camera under it, and everything that poses or solves against the head
19
+ * resolves it with `findComponent('camera')` on the rig: `WalkthroughCamera`'s pointer and keyboard
20
+ * walk, `XrStartPose`, `XrNavigation`'s thumbstick movement and turning, and the bounds clamp all go
21
+ * inert, so the head is never placed at the walkthrough's start point and can never be moved from
22
+ * wherever the host scene left it. Splat LOD is chosen from
23
+ * the camera's distance to each octree node, so the far side of the model stays coarse for the whole
24
+ * session no matter which way the headset turns.
25
+ *
26
+ * Reparenting carries the live camera component with it, so the session, its views, and the host's
27
+ * own clear color and clip planes are all untouched — only which entity the camera hangs off
28
+ * changes. The camera becomes a direct child of the rig, which is exactly where a camera the viewer
29
+ * owns sits, so the rig means the same thing either way.
30
+ *
31
+ * @param root - The scene root to look for a camera in, expected to hold exactly one camera.
32
+ * @param rig - The entity the walkthrough's XR scripts move, `camera-root`.
33
+ * @param ownCamera - The entity the viewer mounts its own camera on. Nothing is adopted when it
34
+ * already carries one.
35
+ * @returns The adopted camera and the call that puts it back, or null when there was nothing to
36
+ * adopt.
37
+ */
38
+ export declare const adoptSceneCamera: (root: Entity, rig: Entity, ownCamera: Entity) => AdoptedCamera | null;
39
+ //# sourceMappingURL=camera-adoption.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"camera-adoption.d.ts","sourceRoot":"","sources":["../../../src/components/VirtualWalkthrough/camera-adoption.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmB,MAAM,EAAc,MAAM,YAAY,CAAC;AAWjE;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,gGAAgG;IAChG,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,eAAO,MAAM,gBAAgB,GAAI,MAAM,MAAM,EAAE,KAAK,MAAM,EAAE,WAAW,MAAM,KAAG,aAAa,GAAG,IA8B/F,CAAC"}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * The pose an adopted camera is put back to when the walkthrough releases it.
3
+ */
4
+
5
+ /**
6
+ * A camera the walkthrough took over, and the call that hands it back.
7
+ */
8
+
9
+ /**
10
+ * Hands the scene's existing camera to a walkthrough rig that hasn't been given one.
11
+ *
12
+ * A caller mounting the viewer into a scene it doesn't own passes `camera={null}`, because an XR
13
+ * session is welded to the camera component it was started on — `XrManager.camera` is read-only and
14
+ * `start` refuses while a session is running — so a walkthrough that brought a camera of its own
15
+ * could only become the session's camera by ending it, which drops the user out of the headset.
16
+ *
17
+ * That leaves the rig with no camera under it, and everything that poses or solves against the head
18
+ * resolves it with `findComponent('camera')` on the rig: `WalkthroughCamera`'s pointer and keyboard
19
+ * walk, `XrStartPose`, `XrNavigation`'s thumbstick movement and turning, and the bounds clamp all go
20
+ * inert, so the head is never placed at the walkthrough's start point and can never be moved from
21
+ * wherever the host scene left it. Splat LOD is chosen from
22
+ * the camera's distance to each octree node, so the far side of the model stays coarse for the whole
23
+ * session no matter which way the headset turns.
24
+ *
25
+ * Reparenting carries the live camera component with it, so the session, its views, and the host's
26
+ * own clear color and clip planes are all untouched — only which entity the camera hangs off
27
+ * changes. The camera becomes a direct child of the rig, which is exactly where a camera the viewer
28
+ * owns sits, so the rig means the same thing either way.
29
+ *
30
+ * @param root - The scene root to look for a camera in, expected to hold exactly one camera.
31
+ * @param rig - The entity the walkthrough's XR scripts move, `camera-root`.
32
+ * @param ownCamera - The entity the viewer mounts its own camera on. Nothing is adopted when it
33
+ * already carries one.
34
+ * @returns The adopted camera and the call that puts it back, or null when there was nothing to
35
+ * adopt.
36
+ */
37
+ const adoptSceneCamera = (root, rig, ownCamera) => {
38
+ if (ownCamera.camera) {
39
+ return null;
40
+ }
41
+ const camera = root.findComponent('camera')?.entity;
42
+ // isDescendantOf covers a host that mounted the walkthrough inside its own camera, where
43
+ // reparenting would make a cycle.
44
+ if (!camera || camera === rig || !camera.parent || rig.isDescendantOf(camera)) {
45
+ return null;
46
+ }
47
+ const previous = {
48
+ parent: camera.parent,
49
+ position: camera.getLocalPosition().clone(),
50
+ rotation: camera.getLocalRotation().clone()
51
+ };
52
+
53
+ // addChild keeps the local transform, so the host's authored camera pose survives the move and
54
+ // only has to be restored because a session overwrites it with the head pose every frame.
55
+ rig.addChild(camera);
56
+ return {
57
+ camera,
58
+ release: () => {
59
+ previous.parent.addChild(camera);
60
+ camera.setLocalPosition(previous.position);
61
+ camera.setLocalRotation(previous.rotation);
62
+ }
63
+ };
64
+ };
65
+
66
+ export { adoptSceneCamera };
@@ -0,0 +1,14 @@
1
+ import { Entity } from 'playcanvas';
2
+ /**
3
+ * Keeps the walkthrough's rig holding a camera for as long as the viewer is mounted, adopting the
4
+ * scene's own when the viewer wasn't given one. See {@link adoptSceneCamera} for why.
5
+ *
6
+ * @param rig - The `camera-root` entity, or null before it exists.
7
+ * @param ownCamera - The entity the viewer mounts its own camera on, or null before it exists.
8
+ * @returns The adopted camera, or null when the viewer brought one of its own. Held as state so the
9
+ * viewer re-renders and re-points everything that addresses the camera at the entity now carrying
10
+ * it.
11
+ */
12
+ export declare const useAdoptedCamera: (rig: Entity | null, ownCamera: Entity | null) => Entity | null;
13
+ export default useAdoptedCamera;
14
+ //# sourceMappingURL=useAdoptedCamera.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useAdoptedCamera.d.ts","sourceRoot":"","sources":["../../../src/components/VirtualWalkthrough/useAdoptedCamera.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAIpC;;;;;;;;;GASG;AACH,eAAO,MAAM,gBAAgB,GAAI,KAAK,MAAM,GAAG,IAAI,EAAE,WAAW,MAAM,GAAG,IAAI,KAAG,MAAM,GAAG,IA8BxF,CAAC;AAEF,eAAe,gBAAgB,CAAC"}
@@ -0,0 +1,45 @@
1
+ import { useState, useLayoutEffect } from 'react';
2
+ import { useApp } from '@playcanvas/react/hooks';
3
+ import { adoptSceneCamera } from './camera-adoption.js';
4
+
5
+ /**
6
+ * Keeps the walkthrough's rig holding a camera for as long as the viewer is mounted, adopting the
7
+ * scene's own when the viewer wasn't given one. See {@link adoptSceneCamera} for why.
8
+ *
9
+ * @param rig - The `camera-root` entity, or null before it exists.
10
+ * @param ownCamera - The entity the viewer mounts its own camera on, or null before it exists.
11
+ * @returns The adopted camera, or null when the viewer brought one of its own. Held as state so the
12
+ * viewer re-renders and re-points everything that addresses the camera at the entity now carrying
13
+ * it.
14
+ */
15
+ const useAdoptedCamera = (rig, ownCamera) => {
16
+ const app = useApp();
17
+ const [adopted, setAdopted] = useState(null);
18
+
19
+ // A layout effect, so that the camera is handed back before the rig it was parented into is
20
+ // destroyed. `@playcanvas/react`'s Entity destroys its entity in a layout effect's teardown, and
21
+ // destroying an entity destroys its children — so a release deferred to a passive effect runs
22
+ // after the host's camera has already been destroyed along with the walkthrough, leaving the host
23
+ // scene with no camera to render through at all.
24
+ //
25
+ // The rig is a child of the viewer, and React tears an unmounting tree's layout effects down from
26
+ // the top, so this runs first.
27
+ useLayoutEffect(() => {
28
+ if (!rig || !ownCamera) {
29
+ return;
30
+ }
31
+
32
+ // Both entities are state set from refs, so this runs a commit after the child that mounts the
33
+ // camera component: a viewer that was given a camera is recognised as owning one rather than
34
+ // adopted on top of.
35
+ const adoption = adoptSceneCamera(app.root, rig, ownCamera);
36
+ setAdopted(adoption?.camera ?? null);
37
+ return () => {
38
+ adoption?.release();
39
+ setAdopted(null);
40
+ };
41
+ }, [app, rig, ownCamera]);
42
+ return adopted;
43
+ };
44
+
45
+ export { useAdoptedCamera as default, useAdoptedCamera };
@@ -1,5 +1,6 @@
1
1
  /**
2
- * Holds the XR render settings in place for the duration of a session.
2
+ * Holds the XR render settings in place for the duration of a session, including one that is
3
+ * already running when this mounts.
3
4
  *
4
5
  * Call this once per application. The gsplat settings it writes are scene-global, so a second
5
6
  * caller would capture the first caller's tuned values as the state to restore, and the scene would
@@ -1 +1 @@
1
- {"version":3,"file":"useXrRenderTuning.d.ts","sourceRoot":"","sources":["../../../src/components/VirtualWalkthrough/useXrRenderTuning.ts"],"names":[],"mappings":"AAMA;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,YAsC7B,CAAC;AAEF,eAAe,iBAAiB,CAAC"}
1
+ {"version":3,"file":"useXrRenderTuning.d.ts","sourceRoot":"","sources":["../../../src/components/VirtualWalkthrough/useXrRenderTuning.ts"],"names":[],"mappings":"AAMA;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,YA8C7B,CAAC;AAEF,eAAe,iBAAiB,CAAC"}
@@ -3,7 +3,8 @@ import { useApp } from '@playcanvas/react/hooks';
3
3
  import { applyGsplatTuning, XR_FIXED_FOVEATION, restoreGsplatTuning } from './xr-render-tuning.js';
4
4
 
5
5
  /**
6
- * Holds the XR render settings in place for the duration of a session.
6
+ * Holds the XR render settings in place for the duration of a session, including one that is
7
+ * already running when this mounts.
7
8
  *
8
9
  * Call this once per application. The gsplat settings it writes are scene-global, so a second
9
10
  * caller would capture the first caller's tuned values as the state to restore, and the scene would
@@ -35,6 +36,14 @@ const useXrRenderTuning = () => {
35
36
  };
36
37
  xr.on('start', handleStart);
37
38
  xr.on('end', restore);
39
+
40
+ // A viewer can be mounted into a scene that is already in a headset, in which case `start`
41
+ // fired before this hook existed and waiting for it would leave the whole session rendering
42
+ // with the desktop settings. `handleStart` is idempotent, so a session that goes on to fire
43
+ // `start` anyway does not capture the tuned values as the ones to restore.
44
+ if (xr.active) {
45
+ handleStart();
46
+ }
38
47
  return () => {
39
48
  xr.off('start', handleStart);
40
49
  xr.off('end', restore);
@@ -34,7 +34,7 @@ export declare class WalkthroughCamera extends Script {
34
34
  * Script component is wrapped in memo() whose shallowEquals comparator returns early on the first prop
35
35
  * with a .equals() method (Vec3), so any prop listed after boundsCenter is never compared and the
36
36
  * component won't re-render when it changes. Set via:
37
- * app.root.findByName('camera')?.script?.walkthroughCamera.groundPlane = points
37
+ * cameraRoot.script.walkthroughCamera.groundPlane = points
38
38
  * Should be updated if shallowEquals is fixed (see https://github.com/playcanvas/react/pull/298)
39
39
  */
40
40
  groundPlane: Vec3[];
@@ -48,9 +48,16 @@ export declare class WalkthroughCamera extends Script {
48
48
  private _scrollDelta;
49
49
  private _keys;
50
50
  private _removeListeners;
51
+ private _camera;
51
52
  private _planeNormal;
52
53
  private _planePoint;
53
54
  private _hasGroundPlane;
55
+ /**
56
+ * The camera to pose, or null while there is none to pose. The entity this script is mounted on is
57
+ * never posed in its place: on the walkthrough that is the rig, which is the frame an XR session
58
+ * is measured from and whose height the session's floor is taken from.
59
+ */
60
+ private _posedCamera;
54
61
  private _syncGroundPlane;
55
62
  initialize(): void;
56
63
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"walkthrough-camera.d.ts","sourceRoot":"","sources":["../../../src/components/VirtualWalkthrough/walkthrough-camera.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,MAAM,EAAE,IAAI,EAAQ,MAAM,YAAY,CAAC;AA8BtD,qBAAa,iBAAkB,SAAQ,MAAM;IAC3C,MAAM,CAAC,UAAU,SAAuB;IAExC,SAAS,SAAO;IAEhB,iBAAiB;IACjB,aAAa,SAAO;IAEpB,iBAAiB;IACjB,aAAa,SAAQ;IAErB,iBAAiB;IACjB,eAAe,SAAO;IAEtB,iBAAiB;IACjB,aAAa,SAAQ;IAErB,iBAAiB;IACjB,QAAQ,SAAO;IAEf,iBAAiB;IACjB,QAAQ,SAAM;IAEd,iBAAiB;IACjB,WAAW,SAAS;IAEpB,iBAAiB;IACjB,YAAY,OAAqB;IAEjC,iBAAiB;IACjB,YAAY,SAAM;IAElB,iBAAiB;IACjB,OAAO,UAAS;IAEhB,iBAAiB;IACjB,SAAS,UAAQ;IAEjB,iBAAiB;IACjB,mBAAmB,SAAK;IAExB;;;;;;;;;;OAUG;IACH,WAAW,EAAE,IAAI,EAAE,CAAM;IAGzB,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,IAAI,CAAK;IAGjB,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,UAAU,CAAK;IAEvB,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,KAAK,CAAwC;IACrD,OAAO,CAAC,gBAAgB,CAAsB;IAG9C,OAAO,CAAC,YAAY,CAAqB;IACzC,OAAO,CAAC,WAAW,CAAqB;IACxC,OAAO,CAAC,eAAe,CAAS;IAEhC,OAAO,CAAC,gBAAgB;IAaxB,UAAU;IAkFV;;;OAGG;IACH,KAAK,CAAC,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI;IAsBjC,IAAI,UAAU,IAAI,MAAM,CAEvB;IAED,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED;;;OAGG;IACH,SAAS,CAAC,QAAQ,EAAE,MAAM;IAsB1B;;;OAGG;IACH,IAAI,UAAU,IAAI,IAAI,CAMrB;IAED,MAAM,CAAC,EAAE,EAAE,MAAM;IA4FjB,OAAO;CAIR"}
1
+ {"version":3,"file":"walkthrough-camera.d.ts","sourceRoot":"","sources":["../../../src/components/VirtualWalkthrough/walkthrough-camera.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiC,MAAM,EAAE,IAAI,EAAQ,MAAM,YAAY,CAAC;AAmC/E,qBAAa,iBAAkB,SAAQ,MAAM;IAC3C,MAAM,CAAC,UAAU,SAAuB;IAExC,SAAS,SAAO;IAEhB,iBAAiB;IACjB,aAAa,SAAO;IAEpB,iBAAiB;IACjB,aAAa,SAAQ;IAErB,iBAAiB;IACjB,eAAe,SAAO;IAEtB,iBAAiB;IACjB,aAAa,SAAQ;IAErB,iBAAiB;IACjB,QAAQ,SAAO;IAEf,iBAAiB;IACjB,QAAQ,SAAM;IAEd,iBAAiB;IACjB,WAAW,SAAS;IAEpB,iBAAiB;IACjB,YAAY,OAAqB;IAEjC,iBAAiB;IACjB,YAAY,SAAM;IAElB,iBAAiB;IACjB,OAAO,UAAS;IAEhB,iBAAiB;IACjB,SAAS,UAAQ;IAEjB,iBAAiB;IACjB,mBAAmB,SAAK;IAExB;;;;;;;;;;OAUG;IACH,WAAW,EAAE,IAAI,EAAE,CAAM;IAGzB,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,IAAI,CAAK;IAGjB,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,UAAU,CAAK;IAEvB,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,KAAK,CAAwC;IACrD,OAAO,CAAC,gBAAgB,CAAsB;IAI9C,OAAO,CAAC,OAAO,CAAuB;IAGtC,OAAO,CAAC,YAAY,CAAqB;IACzC,OAAO,CAAC,WAAW,CAAqB;IACxC,OAAO,CAAC,eAAe,CAAS;IAEhC;;;;OAIG;IACH,OAAO,CAAC,YAAY;IAMpB,OAAO,CAAC,gBAAgB;IAaxB,UAAU;IAkFV;;;OAGG;IACH,KAAK,CAAC,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI;IAuBjC,IAAI,UAAU,IAAI,MAAM,CAEvB;IAED,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED;;;OAGG;IACH,SAAS,CAAC,QAAQ,EAAE,MAAM;IA0B1B;;;OAGG;IACH,IAAI,UAAU,IAAI,IAAI,CAMrB;IAED,MAAM,CAAC,EAAE,EAAE,MAAM;IAgGjB,OAAO;CAIR"}
@@ -12,6 +12,11 @@ import { computeGroundPlane, yOnPlane } from './groundPlane.js';
12
12
  * - Y position is clamped between boundsMin.y and boundsMax.y; set them equal
13
13
  * to lock vertical position entirely.
14
14
  * - Exposes reset() and focusPoint to stay compatible with useCameraPosition.
15
+ *
16
+ * The pose is written to the camera under this script's entity rather than to the entity itself, so
17
+ * mounting it on the walkthrough's rig drives whichever camera the rig is holding — one the viewer
18
+ * brought, or one it adopted from the host scene. Mounted straight onto a camera entity it still
19
+ * drives that entity, which `findComponent` resolves to itself.
15
20
  */
16
21
 
17
22
  const _quat = new Quat();
@@ -74,7 +79,7 @@ class WalkthroughCamera extends Script {
74
79
  * Script component is wrapped in memo() whose shallowEquals comparator returns early on the first prop
75
80
  * with a .equals() method (Vec3), so any prop listed after boundsCenter is never compared and the
76
81
  * component won't re-render when it changes. Set via:
77
- * app.root.findByName('camera')?.script?.walkthroughCamera.groundPlane = points
82
+ * cameraRoot.script.walkthroughCamera.groundPlane = points
78
83
  * Should be updated if shallowEquals is fixed (see https://github.com/playcanvas/react/pull/298)
79
84
  */
80
85
  groundPlane = [];
@@ -93,10 +98,24 @@ class WalkthroughCamera extends Script {
93
98
  _keys = {};
94
99
  _removeListeners = [];
95
100
 
101
+ // The camera being posed, resolved lazily. Held until it exists: a camera adopted from the host
102
+ // scene is reparented into the rig by an effect that runs after the scripts are mounted.
103
+ _camera = null;
104
+
96
105
  // Cached plane derived from groundPlane attribute. Computed once when 3 points become available.
97
106
  _planeNormal = new Vec3(0, 1, 0);
98
107
  _planePoint = new Vec3(0, 0, 0);
99
108
  _hasGroundPlane = false;
109
+
110
+ /**
111
+ * The camera to pose, or null while there is none to pose. The entity this script is mounted on is
112
+ * never posed in its place: on the walkthrough that is the rig, which is the frame an XR session
113
+ * is measured from and whose height the session's floor is taken from.
114
+ */
115
+ _posedCamera() {
116
+ this._camera ??= this.entity.findComponent('camera')?.entity ?? null;
117
+ return this._camera;
118
+ }
100
119
  _syncGroundPlane() {
101
120
  if (this._hasGroundPlane || this.groundPlane.length < 3) {
102
121
  return;
@@ -179,7 +198,8 @@ class WalkthroughCamera extends Script {
179
198
  * Compatible with the useCameraPosition hook's reset() call.
180
199
  */
181
200
  reset(focus, position) {
182
- this.entity.setPosition(position);
201
+ const camera = this._posedCamera();
202
+ camera?.setPosition(position);
183
203
  const dx = focus.x - position.x;
184
204
  const dy = focus.y - position.y;
185
205
  const dz = focus.z - position.z;
@@ -195,7 +215,7 @@ class WalkthroughCamera extends Script {
195
215
  // Snap targets so there's no damped drift from the previous pose.
196
216
  this._targetPitch = this._pitch;
197
217
  this._targetYaw = this._yaw;
198
- this.entity.setEulerAngles(this._pitch, this._yaw, 0);
218
+ camera?.setEulerAngles(this._pitch, this._yaw, 0);
199
219
  }
200
220
  get currentYaw() {
201
221
  return this._targetYaw;
@@ -210,7 +230,11 @@ class WalkthroughCamera extends Script {
210
230
  */
211
231
  orbitStep(yawDelta) {
212
232
  this._syncGroundPlane();
213
- const pos = this.entity.getPosition();
233
+ const camera = this._posedCamera();
234
+ if (!camera) {
235
+ return;
236
+ }
237
+ const pos = camera.getPosition();
214
238
  const dx = pos.x - this.boundsCenter.x;
215
239
  const dz = pos.z - this.boundsCenter.z;
216
240
  const radius = Math.sqrt(dx * dx + dz * dz) || this.boundsRadius * 0.5;
@@ -219,12 +243,12 @@ class WalkthroughCamera extends Script {
219
243
  const nx = this.boundsCenter.x + radius * Math.cos(newAngle);
220
244
  const nz = this.boundsCenter.z + radius * Math.sin(newAngle);
221
245
  const ny = this._hasGroundPlane ? yOnPlane(nx, nz, this._planeNormal, this._planePoint, this.boundsCenter.y) + this.averageCameraHeight : this.boundsCenter.y;
222
- this.entity.setPosition(nx, ny, nz);
246
+ camera.setPosition(nx, ny, nz);
223
247
  const faceDx = this.boundsCenter.x - nx;
224
248
  const faceDz = this.boundsCenter.z - nz;
225
249
  this._yaw = Math.atan2(-faceDx, -faceDz) * (180 / Math.PI);
226
250
  this._targetYaw = this._yaw;
227
- this.entity.setEulerAngles(this._pitch, this._yaw, 0);
251
+ camera.setEulerAngles(this._pitch, this._yaw, 0);
228
252
  }
229
253
 
230
254
  /**
@@ -232,13 +256,17 @@ class WalkthroughCamera extends Script {
232
256
  * Compatible with the useCameraPosition hook's focusPoint read.
233
257
  */
234
258
  get focusPoint() {
235
- const pos = this.entity.getPosition();
259
+ const pos = (this._posedCamera() ?? this.entity).getPosition();
236
260
  _quat.setFromEulerAngles(this._pitch, this._yaw, 0);
237
261
  _quat.transformVector(Vec3.FORWARD, _flatForward);
238
262
  return new Vec3(pos.x + _flatForward.x, pos.y + _flatForward.y, pos.z + _flatForward.z);
239
263
  }
240
264
  update(dt) {
241
265
  this._syncGroundPlane();
266
+ const camera = this._posedCamera();
267
+ if (!camera) {
268
+ return;
269
+ }
242
270
 
243
271
  // Lerp current angles toward targets using the same damping as CameraControls.
244
272
  // Snap to the target once within ANGLE_EPSILON so the asymptotic lerp actually
@@ -262,9 +290,9 @@ class WalkthroughCamera extends Script {
262
290
  this._pitch = nextPitch;
263
291
  this._yaw = nextYaw;
264
292
  if (anglesChanged) {
265
- this.entity.setEulerAngles(this._pitch, this._yaw, 0);
293
+ camera.setEulerAngles(this._pitch, this._yaw, 0);
266
294
  }
267
- const pos = this.entity.getPosition();
295
+ const pos = camera.getPosition();
268
296
  let nx = pos.x;
269
297
  let ny = pos.y;
270
298
  let nz = pos.z;
@@ -308,7 +336,7 @@ class WalkthroughCamera extends Script {
308
336
  // idle frames; note it does not stop per-frame rasterization — while autoRender is on
309
337
  // the app renders every frame regardless of whether the transform changed.
310
338
  if (Math.abs(nx - pos.x) > POSITION_EPSILON || Math.abs(targetY - pos.y) > POSITION_EPSILON || Math.abs(nz - pos.z) > POSITION_EPSILON) {
311
- this.entity.setPosition(nx, targetY, nz);
339
+ camera.setPosition(nx, targetY, nz);
312
340
  }
313
341
  }
314
342
  destroy() {
@@ -1 +1 @@
1
- {"version":3,"file":"xr-annotation-interaction.d.ts","sourceRoot":"","sources":["../../../src/components/VirtualWalkthrough/xr-annotation-interaction.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAkC,MAAM,YAAY,CAAC;AAOpE,qBAAa,uBAAwB,SAAQ,MAAM;IACjD,MAAM,CAAC,UAAU,SAA6B;IAE9C,iBAAiB,CAAC,EAAE,MAAM,IAAI,CAAC;IAE/B,OAAO,CAAC,YAAY,CAAgC;IAEpD,OAAO,CAAC,WAAW,CAAyE;IAE5F,OAAO,CAAC,QAAQ,CAId;IAEF,OAAO,CAAC,SAAS,CAKf;IAEF,OAAO,CAAC,uBAAuB;IAqB/B,UAAU;IAKV,MAAM;CAeP"}
1
+ {"version":3,"file":"xr-annotation-interaction.d.ts","sourceRoot":"","sources":["../../../src/components/VirtualWalkthrough/xr-annotation-interaction.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmB,MAAM,EAAkC,MAAM,YAAY,CAAC;AAOrF,qBAAa,uBAAwB,SAAQ,MAAM;IACjD,MAAM,CAAC,UAAU,SAA6B;IAE9C,iBAAiB,CAAC,EAAE,MAAM,IAAI,CAAC;IAE/B,OAAO,CAAC,YAAY,CAAgC;IAEpD,OAAO,CAAC,WAAW,CAAyE;IAE5F,OAAO,CAAC,QAAQ,CAId;IAEF,OAAO,CAAC,SAAS,CAKf;IAEF,OAAO,CAAC,uBAAuB;IAqB/B,UAAU;IAKV,MAAM;CAeP"}
@@ -35,8 +35,8 @@ class XrAnnotationInteraction extends Script {
35
35
  entity,
36
36
  script
37
37
  } = candidates[index];
38
- const camera = this.app.root.findByName('camera');
39
- const screen = camera?.camera?.worldToScreen(entity.getPosition());
38
+ const camera = this.app.root.findComponent('camera');
39
+ const screen = camera?.worldToScreen(entity.getPosition());
40
40
  script.onVrOpenCallback(screen?.x ?? 0, screen?.y ?? 0);
41
41
  }
42
42
  initialize() {
@@ -1 +1 @@
1
- {"version":3,"file":"xr-gaze-dwell.d.ts","sourceRoot":"","sources":["../../../src/components/VirtualWalkthrough/xr-gaze-dwell.ts"],"names":[],"mappings":"AAAA,OAAO,EAML,MAAM,EAKP,MAAM,YAAY,CAAC;AAmBpB,qBAAa,WAAY,SAAQ,MAAM;IACrC,MAAM,CAAC,UAAU,SAAiB;IAElC;6FACyF;IACzF,WAAW,SAAM;IAEjB,OAAO,CAAC,MAAM,CAAmC;IACjD,OAAO,CAAC,WAAW,CAAqC;IAExD,OAAO,CAAC,UAAU,CAAC,CAAS;IAC5B,OAAO,CAAC,YAAY,CAAC,CAAiB;IACtC,OAAO,CAAC,QAAQ,CAAC,CAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,CAAU;IAE3B,OAAO,CAAC,QAAQ,CAAc;IAC9B,OAAO,CAAC,QAAQ,CAAc;IAC9B,OAAO,CAAC,WAAW,CAAc;IACjC,OAAO,CAAC,QAAQ,CAAc;IAC9B,OAAO,CAAC,aAAa,CAAc;IAEnC,OAAO,CAAC,WAAW,CAAyE;IAE5F;;;;;OAKG;IACH,OAAO,CAAC,UAAU,CAGhB;IAEF,OAAO,CAAC,QAAQ,CAId;IAEF,OAAO,CAAC,cAAc;IAMtB,UAAU;IA4BV,MAAM,CAAC,EAAE,EAAE,MAAM;IAgCjB,OAAO,CAAC,UAAU;IAgBlB,uDAAuD;IACvD,OAAO,CAAC,SAAS;IAcjB,OAAO;CAiBR"}
1
+ {"version":3,"file":"xr-gaze-dwell.d.ts","sourceRoot":"","sources":["../../../src/components/VirtualWalkthrough/xr-gaze-dwell.ts"],"names":[],"mappings":"AAAA,OAAO,EAOL,MAAM,EAKP,MAAM,YAAY,CAAC;AAmBpB,qBAAa,WAAY,SAAQ,MAAM;IACrC,MAAM,CAAC,UAAU,SAAiB;IAElC;6FACyF;IACzF,WAAW,SAAM;IAEjB,OAAO,CAAC,MAAM,CAAmC;IACjD,OAAO,CAAC,WAAW,CAAqC;IAExD,OAAO,CAAC,UAAU,CAAC,CAAS;IAC5B,OAAO,CAAC,YAAY,CAAC,CAAiB;IACtC,OAAO,CAAC,QAAQ,CAAC,CAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,CAAU;IAE3B,OAAO,CAAC,QAAQ,CAAc;IAC9B,OAAO,CAAC,QAAQ,CAAc;IAC9B,OAAO,CAAC,WAAW,CAAc;IACjC,OAAO,CAAC,QAAQ,CAAc;IAC9B,OAAO,CAAC,aAAa,CAAc;IAEnC,OAAO,CAAC,WAAW,CAAyE;IAE5F;;;;;OAKG;IACH,OAAO,CAAC,UAAU,CAGhB;IAEF,OAAO,CAAC,QAAQ,CAId;IAEF,OAAO,CAAC,cAAc;IAMtB,UAAU;IA4BV,MAAM,CAAC,EAAE,EAAE,MAAM;IAgCjB,OAAO,CAAC,UAAU;IAgBlB,uDAAuD;IACvD,OAAO,CAAC,SAAS;IAcjB,OAAO;CAiBR"}
@@ -102,8 +102,8 @@ class XrGazeDwell extends Script {
102
102
  entity,
103
103
  script
104
104
  } = candidates[target];
105
- const camera = this.app.root.findByName('camera');
106
- const screen = camera?.camera?.worldToScreen(entity.getPosition());
105
+ const camera = this.app.root.findComponent('camera');
106
+ const screen = camera?.worldToScreen(entity.getPosition());
107
107
  script.onVrOpenCallback(screen?.x ?? 0, screen?.y ?? 0);
108
108
  }
109
109
  }
@@ -0,0 +1,12 @@
1
+ import { Entity } from 'playcanvas';
2
+ /**
3
+ * The camera entity the walkthrough owns, published by `VirtualWalkthroughViewer` for the hooks
4
+ * beneath it.
5
+ *
6
+ * Looking the camera up by name is not safe once the viewer shares a scene graph with a host that
7
+ * has entities of its own: `findByName('camera')` walks the whole graph and returns whichever
8
+ * entity was added first, so a host that names its own camera entity `camera` shadows the viewer's.
9
+ * Null outside a viewer, where the name lookup is still the only thing to go on.
10
+ */
11
+ export declare const CameraEntityContext: import("react").Context<Entity | null>;
12
+ //# sourceMappingURL=cameraEntityContext.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cameraEntityContext.d.ts","sourceRoot":"","sources":["../../src/hooks/cameraEntityContext.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAEpC;;;;;;;;GAQG;AACH,eAAO,MAAM,mBAAmB,wCAAqC,CAAC"}
@@ -0,0 +1,14 @@
1
+ import { createContext } from 'react';
2
+
3
+ /**
4
+ * The camera entity the walkthrough owns, published by `VirtualWalkthroughViewer` for the hooks
5
+ * beneath it.
6
+ *
7
+ * Looking the camera up by name is not safe once the viewer shares a scene graph with a host that
8
+ * has entities of its own: `findByName('camera')` walks the whole graph and returns whichever
9
+ * entity was added first, so a host that names its own camera entity `camera` shadows the viewer's.
10
+ * Null outside a viewer, where the name lookup is still the only thing to go on.
11
+ */
12
+ const CameraEntityContext = /*#__PURE__*/createContext(null);
13
+
14
+ export { CameraEntityContext };
@@ -1,4 +1,10 @@
1
- export declare const useCameraPosition: () => {
1
+ import { Entity } from 'playcanvas';
2
+ /**
3
+ * @param cameraEntity - The camera to drive. Callers above {@link CameraEntityContext} pass it
4
+ * directly; the ones beneath take it from the context, and anything with neither falls back to
5
+ * looking an entity named `camera` in the scene graph.
6
+ */
7
+ export declare const useCameraPosition: (cameraEntity?: Entity | null) => {
2
8
  setCamera: (focus: [number, number, number], position?: [number, number, number], horizontalNdcBias?: number) => void;
3
9
  getCameraState: () => {
4
10
  position: [number, number, number];
@@ -1 +1 @@
1
- {"version":3,"file":"useCameraPosition.d.ts","sourceRoot":"","sources":["../../src/hooks/useCameraPosition.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,iBAAiB;uBAIlB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;;kBAqBf,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;eACrB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;;CAQpF,CAAC"}
1
+ {"version":3,"file":"useCameraPosition.d.ts","sourceRoot":"","sources":["../../src/hooks/useCameraPosition.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,MAAM,EAAmB,MAAM,YAAY,CAAC;AAcrD;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,GAAI,eAAe,MAAM,GAAG,IAAI;uBAkBlD,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;;kBAmBf,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;eACrB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;;CAQpF,CAAC"}
@@ -1,22 +1,44 @@
1
- import { useCallback, useMemo } from 'react';
1
+ import { useContext, useCallback, useMemo } from 'react';
2
2
  import { useApp } from '@playcanvas/react/hooks';
3
3
  import { Vec3 } from 'playcanvas';
4
+ import { CameraEntityContext } from './cameraEntityContext.js';
4
5
 
5
- const useCameraPosition = () => {
6
+ /** The camera-driving scripts this hook talks to. */
7
+
8
+ /** Scripts are added to entities dynamically, so they aren't on the engine's types. */
9
+ const scriptsOn = node => node?.script;
10
+
11
+ /**
12
+ * @param cameraEntity - The camera to drive. Callers above {@link CameraEntityContext} pass it
13
+ * directly; the ones beneath take it from the context, and anything with neither falls back to
14
+ * looking an entity named `camera` in the scene graph.
15
+ */
16
+ const useCameraPosition = cameraEntity => {
6
17
  const app = useApp();
18
+ const contextCamera = useContext(CameraEntityContext);
19
+ const resolvedCamera = cameraEntity ?? contextCamera;
20
+ const findCamera = useCallback(() => resolvedCamera ?? app.root.findByName('camera'), [app, resolvedCamera]);
21
+
22
+ /**
23
+ * The script driving the camera. WalkthroughCamera is mounted on the rig above the camera rather
24
+ * than on the camera entity, so that it poses a camera the walkthrough adopted from a host scene
25
+ * as readily as one of its own; CameraControls sits on the camera entity itself.
26
+ */
27
+ const findControls = useCallback(camera => {
28
+ const own = scriptsOn(camera);
29
+ return own?.cameraControls ?? own?.walkthroughCamera ?? scriptsOn(camera?.parent)?.walkthroughCamera;
30
+ }, []);
7
31
  const setCamera = useCallback((focus, position, horizontalNdcBias = 0) => {
8
- const camera = app.root.findByName('camera');
9
- // @ts-expect-error - scripts are added dynamically to the camera entity
10
- const controls = camera?.script?.cameraControls ?? camera?.script?.walkthroughCamera;
32
+ const camera = findCamera();
33
+ const controls = findControls(camera);
11
34
  if (controls && camera) {
12
35
  // The extra bias arg is only honored by WalkthroughCamera; CameraControls ignores it.
13
36
  controls.reset(new Vec3(focus), position ? new Vec3(position) : camera.getPosition(), horizontalNdcBias);
14
37
  }
15
- }, [app]);
38
+ }, [findCamera, findControls]);
16
39
  const getCameraState = useCallback(() => {
17
- const camera = app.root.findByName('camera');
18
- // @ts-expect-error - scripts are added dynamically to the camera entity
19
- const controls = camera?.script?.cameraControls ?? camera?.script?.walkthroughCamera;
40
+ const camera = findCamera();
41
+ const controls = findControls(camera);
20
42
  if (camera && controls) {
21
43
  const position = camera.getPosition();
22
44
  const focusPoint = controls?.focusPoint;
@@ -26,7 +48,7 @@ const useCameraPosition = () => {
26
48
  };
27
49
  }
28
50
  return null;
29
- }, [app]);
51
+ }, [findCamera, findControls]);
30
52
  return useMemo(() => ({
31
53
  setCamera,
32
54
  getCameraState
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@terraware/web-components",
3
- "version": "5.0.6",
3
+ "version": "5.0.7-rc.2",
4
4
  "author": "Terraformation Inc.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {